Related
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
A geofence is a virtual perimeter set on a real geographic area. Combining a user position with a geofence perimeter, it is possible to know if the user is inside the geofence or if he is exiting or entering the area.
In this article, we will discuss how to use the geofence to notify the user when the device enters/exits an area using the HMS Location Kit in a Xamarin.Android application. We will also add and customize HuaweiMap, which includes drawing circles, adding pointers, and using nearby searches in search places. We are going to learn how to use the below features together:
Geofence
Reverse Geocode
HuaweiMap
Nearby Search
First of all, you need to be a registered Huawei Mobile Developer and create an application in Huawei App Console in order to use HMS Map Location and Site Kits. You can follow there steps in to complete the configuration that required for development.
Configuring App Information in AppGallery Connect --> shorturl.at/rL347
Creating Xamarin Android Binding Libraries --> shorturl.at/rBP46
Integrating the HMS Map Kit Libraries for Xamarin --> shorturl.at/vAHPX
Integrating the HMS Location Kit Libraries for Xamarin --> shorturl.at/dCX07
Integrating the HMS Site Kit Libraries for Xamarin --> shorturl.at/bmDX6
Integrating the HMS Core SDK --> shorturl.at/qBISV
Setting Package in Xamarin --> shorturl.at/brCU1
When we create our Xamarin.Android application in the above steps, we need to make sure that the package name is the same as we entered the Console. Also, don’t forget the enable them in Console.
Manifest & Permissions
We have to update the application’s manifest file by declaring permissions that we need as shown below.
Code:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Also, add a meta-data element to embed your app id in the application tag, it is required for this app to authenticate on the Huawei’s cloud server. You can find this id in agconnect-services.json file.
Code:
<meta-data android:name="com.huawei.hms.client.appid" android:value="appid=YOUR_APP_ID" />
Request location permission
Code:
private void RequestPermissions()
{
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.AccessCoarseLocation) != (int)Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.AccessFineLocation) != (int)Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != (int)Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadExternalStorage) != (int)Permission.Granted ||
ContextCompat.CheckSelfPermission(this, Manifest.Permission.Internet) != (int)Permission.Granted)
{
ActivityCompat.RequestPermissions(this,
new System.String[]
{
Manifest.Permission.AccessCoarseLocation,
Manifest.Permission.AccessFineLocation,
Manifest.Permission.WriteExternalStorage,
Manifest.Permission.ReadExternalStorage,
Manifest.Permission.Internet
},
100);
}
else
GetCurrentPosition();
}
Add a Map
Add a <fragment> element to your activity’s layout file, activity_main.xml. This element defines a MapFragment to act as a container for the map and to provide access to the HuaweiMap object.
Code:
<fragment
android:id="@+id/mapfragment"
class="com.huawei.hms.maps.MapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Implement the IOnMapReadyCallback interface to MainActivity and override OnMapReady method which is triggered when the map is ready to use. Then use GetMapAsync to register for the map callback.
We request the address corresponding to a given latitude/longitude. Also specified that the output must be in JSON format.
Code:
public class MainActivity : AppCompatActivity, IOnMapReadyCallback
{
...
public void OnMapReady(HuaweiMap map)
{
hMap = map;
hMap.UiSettings.MyLocationButtonEnabled = true;
hMap.UiSettings.CompassEnabled = true;
hMap.UiSettings.ZoomControlsEnabled = true;
hMap.UiSettings.ZoomGesturesEnabled = true;
hMap.MyLocationEnabled = true;
hMap.MapClick += HMap_MapClick;
if (selectedCoordinates == null)
selectedCoordinates = new GeofenceModel { LatLng = CurrentPosition, Radius = 30 };
}
}
As you can see above, with the UiSettings property of the HuaweiMap object we set my location button, enable compass, etc. Now when the app launch, directly get the current location and move the camera to it. In order to do that we use FusedLocationProviderClient that we instantiated and call LastLocation API.
LastLocation API returns a Task object that we can check the result by implementing the relevant listeners for success and failure.In success listener we are going to move the map’s camera position to the last known position.
Code:
private void GetCurrentPosition()
{
var locationTask = fusedLocationProviderClient.LastLocation;
locationTask.AddOnSuccessListener(new LastLocationSuccess(this));
locationTask.AddOnFailureListener(new LastLocationFail(this));
}
...
public class LastLocationSuccess : Java.Lang.Object, IOnSuccessListener
{
...
public void OnSuccess(Java.Lang.Object location)
{
Toast.MakeText(mainActivity, "LastLocation request successful", ToastLength.Long).Show();
if (location != null)
{
MainActivity.CurrentPosition = new LatLng((location as Location).Latitude, (location as Location).Longitude);
mainActivity.RepositionMapCamera((location as Location).Latitude, (location as Location).Longitude);
}
}
}
To change the position of the camera, we must specify where we want to move the camera, using a CameraUpdate. The Map Kit allows us to create many different types of CameraUpdate using CameraUpdateFactory.
There are some methods for the camera position changes as we see above. Simply these are:
NewLatLng: Change camera’s latitude and longitude, while keeping other properties
NewLatLngZoom: Changes the camera’s latitude, longitude, and zoom, while keeping other properties
NewCameraPosition: Full flexibility in changing the camera position
We are going to use NewCameraPosition. A CameraPosition can be obtained with a CameraPosition.Builder. And then we can set target, bearing, tilt and zoom properties.
Code:
public void RepositionMapCamera(double lat, double lng)
{
var cameraPosition = new CameraPosition.Builder();
cameraPosition.Target(new LatLng(lat, lng));
cameraPosition.Zoom(1000);
cameraPosition.Bearing(45);
cameraPosition.Tilt(20);
CameraUpdate cameraUpdate = CameraUpdateFactory.NewCameraPosition(cameraPosition.Build());
hMap.MoveCamera(cameraUpdate);
}
Creating Geofence
In this part, we will choose the location where we want to set geofence in two different ways. The first is to select the location by clicking on the map, and the second is to search for nearby places by keyword and select one after placing them on the map with the marker.
Set the geofence location by clicking on the map
It is always easier to select a location by seeing it. After this section, we are able to set a geofence around the clicked point when the map’s clicked. We attached the Click event to our map in the OnMapReady method. In this Click event, we will add a marker to the clicked point and draw a circle around it.
Also, we will use the Seekbar at the bottom of the page to adjust the circle radius. We set selectedCoordinates variable when adding the marker. Let’s create the following method to create the marker:
Code:
private void HMap_MapClick(object sender, HuaweiMap.MapClickEventArgs e)
{
selectedCoordinates.LatLng = e.P0;
if (circle != null)
{
circle.Remove();
circle = null;
}
AddMarkerOnMap();
}
void AddMarkerOnMap()
{
if (marker != null) marker.Remove();
var markerOption = new MarkerOptions()
.InvokeTitle("You are here now")
.InvokePosition(selectedCoordinates.LatLng);
hMap.SetInfoWindowAdapter(new MapInfoWindowAdapter(this));
marker = hMap.AddMarker(markerOption);
bool isInfoWindowShown = marker.IsInfoWindowShown;
if (isInfoWindowShown)
marker.HideInfoWindow();
else
marker.ShowInfoWindow();
}
Adding MapInfoWindowAdapter class to our project for rendering the custom info model. And implement HuaweiMap.IInfoWindowAdapter interface to it. When an information window needs to be displayed for a marker, methods provided by this adapter are called in any case.
Now let’s create a custom info window layout and named it as map_info_view.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:text="Add geofence"
android:width="100dp"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnInfoWindow" />
</LinearLayout>
And return it after customizing it in GetInfoWindow() method. The full code of the adapter is below:
Code:
internal class MapInfoWindowAdapter : Java.Lang.Object, HuaweiMap.IInfoWindowAdapter
{
private MainActivity activity;
private GeofenceModel selectedCoordinates;
private View addressLayout;
public MapInfoWindowAdapter(MainActivity currentActivity){activity = currentActivity;}
public View GetInfoContents(Marker marker){return null;}
public View GetInfoWindow(Marker marker)
{
if (marker == null)
return null;
selectedCoordinates = new GeofenceModel { LatLng = new LatLng(marker.Position.Latitude, marker.Position.Longitude) };
View mapInfoView = activity.LayoutInflater.Inflate(Resource.Layout.map_info_view, null);
var radiusBar = activity.FindViewById<SeekBar>(Resource.Id.radiusBar);
if (radiusBar.Visibility == Android.Views.ViewStates.Invisible)
{
radiusBar.Visibility = Android.Views.ViewStates.Visible;
radiusBar.SetProgress(30, true);
}
activity.FindViewById<SeekBar>(Resource.Id.radiusBar)?.SetProgress(30, true);
activity.DrawCircleOnMap(selectedCoordinates);
Button button = mapInfoView.FindViewById<Button>(Resource.Id.btnInfoWindow);
button.Click += btnInfoWindow_ClickAsync;
return mapInfoView;
}
}
Now we create a method to arrange a circle around the marker that representing the geofence radius. Create a new DrawCircleOnMap method in MainActivity for this. To construct a circle, we must specify the Center and Radius. Also, I set other properties like StrokeColor etc.
Code:
public void DrawCircleOnMap(GeofenceModel geoModel)
{
if (circle != null)
{
circle.Remove();
circle = null;
}
CircleOptions circleOptions = new CircleOptions()
.InvokeCenter(geoModel.LatLng)
.InvokeRadius(geoModel.Radius)
.InvokeFillColor(Color.Argb(50, 0, 14, 84))
.InvokeStrokeColor(Color.Yellow)
.InvokeStrokeWidth(15);
circle = hMap.AddCircle(circleOptions);
}
private void radiusBar_ProgressChanged(object sender, SeekBar.ProgressChangedEventArgs e)
{
selectedCoordinates.Radius = e.Progress;
DrawCircleOnMap(selectedCoordinates);
}
We will use SeekBar to change the radius of the circle. As the value changes, the drawn circle will expand or shrink.
Reverse Geocoding
Now let’s handle the click event of the info window.
But before open that window, we need to reverse geocoding selected coordinates to getting a formatted address. HUAWEI Site Kit provides us a set of HTTP API including the one that we need, reverseGeocode.
Let’s add the GeocodeManager class to our project and update it as follows:
Code:
public async Task<Site> ReverseGeocode(double lat, double lng)
{
string result = "";
using (var client = new HttpClient())
{
MyLocation location = new MyLocation();
location.Lat = lat;
location.Lng = lng;
var root = new ReverseGeocodeRequest();
root.Location = location;
var settings = new JsonSerializerSettings();
settings.ContractResolver = new LowercaseSerializer();
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = "siteapi.cloud.huawei.com/mapApi/v1/siteService/reverseGeocode?key=" + Android.Net.Uri.Encode(ApiKey);
var response = await client.PostAsync(url, data);
result = response.Content.ReadAsStringAsync().Result;
}
return JsonConvert.DeserializeObject<ReverseGeocodeResponse>(result).sites.FirstOrDefault();
}
In the above code, we request the address corresponding to a given latitude/longitude. Also specified that the output must be in JSON format.
siteapi.cloud.huawei.com/mapApi/v1/siteService/reverseGeocode?key=APIKEY
Click to expand...
Click to collapse
Request model:
Code:
public class MyLocation
{
public double Lat { get; set; }
public double Lng { get; set; }
}
public class ReverseGeocodeRequest
{
public MyLocation Location { get; set; }
}
Note that the JSON response contains three root elements:
“returnCode”: For details, please refer to Result Codes.
“returnDesc”: description
“sites” contains an array of geocoded address information
Generally, only one entry in the “sites” array is returned for address lookups, though the geocoder may return several results when address queries are ambiguous.
Add the following codes to our MapInfoWindowAdapter where we get results from the Reverse Geocode API and set the UI elements.
Code:
private async void btnInfoWindow_ClickAsync(object sender, System.EventArgs e)
{
addressLayout = activity.LayoutInflater.Inflate(Resource.Layout.reverse_alert_layout, null);
GeocodeManager geocodeManager = new GeocodeManager(activity);
var addressResult = await geocodeManager.ReverseGeocode(selectedCoordinates.LatLng.Latitude, selectedCoordinates.LatLng.Longitude);
if (addressResult.ReturnCode != 0)
return;
var address = addressResult.Sites.FirstOrDefault();
var txtAddress = addressLayout.FindViewById<TextView>(Resource.Id.txtAddress);
var txtRadius = addressLayout.FindViewById<TextView>(Resource.Id.txtRadius);
txtAddress.Text = address.FormatAddress;
txtRadius.Text = selectedCoordinates.Radius.ToString();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.SetView(addressLayout);
builder.SetTitle(address.Name);
builder.SetPositiveButton("Save", (sender, arg) =>
{
selectedCoordinates.Conversion = GetSelectedConversion();
GeofenceManager geofenceManager = new GeofenceManager(activity);
geofenceManager.AddGeofences(selectedCoordinates);
});
builder.SetNegativeButton("Cancel", (sender, arg) => { builder.Dispose(); });
AlertDialog alert = builder.Create();
alert.Show();
}
Now, after selecting the conversion, we can complete the process by calling the AddGeofence method in the GeofenceManager class by pressing the save button in the dialog window.
Code:
public void AddGeofences(GeofenceModel geofenceModel)
{
//Set parameters
geofenceModel.Id = Guid.NewGuid().ToString();
if (geofenceModel.Conversion == 5) //Expiration value that indicates the geofence should never expire.
geofenceModel.Timeout = Geofence.GeofenceNeverExpire;
else
geofenceModel.Timeout = 10000;
List<IGeofence> geofenceList = new List<IGeofence>();
//Geofence Service
GeofenceService geofenceService = LocationServices.GetGeofenceService(activity);
PendingIntent pendingIntent = CreatePendingIntent();
GeofenceBuilder somewhereBuilder = new GeofenceBuilder()
.SetUniqueId(geofenceModel.Id)
.SetValidContinueTime(geofenceModel.Timeout)
.SetRoundArea(geofenceModel.LatLng.Latitude, geofenceModel.LatLng.Longitude, geofenceModel.Radius)
.SetDwellDelayTime(10000)
.SetConversions(geofenceModel.Conversion); ;
//Create geofence request
geofenceList.Add(somewhereBuilder.Build());
GeofenceRequest geofenceRequest = new GeofenceRequest.Builder()
.CreateGeofenceList(geofenceList)
.Build();
//Register geofence
var geoTask = geofenceService.CreateGeofenceList(geofenceRequest, pendingIntent);
geoTask.AddOnSuccessListener(new CreateGeoSuccessListener(activity));
geoTask.AddOnFailureListener(new CreateGeoFailListener(activity));
}
In the AddGeofence method, we need to set the geofence request parameters, like the selected conversion, unique Id and timeout according to conversion, etc. with GeofenceBuilder. We create GeofenceBroadcastReceiver and display a toast message when a geofence action occurs.
Code:
[BroadcastReceiver(Enabled = true)]
[IntentFilter(new[] { "com.huawei.hms.geofence.ACTION_PROCESS_ACTIVITY" })]
class GeofenceBroadcastReceiver : BroadcastReceiver
{
public static readonly string ActionGeofence = "com.huawei.hms.geofence.ACTION_PROCESS_ACTIVITY";
public override void OnReceive(Context context, Intent intent)
{
if (intent != null)
{
var action = intent.Action;
if (action == ActionGeofence)
{
GeofenceData geofenceData = GeofenceData.GetDataFromIntent(intent);
if (geofenceData != null)
{
Toast.MakeText(context, "Geofence triggered: " + geofenceData.ConvertingLocation.Latitude +"\n" + geofenceData.ConvertingLocation.Longitude + "\n" + geofenceData.Conversion.ToConversionName(), ToastLength.Long).Show();
}
}
}
}
}
After that in CreateGeoSuccessListener and CreateGeoFailureListener that we implement IOnSuccessListener and IOnFailureListener respectively, we display a toast message to the user like this:
Code:
public class CreateGeoFailListener : Java.Lang.Object, IOnFailureListener
{
public void OnFailure(Java.Lang.Exception ex)
{
Toast.MakeText(mainActivity, "Geofence request failed: " + GeofenceErrorCodes.GetErrorMessage((ex as ApiException).StatusCode), ToastLength.Long).Show();
}
}
public class CreateGeoSuccessListener : Java.Lang.Object, IOnSuccessListener
{
public void OnSuccess(Java.Lang.Object data)
{
Toast.MakeText(mainActivity, "Geofence request successful", ToastLength.Long).Show();
}
}
Set geofence location using Nearby Search
On the main layout when the user clicks the Search Nearby Places button, a search dialog like below appears:
Create search_alert_layout.xml with a search input In Main Activity, create click event of that button and open an alert dialog after it’s view is set to search_alert_layout. And make NearbySearch when clicking the Search button:
Code:
private void btnGeoWithAddress_Click(object sender, EventArgs e)
{
search_view = base.LayoutInflater.Inflate(Resource.Layout.search_alert_layout, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.SetView(search_view);
builder.SetTitle("Search Location");
builder.SetNegativeButton("Cancel", (sender, arg) => { builder.Dispose(); });
search_view.FindViewById<Button>(Resource.Id.btnSearch).Click += btnSearchClicked;
alert = builder.Create();
alert.Show();
}
private void btnSearchClicked(object sender, EventArgs e)
{
string searchText = search_view.FindViewById<TextView>(Resource.Id.txtSearch).Text;
GeocodeManager geocodeManager = new GeocodeManager(this);
geocodeManager.NearbySearch(CurrentPosition, searchText);
}
We pass search text and Current Location into the GeocodeManager NearbySearch method as parameters. We need to modify GeoCodeManager class and add nearby search method to it.
Code:
public void NearbySearch(LatLng currentLocation, string searchText)
{
ISearchService searchService = SearchServiceFactory.Create(activity, Android.Net.Uri.Encode("YOUR_API_KEY"));
NearbySearchRequest nearbySearchRequest = new NearbySearchRequest();
nearbySearchRequest.Query = searchText;
nearbySearchRequest.Language = "en";
nearbySearchRequest.Location = new Coordinate(currentLocation.Latitude, currentLocation.Longitude);
nearbySearchRequest.Radius = (Integer)2000;
nearbySearchRequest.PageIndex = (Integer)1;
nearbySearchRequest.PageSize = (Integer)5;
nearbySearchRequest.PoiType = LocationType.Address;
searchService.NearbySearch(nearbySearchRequest, new QuerySuggestionResultListener(activity as MainActivity));
}
And to handle the result we must create a listener and implement the ISearchResultListener interface to it.
Code:
public class NearbySearchResultListener : Java.Lang.Object, ISearchResultListener
{
public void OnSearchError(SearchStatus status)
{
Toast.MakeText(context, "Error Code: " + status.ErrorCode + " Error Message: " + status.ErrorMessage, ToastLength.Long);
}
public void OnSearchResult(Java.Lang.Object results)
{
NearbySearchResponse nearbySearchResponse = (NearbySearchResponse)results;
if (nearbySearchResponse != null && nearbySearchResponse.TotalCount > 0)
context.SetSearchResultOnMap(nearbySearchResponse.Sites);
}
}
In OnSearchResult method, NearbySearchResponse object return. We will insert markers to the mapper element in this response. The map will look like this:
In Main Activity create a method named SetSearchResultOnMap and pass IList<Site> as a parameter to insert multiple markers on the map.
Code:
public void SetSearchResultOnMap(IList<Com.Huawei.Hms.Site.Api.Model.Site> sites)
{
hMap.Clear();
if (searchMarkers != null && searchMarkers.Count > 0)
foreach (var item in searchMarkers)
item.Remove();
searchMarkers = new List<Marker>();
for (int i = 0; i < sites.Count; i++)
{
MarkerOptions marker1Options = new MarkerOptions()
.InvokePosition(new LatLng(sites[i].Location.Lat, sites[i].Location.Lng))
.InvokeTitle(sites[i].Name).Clusterable(true);
hMap.SetInfoWindowAdapter(new MapInfoWindowAdapter(this));
var marker1 = hMap.AddMarker(marker1Options);
searchMarkers.Add(marker1);
RepositionMapCamera(sites[i].Location.Lat, sites[i].Location.Lng);
}
hMap.SetMarkersClustering(true);
alert.Dismiss();
}
Now, we add markers as we did above. But here we use SetMarkersClustering(true) to consolidates markers into clusters when zooming out of the map.
You can download the source code from below:
github.com/stugcearar/HMSCore-Xamarin-Android-Samples/tree/master/LocationKit/HMS_Geofence
Also if you have any questions, ask away in Huawei Developer Forums.
Errors
If your location permission set “Allowed only while in use instead” of ”Allowed all the time” below exception will be thrown.
int GEOFENCE_INSUFFICIENT_PERMISSION
Insufficient permission to perform geofence-related operations.
You can see all result codes including errors, in here for Location service.
You can find result codes with details here for Geofence request.
In a previous post I've shown you how to use the HQUIC kit to perform a simple GET request to download the latest local news by using a third party API. At this point everything is ok, but, what if I want to send a request with headers? or, how can I perform a POST request?. If you have made the same questions, please keep reading.
Previous requirements
An Android Studio project
Integrating the HQUIC SDK
HQUC will perform HTTP requests over the QUIC protocol to let your users enjoy faster connections with lower bandwidth. If the remote server does not support QUIC, the kit will use HTTP V2 instead, so, you just need to code once.
To add the HQUIC kit to your app, add the next dependency to your app level build.gradle file
Code:
implementation 'com.huawei.hms:hquic-provider:5.0.0.300'
Sync your project and you will be ready to use the HQUIC SDK. We will reuse the HQUICService class provided on the HQUIC sample code, but with little modifications
Code:
class HQUICService(val context: Context) {
private val TAG = "HQUICService"
private val DEFAULT_PORT = 443
private val DEFAULT_ALTERNATEPORT = 443
private val executor: Executor = Executors.newSingleThreadExecutor()
private var cronetEngine: CronetEngine? = null
private var callback: UrlRequest.Callback? = null
/**
* Asynchronous initialization.
*/
init {
HQUICManager.asyncInit(
context,
object : HQUICManager.HQUICInitCallback {
override fun onSuccess() {
Log.i(TAG, "HQUICManager asyncInit success")
}
override fun onFail(e: Exception?) {
Log.w(TAG, "HQUICManager asyncInit fail")
}
})
}
/**
* Create a Cronet engine.
*
* @param url URL.
* @return cronetEngine Cronet engine.
*/
private fun createCronetEngine(url: String): CronetEngine? {
if (cronetEngine != null) {
return cronetEngine
}
val builder = CronetEngine.Builder(context)
builder.enableQuic(true)
builder.addQuicHint(getHost(url), DEFAULT_PORT, DEFAULT_ALTERNATEPORT)
cronetEngine = builder.build()
return cronetEngine
}
/**
* Construct a request
*
* @param url Request URL.
* @param method method Method type.
* @return UrlRequest urlrequest instance.
*/
private fun builRequest(
url: String,
method: String,
headers: HashMap<String, String>?,
body:ByteArray?
): UrlRequest? {
val cronetEngine: CronetEngine? = createCronetEngine(url)
val requestBuilder = cronetEngine?.newUrlRequestBuilder(url, callback, executor)
requestBuilder?.apply {
setHttpMethod(method)
if(method=="POST"){
body?.let {
setUploadDataProvider(UploadDataProviders.create(ByteBuffer.wrap(it)), executor) }
}
headers?.let{
for (key in it.keys) {
addHeader(key, headers[key])
}
}
return build()
}
return null
}
/**
* Send a request to the URL.
*
* @param url Request URL.
* @param method Request method type.
*/
fun sendRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null) {
Log.i(TAG, "callURL: url is " + url + "and method is " + method)
val urlRequest: UrlRequest? = builRequest(url, method, headers,body)
urlRequest?.apply { urlRequest.start() }
}
/**
* Parse the domain name to obtain the host name.
*
* @param url Request URL.
* @return host Host name.
*/
private fun getHost(url: String): String? {
var host: String? = null
try {
val url1 = URL(url)
host = url1.host
} catch (e: MalformedURLException) {
Log.e(TAG, "getHost: ", e)
}
return host
}
fun setCallback(mCallback: UrlRequest.Callback?) {
callback = mCallback
}
}
The sendRequest method has been modified to receive a HashMap with the headers, and a ByteArray with the Body payload. Note the sendRequest method if the body or the headers are not null, will be added to the request.
Code:
requestBuilder?.apply {
setHttpMethod(method)
if(method=="POST"){
body?.let {//Adding the request Body
setUploadDataProvider(UploadDataProviders.create(ByteBuffer.wrap(it)), executor) }
}
headers?.let{
for (key in it.keys) {//Adding all the headers
addHeader(key, headers[key])
}
}
With that modifications can perform an HTTP request by this way
Code:
val map=HashMap<String,String>()
map["Content-Type"] = "application/json"
val body=JSONObject().apply {
put("key1","value1")
put("key2","value2")
}
HQUICService(context).sendRequest(HOST,"POST",map,body.toString().toByteArray())
That's enough to send a request, but, what about the response? HQUIC provides an Abstract Class for listening the request events. All we need is inherit from UrlRequest.Callback. Let's do it.
Code:
class HQUICClient(context: Context) : UrlRequest.Callback() {
var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="QUICClient"
val response=ByteArrayOutputStream()
var listener:QuicClientListener?=null
init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}
fun makeRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null){
hquicService?.sendRequest(url,method,headers,body)
}
override fun onRedirectReceived(
request: UrlRequest?,
info: UrlResponseInfo?,
newLocationUrl: String?
) {
request?.followRedirect()
}
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request?.read(byteBuffer)
}
override fun onReadCompleted(
request: UrlRequest?,
info: UrlResponseInfo?,
byteBuffer: ByteBuffer?
) {
byteBuffer?.apply {
response.write(array(),arrayOffset(),position())
response.flush()
}
request?.read(ByteBuffer.allocateDirect(CAPACITY))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
listener?.onSuccess(response.toByteArray())
}
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
listener?.apply { onFailure(error.toString()) }
}
Remember, certain number of bytes can be readed per time, so for long responses, the method onReadCompleated will be called multiple times until the response has been successfully readed, or an error ocurs. When the operation is complete, the onSucceeded callback will be called and you will be able to parse the response. If the request fails, you will get an exception on the onFailed callback.
To report the request result, you can create a public interface
Code:
interface HQUICClientListener{
fun onSuccess(response: ByteArray)
fun onFailure(error: String)
}
And then, if the response is succesful, you can parse your byte array properly.
Code:
override fun onSuccess(response: ByteArray) {
//For text
Log.i(TAG, String(response))
//For images
BitmapFactory.decodeByteArray(response,0,response.size)
}
Conclusion
With HQUIC you can easily create a REST client for your android app, taking advantage of the QUIC features and keeping HTTP 2 compatibility.
Reference
HQUIC developer guide
Overview
In the previous post, we learned how to integrate the UserDetect API in Safety Detect into your app to identify fake users. I've been glad to receive so much positive feedback. In this post, I'll introduce SysIntegrity (system integrity check), yet another handy function in Safety Detect. Apps can face malicious attacks when they run on insecure devices, for example, rooted devices. For certain apps, such as e-commerce apps, ensuring a secure running environment during user transactions is particularly crucial. SysIntegrity enables apps to do so by helping detect device system-related risks in advance.
I encountered this function on the HUAWEI Developers website. SysIntegrity works by integrating the Safety Detect SDK into your app, and calling the SysIntegrity API in Safety Detect. It performs the check within a Trusted Execution Environment (TEE) and signs the check result with an X.509 digital certificate to ensure that it is trustworthy and tamperproof.
Now let's take a look at what it's like in practice.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Next, I'll show you how to integrate SysIntegrity.
Content
1 Preparations
1.1 Installing Android studio
1.2 Configuring App Information in AppGallery Connect
1.3 Configuring the Huawei Maven Repository Address
1.4 Adding Build Dependencies
1.5 Configuring Obfuscation Scripts
2 Code Development
2.1 Creating a SafetyDetectClient Instance and Generating a Nonce
2.2 Calling the SysIntegrity API
2.3 Verifying the Check Result on the App Server
2.4 Obtaining the System Integrity Check Result
1. Preparations
1.1 Installing Android Studio
To install Android Studio, please refer to the following:
l Android Studio Official Website
l Android Studio Installation and Configuration
1.2 Configuring App Information in AppGallery Connect
Before developing an app, you'll need to configure the app information in AppGallery Connect. For more details, please refer to Preparations.
1.3 Configuring the Huawei Maven Repository Address
Open the build.gradle file in the root directory of your Android Studio project.
Add the AppGallery Connect plug-in and the Maven repository address.
(1) Go to buildscript > repositories and configure the Maven repository address for the HMS Core SDK.
(2) Go to allprojects > repositories and configure the Maven repository address for the HMS Core SDK.
(3) If the agconnect-services.json file has been added to the app, go to buildscript > dependencies and add the AppGallery Connect plug-in configuration.
XML:
<p style="line-height: 1.5em;">apply plugin: 'com.huawei.agconnect'
</p>
Add the build dependency in the dependencies section.
XML:
<p style="line-height: 1.5em;">dependencies {
implementation 'com.huawei.hms:safetydetect:5.0.5.302'
}
</p>
1.5 Configuring Obfuscation Scripts
If you are using AndResGuard, add its trustlist to the build.gradle file in the app directory of your project. For more details about the code, please refer to Configuring Obfuscation Scripts on the HUAWEI Developers website.
2. Code Development
2.1 Creating a SafetyDetectClient Instance and Generating a Nonce
The nonce value will be contained in the check result. You can check the nonce value to verify that the returned result corresponds to your request, and does not encounter replay attacks. A nonce value should:
l Be used only once.
l Contain 16 to 66 bytes.
l Be derived from data sent to your server (Recommended).
2.2 Calling the SysIntegrity API
(1) The SysIntegrity API has two input parameters: the nonce value (obtained during the previous step) and your app ID. You can obtain the app ID from the agconnect-services.jsonfile under the app directory of your project.
Ø Sign in to AppGallery Connect and click My projects.
Ø Find your app project and click the desired app name.
Ø Go to Project Setting > General information, and view the app ID in the App information area.
Java:
<p style="line-height: 1.5em;">private void onAdapterItemClick(int position) {
// Call the SysIntegrity API to check for risks in the payment environment.
SafetyDetectUtil.detectSysIntegrity(this, new ICallBack<Boolean>() {
@Override
public void onSuccess(Boolean baseIntegrity) {
if (baseIntegrity) {
// The system integrity is not corrupted, and the user can proceed with the purchase.
buy(productInfo);
} else {
// The system integrity is corrupted. A popup is displayed to warn the user and ask the user whether to continue.
showRootTipDialog(productInfo);
}
}
…
});
}
</p>
(3) In my app, I encapsulated the SysIntegrity API in the detectSysIntegrity method of the SafetyDetectUtil.java class. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">public static void detectSysIntegrity(final Activity activity, final ICallBack<? super Boolean> callBack) {
// Generate a nonce value.
byte[] nonce = ("Sample" + System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8);
// Read the app_id field from the agconnect-services.json file in the app directory.
String appId = AGConnectServicesConfig.fromContext(activity).getString("client/app_id");
// Obtain the Safety Detect client, call the SysIntegrity API, and add a success event listener.
SysIntegrityRequest sysintegrityrequest = new SysIntegrityRequest();
sysintegrityrequest.setAppid(appId);
sysintegrityrequest.setNonce(nonce);
// PS256 or RS256
sysintegrityrequest.setAlg("RS256");
Task task = mClient.sysIntegrity(sysintegrityrequest);
task.addOnSuccessListener(new OnSuccessListener<SysIntegrityResp>() {
@Override
public void onSuccess(SysIntegrityResp response) {
// Call the getResult method of the SysIntegrityResp class to obtain the check result.
String jwsStr = response.getResult();
VerifyResultHandler verifyResultHandler = new VerifyResultHandler(jwsStr, callBack);
// Send the check result to your server for verification.
verifyJws(activity, jwsStr, verifyResultHandler);
}
});
}
</p>
(4) Here, I called the relevant app server API in the verifyJws method to verify the check result. The third parameter in this method is an object of the VerifyResultHandler class, which implements a callback API to process the verification result.
2.3 Verifying the Check Result on the App Server
After receiving the check result from the TSMS server, the app will send the result to the app server. The server uses the HUAWEI CBG Root CA certificate to verify the signature and certificate chain in the check result, and thereby determines whether the check result is valid.
The sample code for the app server to read the certificate and verify the JWS string is as follows:
(1) Parse the header, payload, and signature from the JWS string.
Java:
<p style="line-height: 1.5em;">public JwsVerifyResp verifyJws(JwsVerifyReq jwsVerifyReq) {
// Obtain the JWS information sent from the app to the server.
String jwsStr = jwsVerifyReq.getJws();
// Parse the JWS segments. A JWS has three fixed segments, which are separated by periods (.).
String[] jwsSplit = jwsStr.split("\\.");
try {
// Perform Base64 decoding on each segment and construct a JWS object for each decoded segment.
JWSObject jwsObject = new JWSObject(new Base64URL(jwsSplit[0]), new Base64URL(jwsSplit[1]), new Base64URL(jwsSplit[2]));
// Verify the JWS and set the verification result.
boolean result = VerifySignatureUtil.verifySignature(jwsObject);
// Construct the response body for check result verification on the app server.
JwsVerifyResp jwsVerifyResp = new JwsVerifyResp();
jwsVerifyResp.setResult(result);
} catch (ParseException | NoSuchAlgorithmException e) {
RUN_LOG.catching(e);
}
return jwsVerifyResp;
}
</p>
(2) Use the verifySignature method of the VerifySignatureUtil class to verify relevant information, including the JWS signature algorithm, certificate chain, host name in signing certificate, and JWS signature. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">public static boolean verifySignature(JWSObject jws) throws NoSuchAlgorithmException {
JWSAlgorithm jwsAlgorithm = jws.getHeader().getAlgorithm();
// 1. Verify the JWS signature algorithm.
if ("RS256".equals(jwsAlgorithm.getName())) {
// Verify the certificate chain and obtain an instance of the Signature class based on the signature algorithm to verify the signature.
return verify(Signature.getInstance("SHA256withRSA"), jws);
}
return false;
}
private static boolean verify(Signature signature, JWSObject jws) {
// Extract the certificate chain information from the JWS header and convert the certificate chain into a proper type for subsequent processing.
X509Certificate[] certs = extractX509CertChain(jws);
// 2. Verify the certificate chain.
try {
verifyCertChain(certs);
} catch (Exception e) {
return false;
}
// 3. Verify the domain name in the signing certificate (leaf certificate). The domain name must be sysintegrity.platform.hicloud.com.
try {
new DefaultHostnameVerifier().verify("sysintegrity.platform.hicloud.com", certs[0]);
} catch (SSLException e) {
return false;
}
// 4. Verify the JWS signature information using the public key obtained from the signing certificate.
PublicKey pubKey = certs[0].getPublicKey();
try {
// Use the public key obtained from the signing certificate to initialize the Signature instance.
signature.initVerify(pubKey);
// Extract the input signature from the JWS and pass it to the Signature instance.
signature.update(jws.getSigningInput());
// Use the Signature instance to verify the signature information.
return signature.verify(jws.getSignature().decode());
} catch (InvalidKeyException | SignatureException e) {
return false;
}
}
</p>
(3) Call the extractX509CertChain method to extract the certificate chain from the JWS header. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">private static X509Certificate[] extractX509CertChain(JWSObject jws) {
List<X509Certificate> certs = new ArrayList<>();
List<com.nimbusds.jose.util.Base64> x509CertChain = jws.getHeader().getX509CertChain();
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
certs.addAll(x509CertChain.stream().map(cert -> {
try {
return (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream(cert.decode()) );
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList()));
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return (X509Certificate[]) certs.toArray();
}
</p>
(4) Call the verifyCertChain method to verify the certificate chain. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">private static void verifyCertChain(X509Certificate[] certs) throws CertificateException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchProviderException, SignatureException {
// Verify the validity period and issuing relationship of each certificate one by one.
for (int i = 0; i < certs.length - 1; ++i) {
certs[i].checkValidity();
PublicKey pubKey = certs[i + 1].getPublicKey();
certs[i].verify(pubKey);
}
// Use the preset HUAWEI CBG Root CA certificate to verify the last certificate in the certificate chain.
PublicKey caPubKey = huaweiCbgRootCaCert.getPublicKey();
certs[certs.length - 1].verify(caPubKey);
}
</p>
(5) Load the HUAWEI CBG Root CA certificate in the static code snippet of the VerifySignatureUtil class. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">static {
// Load the preset HUAWEI CBG Root CA certificate.
File filepath = "~/certs/Huawei_cbg_root.cer";
try (FileInputStream in = new FileInputStream(filepath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
huaweiCbgRootCaCert = (X509Certificate) cf.generateCertificate(in);
} catch (IOException | CertificateException e) {
RUN_LOG.error("HUAWEI CBG root cert load failed!");
}
}
</p>
We have now verified the check result on the app server, and the verification result will be returned to the app for subsequent service processing.
2.4 Obtaining the System Integrity Check Result
(1) Once you complete the steps above, the app will obtain the reliable system integrity check result from the payload of the JWS string. Parse the system integrity check result from the callback API of the VerifyResultHandler class as follows:
Java:
<p style="line-height: 1.5em;">private static final class VerifyResultHandler implements ICallBack<Boolean> {
private final String jwsStr;
private final ICallBack<? super Boolean> callBack;
private VerifyResultHandler(String jwsStr, ICallBack<? super Boolean> callBack) {
this.jwsStr = jwsStr;
this.callBack = callBack;
}
@Override
public void onSuccess(Boolean verified) {
if (verified) {
// Extract the system integrity check result that has been successfully verified by the app server.
String payloadDetail = new String(Base64.decode(jwsStr.split("\\.")[1].getBytes(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8);
try {
final boolean basicIntegrity = new JSONObject(payloadDetail).getBoolean("basicIntegrity");
// Call back the system integrity check result.
callBack.onSuccess(basicIntegrity);
} catch (JSONException e) {
…
}
}
…
}
}
</p>
(2) The following is an example of the system integrity check response:
XML:
<p style="line-height: 1.5em;">{
"apkCertificateDigestSha256": [
"osaUtTsdAvezjQBaW3IhN3/fsc6NQ5KwKuAQXcfrxb4="
],
"apkDigestSha256": "vFcmE0uw5s+4tFjXF9rVycxk2xR1rXiZFHuuBFzTVy8=",
"apkPackageName": "com.example.mockthirdapp",
"basicIntegrity": false,
"detail": [
"root",
"unlocked"
],
"nonce": "UjJScmEyNGZWbTV4YTJNZw==",
"timestampMs": 1604048377137,
"advice": "RESTORE_TO_FACTORY_ROM"
}
</p>
(3) If the value of the basicIntegrity field in the check result is false, it means that the system integrity is corrupted. In this case, the app can notify the user of any potential risks.
Conclusion
You can complete the integration by referring to the Preparations on HUAWEI Developers website. Also, you can download the SysIntegrity sample code for both Java and Kotlin from the site. And the sample code for four other functions in addition to SysIntegrity is also provided on the site.
Here is the sample code I wrote for my app. Feel free to take a look for yourself!
My sample code
What are all the features we can achieve using this kit
How this TEE works ?
Here we go
Like
Serious work.
What is the criteria for checking the Malicious apps?
Yes
Very nice and secure.
Thanks
Does Safety detect's System integrity work on Rooted device?
Huawei made their own SafetyNet :O
But why would anyone want to do this? It's annoying to legitimate users who root their device, with a lot of the people in this forum being here.
(Well, there's MagiskHide but some apps even check your list of packages and crash without even telling the user that their device is rooted)
ask011 said:
Overview
In the previous post, we learned how to integrate the UserDetect API in Safety Detect into your app to identify fake users. I've been glad to receive so much positive feedback. In this post, I'll introduce SysIntegrity (system integrity check), yet another handy function in Safety Detect. Apps can face malicious attacks when they run on insecure devices, for example, rooted devices. For certain apps, such as e-commerce apps, ensuring a secure running environment during user transactions is particularly crucial. SysIntegrity enables apps to do so by helping detect device system-related risks in advance.
I encountered this function on the HUAWEI Developers website. SysIntegrity works by integrating the Safety Detect SDK into your app, and calling the SysIntegrity API in Safety Detect. It performs the check within a Trusted Execution Environment (TEE) and signs the check result with an X.509 digital certificate to ensure that it is trustworthy and tamperproof.
Now let's take a look at what it's like in practice.
Next, I'll show you how to integrate SysIntegrity.
Content
1 Preparations
1.1 Installing Android studio
1.2 Configuring App Information in AppGallery Connect
1.3 Configuring the Huawei Maven Repository Address
1.4 Adding Build Dependencies
1.5 Configuring Obfuscation Scripts
2 Code Development
2.1 Creating a SafetyDetectClient Instance and Generating a Nonce
2.2 Calling the SysIntegrity API
2.3 Verifying the Check Result on the App Server
2.4 Obtaining the System Integrity Check Result
1. Preparations
1.1 Installing Android Studio
To install Android Studio, please refer to the following:
l Android Studio Official Website
l Android Studio Installation and Configuration
1.2 Configuring App Information in AppGallery Connect
Before developing an app, you'll need to configure the app information in AppGallery Connect. For more details, please refer to Preparations.
1.3 Configuring the Huawei Maven Repository Address
Open the build.gradle file in the root directory of your Android Studio project.
Add the AppGallery Connect plug-in and the Maven repository address.
(1) Go to buildscript > repositories and configure the Maven repository address for the HMS Core SDK.
(2) Go to allprojects > repositories and configure the Maven repository address for the HMS Core SDK.
(3) If the agconnect-services.json file has been added to the app, go to buildscript > dependencies and add the AppGallery Connect plug-in configuration.
XML:
<p style="line-height: 1.5em;">apply plugin: 'com.huawei.agconnect'
</p>
Add the build dependency in the dependencies section.
XML:
<p style="line-height: 1.5em;">dependencies {
implementation 'com.huawei.hms:safetydetect:5.0.5.302'
}
</p>
1.5 Configuring Obfuscation Scripts
If you are using AndResGuard, add its trustlist to the build.gradle file in the app directory of your project. For more details about the code, please refer to Configuring Obfuscation Scripts on the HUAWEI Developers website.
2. Code Development
2.1 Creating a SafetyDetectClient Instance and Generating a Nonce
The nonce value will be contained in the check result. You can check the nonce value to verify that the returned result corresponds to your request, and does not encounter replay attacks. A nonce value should:
l Be used only once.
l Contain 16 to 66 bytes.
l Be derived from data sent to your server (Recommended).
2.2 Calling the SysIntegrity API
(1) The SysIntegrity API has two input parameters: the nonce value (obtained during the previous step) and your app ID. You can obtain the app ID from the agconnect-services.jsonfile under the app directory of your project.
Ø Sign in to AppGallery Connect and click My projects.
Ø Find your app project and click the desired app name.
Ø Go to Project Setting > General information, and view the app ID in the App information area.
Java:
<p style="line-height: 1.5em;">private void onAdapterItemClick(int position) {
// Call the SysIntegrity API to check for risks in the payment environment.
SafetyDetectUtil.detectSysIntegrity(this, new ICallBack<Boolean>() {
@Override
public void onSuccess(Boolean baseIntegrity) {
if (baseIntegrity) {
// The system integrity is not corrupted, and the user can proceed with the purchase.
buy(productInfo);
} else {
// The system integrity is corrupted. A popup is displayed to warn the user and ask the user whether to continue.
showRootTipDialog(productInfo);
}
}
…
});
}
</p>
(3) In my app, I encapsulated the SysIntegrity API in the detectSysIntegrity method of the SafetyDetectUtil.java class. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">public static void detectSysIntegrity(final Activity activity, final ICallBack<? super Boolean> callBack) {
// Generate a nonce value.
byte[] nonce = ("Sample" + System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8);
// Read the app_id field from the agconnect-services.json file in the app directory.
String appId = AGConnectServicesConfig.fromContext(activity).getString("client/app_id");
// Obtain the Safety Detect client, call the SysIntegrity API, and add a success event listener.
SysIntegrityRequest sysintegrityrequest = new SysIntegrityRequest();
sysintegrityrequest.setAppid(appId);
sysintegrityrequest.setNonce(nonce);
// PS256 or RS256
sysintegrityrequest.setAlg("RS256");
Task task = mClient.sysIntegrity(sysintegrityrequest);
task.addOnSuccessListener(new OnSuccessListener<SysIntegrityResp>() {
@Override
public void onSuccess(SysIntegrityResp response) {
// Call the getResult method of the SysIntegrityResp class to obtain the check result.
String jwsStr = response.getResult();
VerifyResultHandler verifyResultHandler = new VerifyResultHandler(jwsStr, callBack);
// Send the check result to your server for verification.
verifyJws(activity, jwsStr, verifyResultHandler);
}
});
}
</p>
(4) Here, I called the relevant app server API in the verifyJws method to verify the check result. The third parameter in this method is an object of the VerifyResultHandler class, which implements a callback API to process the verification result.
2.3 Verifying the Check Result on the App Server
After receiving the check result from the TSMS server, the app will send the result to the app server. The server uses the HUAWEI CBG Root CA certificate to verify the signature and certificate chain in the check result, and thereby determines whether the check result is valid.
The sample code for the app server to read the certificate and verify the JWS string is as follows:
(1) Parse the header, payload, and signature from the JWS string.
Java:
<p style="line-height: 1.5em;">public JwsVerifyResp verifyJws(JwsVerifyReq jwsVerifyReq) {
// Obtain the JWS information sent from the app to the server.
String jwsStr = jwsVerifyReq.getJws();
// Parse the JWS segments. A JWS has three fixed segments, which are separated by periods (.).
String[] jwsSplit = jwsStr.split("\\.");
try {
// Perform Base64 decoding on each segment and construct a JWS object for each decoded segment.
JWSObject jwsObject = new JWSObject(new Base64URL(jwsSplit[0]), new Base64URL(jwsSplit[1]), new Base64URL(jwsSplit[2]));
// Verify the JWS and set the verification result.
boolean result = VerifySignatureUtil.verifySignature(jwsObject);
// Construct the response body for check result verification on the app server.
JwsVerifyResp jwsVerifyResp = new JwsVerifyResp();
jwsVerifyResp.setResult(result);
} catch (ParseException | NoSuchAlgorithmException e) {
RUN_LOG.catching(e);
}
return jwsVerifyResp;
}
</p>
(2) Use the verifySignature method of the VerifySignatureUtil class to verify relevant information, including the JWS signature algorithm, certificate chain, host name in signing certificate, and JWS signature. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">public static boolean verifySignature(JWSObject jws) throws NoSuchAlgorithmException {
JWSAlgorithm jwsAlgorithm = jws.getHeader().getAlgorithm();
// 1. Verify the JWS signature algorithm.
if ("RS256".equals(jwsAlgorithm.getName())) {
// Verify the certificate chain and obtain an instance of the Signature class based on the signature algorithm to verify the signature.
return verify(Signature.getInstance("SHA256withRSA"), jws);
}
return false;
}
private static boolean verify(Signature signature, JWSObject jws) {
// Extract the certificate chain information from the JWS header and convert the certificate chain into a proper type for subsequent processing.
X509Certificate[] certs = extractX509CertChain(jws);
// 2. Verify the certificate chain.
try {
verifyCertChain(certs);
} catch (Exception e) {
return false;
}
// 3. Verify the domain name in the signing certificate (leaf certificate). The domain name must be sysintegrity.platform.hicloud.com.
try {
new DefaultHostnameVerifier().verify("sysintegrity.platform.hicloud.com", certs[0]);
} catch (SSLException e) {
return false;
}
// 4. Verify the JWS signature information using the public key obtained from the signing certificate.
PublicKey pubKey = certs[0].getPublicKey();
try {
// Use the public key obtained from the signing certificate to initialize the Signature instance.
signature.initVerify(pubKey);
// Extract the input signature from the JWS and pass it to the Signature instance.
signature.update(jws.getSigningInput());
// Use the Signature instance to verify the signature information.
return signature.verify(jws.getSignature().decode());
} catch (InvalidKeyException | SignatureException e) {
return false;
}
}
</p>
(3) Call the extractX509CertChain method to extract the certificate chain from the JWS header. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">private static X509Certificate[] extractX509CertChain(JWSObject jws) {
List<X509Certificate> certs = new ArrayList<>();
List<com.nimbusds.jose.util.Base64> x509CertChain = jws.getHeader().getX509CertChain();
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
certs.addAll(x509CertChain.stream().map(cert -> {
try {
return (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream(cert.decode()) );
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList()));
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return (X509Certificate[]) certs.toArray();
}
</p>
(4) Call the verifyCertChain method to verify the certificate chain. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">private static void verifyCertChain(X509Certificate[] certs) throws CertificateException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchProviderException, SignatureException {
// Verify the validity period and issuing relationship of each certificate one by one.
for (int i = 0; i < certs.length - 1; ++i) {
certs[i].checkValidity();
PublicKey pubKey = certs[i + 1].getPublicKey();
certs[i].verify(pubKey);
}
// Use the preset HUAWEI CBG Root CA certificate to verify the last certificate in the certificate chain.
PublicKey caPubKey = huaweiCbgRootCaCert.getPublicKey();
certs[certs.length - 1].verify(caPubKey);
}
</p>
(5) Load the HUAWEI CBG Root CA certificate in the static code snippet of the VerifySignatureUtil class. The sample code is as follows:
Java:
<p style="line-height: 1.5em;">static {
// Load the preset HUAWEI CBG Root CA certificate.
File filepath = "~/certs/Huawei_cbg_root.cer";
try (FileInputStream in = new FileInputStream(filepath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
huaweiCbgRootCaCert = (X509Certificate) cf.generateCertificate(in);
} catch (IOException | CertificateException e) {
RUN_LOG.error("HUAWEI CBG root cert load failed!");
}
}
</p>
We have now verified the check result on the app server, and the verification result will be returned to the app for subsequent service processing.
2.4 Obtaining the System Integrity Check Result
(1) Once you complete the steps above, the app will obtain the reliable system integrity check result from the payload of the JWS string. Parse the system integrity check result from the callback API of the VerifyResultHandler class as follows:
Java:
<p style="line-height: 1.5em;">private static final class VerifyResultHandler implements ICallBack<Boolean> {
private final String jwsStr;
private final ICallBack<? super Boolean> callBack;
private VerifyResultHandler(String jwsStr, ICallBack<? super Boolean> callBack) {
this.jwsStr = jwsStr;
this.callBack = callBack;
}
@Override
public void onSuccess(Boolean verified) {
if (verified) {
// Extract the system integrity check result that has been successfully verified by the app server.
String payloadDetail = new String(Base64.decode(jwsStr.split("\\.")[1].getBytes(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8);
try {
final boolean basicIntegrity = new JSONObject(payloadDetail).getBoolean("basicIntegrity");
// Call back the system integrity check result.
callBack.onSuccess(basicIntegrity);
} catch (JSONException e) {
…
}
}
…
}
}
</p>
(2) The following is an example of the system integrity check response:
XML:
<p style="line-height: 1.5em;">{
"apkCertificateDigestSha256": [
"osaUtTsdAvezjQBaW3IhN3/fsc6NQ5KwKuAQXcfrxb4="
],
"apkDigestSha256": "vFcmE0uw5s+4tFjXF9rVycxk2xR1rXiZFHuuBFzTVy8=",
"apkPackageName": "com.example.mockthirdapp",
"basicIntegrity": false,
"detail": [
"root",
"unlocked"
],
"nonce": "UjJScmEyNGZWbTV4YTJNZw==",
"timestampMs": 1604048377137,
"advice": "RESTORE_TO_FACTORY_ROM"
}
</p>
(3) If the value of the basicIntegrity field in the check result is false, it means that the system integrity is corrupted. In this case, the app can notify the user of any potential risks.
Conclusion
You can complete the integration by referring to the Preparations on HUAWEI Developers website. Also, you can download the SysIntegrity sample code for both Java and Kotlin from the site. And the sample code for four other functions in addition to SysIntegrity is also provided on the site.
Here is the sample code I wrote for my app. Feel free to take a look for yourself!
My sample code
Click to expand...
Click to collapse
Overview
In this article, I will create a College Campus Placement Centre Demo App which highlights ongoing pool college placement with all listed companies and their details. Student can easily apply and register with available food facility on the campus through IAP. I have integrated HMS Account, Ads, Analytics and IAP Kit which is based on Cross-platform Technology Xamarin.
Ads Kit Service Introduction
HMS Ads kit is powered by Huawei which allows the developer to monetize services such as Banner, Splash, Reward and Interstitial Ads. HUAWEI Ads Publisher Service is a monetization service that leverages Huawei's extensive data capabilities to display targeted, high-quality ad content in your application to the vast user base of Huawei devices.
Analytics Kit Service Introduction
Analytics kit is powered by Huawei which allows rich analytics models to help you clearly understand user behavior and gain in-depth insights into users, products, and content. As such, you can carry out data-driven operations and make strategic decisions about app marketing and product optimization.
Analytics Kit implements the following functions using data collected from apps:
1. Provides data collection and reporting APIs for collection and reporting custom events.
2. Sets up to 25 user attributes.
3. Supports automatic event collection and session calculation as well as predefined event IDs and parameters.
HMS IAP Service Introduction
HMS In-App Purchase Kit allows purchasing any product from the application with highly secure payment. Users can purchase a variety of products or services, including common virtual products and subscriptions, directly within your app. It also provides a product management system (PMS) for managing the prices and languages of in-app products (including games) in multiple locations.
These are the following 3 types of in-app products supported by the IAP:
1. Consumable: Consumables are used once, are depleted, and can be purchased again.
2. Non-consumable: Non-consumables are purchased once and do not expire.
3. Auto-renewable subscriptions: Users can purchase access to value-added functions or content in a specified period of time. The subscriptions are automatically renewed on a recurring basis until users decide to cancel.
Account Kit Service Introduction
HMS Account Kit allows you to connect to the Huawei ecosystem using your HUAWEI ID from a range of devices, such as mobile phones, tablets, and smart screens.
It’s a simple, secure, and quick sign-in and authorization functions. Instead of entering accounts and passwords and waiting for authentication.
Complies with international standards and protocols such as OAuth2.0 and OpenID Connect, and supports two-factor authentication (password authentication and mobile number authentication) to ensure high security.
Prerequisite
1. Xamarin Framework
2. Huawei phone
3. Visual Studio 2019
App Gallery Integration process
1. Sign In and Create or Choose a project on AppGallery Connect portal.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
2. Navigate to Project settings > download the configuration file.
3. Navigate to General Information > Data Storage location.
4. Navigate to Manage APIs > enable APIs to require by an application.
5. Navigate to My apps > Operate, and then enter details in Add Product.
6. Click Activate for product activation.
7. Navigate to Huawei Analytics > Overview > Custom dashboard > Enable Analytics.
Xamarin Ads Kit Setup Process
1. Download Xamarin Plugin of all the aar and zip files from the URL:
2. Open the XAdsIdentifier-3.4.35.300.sln solution in Visual Studio.
3. Navigate to Solution Explorer and right-click on jar Add > Existing Item and choose aar file which download in Step 1.
4. Choose aar file from download location.
5. Right-click on added aar file, then choose Properties > Build Action > LibraryProjectZip.
Note: Repeat Step 3 and 4 for all aar file.
6. Build the Library and make DLL files.
Xamarin Analytics Kit Setup Process
1. Download Xamarin Plugin of all the aar and zip files from the URL, see the similar image in Ads kit setup Step 1:
2. Open the XHiAnalytics-5.0.5.300.sln solution in Visual Studio, see the similar image in Ads kit setup Step 2.
3. Navigate to Solution Explorer and right-click on jar Add > Existing Item and choose aar file which download in Step 1.
4. Choose aar file from download location.
5. Right-click on added aar file, then choose Properties > Build Action > LibraryProjectZip.
Note: Repeat Step 3 and 4 for all aar file.
6. Build the Library and make DLL files.
Xamarin Account Kit Setup Process
1. Download Xamarin Plugin all the aar and zip files from the URL, see the similar image in Ads kit setup Step 1:
2. Open the XHwid-5.03.302.sln solution in Visual Studio, see the similar image in Ads kit setup Step 2.
Xamarin IAP Kit Setup Process
1. Download Xamarin Plugin all the aar and zip files from the URL, see the similar image in Ads kit setup Step 1:
2. Open the XIAP-5.0.2.300.sln solution in Visual Studio, see the similar image in Ads kit setup Step 2.
3. Navigate to Solution Explorer and Right-click on jar Add > Existing Item and choose aar file which downloads in Step 1.
4. Right-click on added aar file then choose Properties > Build Action > LibraryProjectZip.
Note: Repeat Step 3 and 4 for all aar file.
5. Build the Library and make dll files.
Xamarin App Development
1. Open Visual Studio 2019 and Create a new project.
2. Navigate to Solution Explore > Project > Assets > Add JSON file.
3. Navigate to Solution Explore > Project > Add > Add New Folder.
4. Navigate to Folder(created) > Add > Add Existing and add all DLL files.
5. Right click > Properties > Build Action > None.
6. Navigate to Solution Explore > Project > Reference > Right Click > Add References, then Navigate to Browse and add all DLL files from the recently added Folder.
7. Added reference then click Ok.
MainActivity.cs
This activity performs all the operation regarding login with Huawei Id.
Java:
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Android.Support.Design.Widget;
using Android.Support.V4.App;
using Android.Support.V4.Content;
using Android.Support.V7.App;
using Android.Util;
using Android.Views;
using Android.Widget;
using Com.Huawei.Agconnect.Config;
using Com.Huawei.Hmf.Tasks;
using Com.Huawei.Hms.Common;
using Com.Huawei.Hms.Ads.Banner;
using Com.Huawei.Hms.Analytics;
using Com.Huawei.Hms.Iap;
using Com.Huawei.Hms.Iap.Entity;
using Com.Huawei.Hms.Support.Hwid;
using Com.Huawei.Hms.Support.Hwid.Request;
using Com.Huawei.Hms.Support.Hwid.Result;
using Com.Huawei.Hms.Support.Hwid.Service;
namespace PlacementApp
{
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
private Button btnLoginWithHuaweiId;
private HuaweiIdAuthParams mAuthParam;
public static IHuaweiIdAuthService mAuthManager;
private static String TAG = "MainActivity";
public static String name, email;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_main);
Android.Support.V7.Widget.Toolbar toolbar = FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
btnLoginWithHuaweiId = FindViewById<Button>(Resource.Id.btn_huawei_id);
// Write code for Huawei id button click
mAuthParam = new HuaweiIdAuthParamsHelper(HuaweiIdAuthParams.DefaultAuthRequestParam)
.SetIdToken().SetEmail()
.SetAccessToken()
.CreateParams();
mAuthManager = HuaweiIdAuthManager.GetService(this, mAuthParam);
HiAnalyticsTools.EnableLog();
instance = HiAnalytics.GetInstance(this);
instance.SetAnalyticsEnabled(true);
// Click listener for each button
btnLoginWithHuaweiId.Click += delegate
{
StartActivityForResult(mAuthManager.SignInIntent, 1011);
string text = "Login Clicked";
Toast.MakeText(Android.App.Application.Context, text, ToastLength.Short).Show();
// Initiate Parameters
Bundle bundle = new Bundle();
bundle.PutString("text", text);
instance.OnEvent("ButtonClickEvent", bundle);
};
CheckIfIAPAvailable();
/*FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource.Id.fab);
fab.Click += FabOnClick;*/
//check permissions
checkPermission(new string[] { Android.Manifest.Permission.Internet,
Android.Manifest.Permission.AccessNetworkState,
Android.Manifest.Permission.ReadSms,
Android.Manifest.Permission.ReceiveSms,
Android.Manifest.Permission.SendSms,
Android.Manifest.Permission.BroadcastSms}, 100);
}
private void loadBannerAds()
{
// Obtain BannerView based on the configuration in layout
BannerView bottomBannerView = FindViewById<BannerView>(Resource.Id.hw_banner_view);
bottomBannerView.AdListener = new AdsListener();
AdParam adParam = new AdParam.Builder().Build();
bottomBannerView.LoadAd(adParam);
// Obtain BannerView using coding
BannerView topBannerview = new BannerView(this);
topBannerview.AdId = "testw6vs28auh3";
topBannerview.BannerAdSize = BannerAdSize.BannerSize32050;
topBannerview.LoadAd(adParam);
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == 1011)
{
//login success
Task authHuaweiIdTask = HuaweiIdAuthManager.ParseAuthResultFromIntent(data);
if (authHuaweiIdTask.IsSuccessful)
{
AuthHuaweiId huaweiAccount = (AuthHuaweiId)authHuaweiIdTask.TaskResult();
Log.Info(TAG, "signIn get code success.");
Log.Info(TAG, "ServerAuthCode: " + huaweiAccount.AuthorizationCode);
Toast.MakeText(Android.App.Application.Context, "SignIn Success", ToastLength.Short).Show();
ManageHomeScreen(huaweiAccount, true);
}
else
{
Log.Info(TAG, "signIn failed: " + ((ApiException)authHuaweiIdTask.Exception).StatusCode);
Toast.MakeText(Android.App.Application.Context, ((ApiException)authHuaweiIdTask.Exception).StatusCode.ToString(), ToastLength.Short).Show();
Toast.MakeText(Android.App.Application.Context, "SignIn Failed", ToastLength.Short).Show();
ManageHomeScreen(null, false);
}
}
}
public void ManageHomeScreen(AuthHuaweiId data, Boolean loginStatus)
{
if (loginStatus)
{
btnLoginWithHuaweiId.Visibility = ViewStates.Gone;
name = data.DisplayName;
email = data.Email;
}
else
{
btnLoginWithHuaweiId.Visibility = ViewStates.Visible;
}
}
public void checkPermission(string[] permissions, int requestCode)
{
foreach (string permission in permissions)
{
if (ContextCompat.CheckSelfPermission(this, permission) == Permission.Denied)
{
ActivityCompat.RequestPermissions(this, permissions, requestCode);
}
}
}
/*private void FabOnClick(object sender, EventArgs eventArgs)
{
View view = (View) sender;
Snackbar.Make(view, "Replace with your own action", Snackbar.LengthLong)
.SetAction("Action", (Android.Views.View.IOnClickListener)null).Show();
}*/
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
protected override void AttachBaseContext(Context context)
{
base.AttachBaseContext(context);
AGConnectServicesConfig config = AGConnectServicesConfig.FromContext(context);
config.OverlayWith(new HmsLazyInputStream(context));
}
private void CancelAuthorisation()
{
Task cancelAuthorizationTask = mAuthManager.CancelAuthorization();
Log.Info(TAG, "Cancel Authorisation");
cancelAuthorizationTask.AddOnCompleteListener(
new OnCompleteListener
(
this, "Cancel Authorization Success",
"Cancel Authorization Failed"
)
);
}
public void SignOut()
{
Task signOutTask = mAuthManager.SignOut();
signOutTask.AddOnSuccessListener(new OnSuccessListener(this, "SignOut Success"))
.AddOnFailureListener(new OnFailureListener("SignOut Failed"));
}
public class OnCompleteListener : Java.Lang.Object, IOnCompleteListener
{
//Message when task is successful
private string successMessage;
//Message when task is failed
private string failureMessage;
MainActivity context;
public OnCompleteListener(MainActivity context, string SuccessMessage, string FailureMessage)
{
this.context = context;
this.successMessage = SuccessMessage;
this.failureMessage = FailureMessage;
}
public void OnComplete(Task task)
{
if (task.IsSuccessful)
{
//do some thing while cancel success
Log.Info(TAG, successMessage);
//context.SignOut();
}
else
{
//do some thing while cancel failed
Exception exception = task.Exception;
if (exception is ApiException)
{
int statusCode = ((ApiException)exception).StatusCode;
Log.Info(TAG, failureMessage + ": " + statusCode);
}
//context.ManageHomeScreen(null, true);
}
}
}
public class OnSuccessListener : Java.Lang.Object, Com.Huawei.Hmf.Tasks.IOnSuccessListener
{
//Message when task is successful
private string successMessage;
MainActivity context;
public OnSuccessListener(MainActivity context, string SuccessMessage)
{
this.successMessage = SuccessMessage;
this.context = context;
}
public void OnSuccess(Java.Lang.Object p0)
{
Log.Info(TAG, successMessage);
Toast.MakeText(Android.App.Application.Context, successMessage, ToastLength.Short).Show();
context.ManageHomeScreen(null, false);
}
}
public class OnFailureListener : Java.Lang.Object, Com.Huawei.Hmf.Tasks.IOnFailureListener
{
//Message when task is failed
private string failureMessage;
public OnFailureListener(string FailureMessage)
{
this.failureMessage = FailureMessage;
}
public void OnFailure(Java.Lang.Exception p0)
{
Log.Info(TAG, failureMessage);
Toast.MakeText(Android.App.Application.Context, failureMessage, ToastLength.Short).Show();
}
}
public void CheckIfIAPAvailable()
{
IIapClient mClient = Iap.GetIapClient(this);
Task isEnvReady = mClient.IsEnvReady();
isEnvReady.AddOnSuccessListener(new ListenerImp(this)).AddOnFailureListener(new ListenerImp(this));
}
class ListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
private MainActivity mainActivity;
public ListenerImp(MainActivity mainActivity)
{
this.mainActivity = mainActivity;
}
public void OnSuccess(Java.Lang.Object IsEnvReadyResult)
{
// Obtain the execution result.
Intent intent = new Intent(mainActivity, typeof(ComapnyActivity));
mainActivity.StartActivity(intent);
}
public void OnFailure(Java.Lang.Exception e)
{
Toast.MakeText(Android.App.Application.Context, "Feature Not available for your country", ToastLength.Short).Show();
if (e.GetType() == typeof(IapApiException))
{
IapApiException apiException = (IapApiException)e;
if (apiException.Status.StatusCode == OrderStatusCode.OrderHwidNotLogin)
{
// Not logged in.
//Call StartResolutionForResult to bring up the login page
}
else if (apiException.Status.StatusCode == OrderStatusCode.OrderAccountAreaNotSupported)
{
// The current region does not support HUAWEI IAP.
}
}
}
}
}
CompanyActivity.cs
This activity performs all the operation In-App purchasing and display list of company with package details.
Java:
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Support.V7.App;
using Android.Support.V7.Widget;
using Android.Util;
using Android.Views;
using Android.Widget;
using Com.Huawei.Hmf.Tasks;
using Com.Huawei.Hms.Iap;
using Com.Huawei.Hms.Iap.Entity;
using Org.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PlacementApp
{
[Activity(Label = "ComapnyActivity", Theme = "@style/AppTheme")]
public class ComapnyActivity : AppCompatActivity, BuyProduct
{
private static String TAG = "ComapnyActivity";
private RecyclerView recyclerView;
private CompanyAdapter adapter;
IList<ProductInfo> productList;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_company);
recyclerView = FindViewById<RecyclerView>(Resource.Id.recyclerview);
recyclerView.SetLayoutManager(new LinearLayoutManager(this));
recyclerView.SetItemAnimator(new DefaultItemAnimator());
//ADAPTER
adapter = new CompanyAdapter(this);
adapter.SetData(productList);
recyclerView.SetAdapter(adapter);
GetProducts();
}
private void GetProducts()
{
List<String> productIdList = new List<String>();
productIdList.Add("Nokia");
productIdList.Add("Hyperlink");
productIdList.Add("Tata");
productIdList.Add("Infosys");
productIdList.Add("Wipro");
ProductInfoReq req = new ProductInfoReq();
// PriceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
req.PriceType = 0;
req.ProductIds = productIdList;
//"this" in the code is a reference to the current activity
Task task = Iap.GetIapClient(this).ObtainProductInfo(req);
task.AddOnSuccessListener(new QueryProductListenerImp(this)).AddOnFailureListener(new QueryProductListenerImp(this));
}
class QueryProductListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
private ComapnyActivity activity;
public QueryProductListenerImp(ComapnyActivity activity)
{
this.activity = activity;
}
public void OnSuccess(Java.Lang.Object result)
{
// Obtain the result
ProductInfoResult productlistwrapper = (ProductInfoResult)result;
IList<ProductInfo> productList = productlistwrapper.ProductInfoList;
activity.adapter.SetData(productList);
activity.adapter.NotifyDataSetChanged();
}
public void OnFailure(Java.Lang.Exception e)
{
//get the status code and handle the error
}
}
public void OnBuyProduct(ProductInfo pInfo)
{
//Toast.MakeText(Android.App.Application.Context, pInfo.ProductName, ToastLength.Short).Show();
CreatePurchaseRequest(pInfo);
}
private void CreatePurchaseRequest(ProductInfo pInfo)
{
// Constructs a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// The product ID is the same as that set by a developer when configuring product information in AppGallery Connect.
// PriceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
req.PriceType = pInfo.PriceType;
req.ProductId = pInfo.ProductId;
//"this" in the code is a reference to the current activity
Task task = Iap.GetIapClient(this).CreatePurchaseIntent(req);
task.AddOnSuccessListener(new BuyListenerImp(this)).AddOnFailureListener(new BuyListenerImp(this));
}
protected override void OnActivityResult(int requestCode, Android.App.Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == 6666)
{
if (data == null)
{
Log.Error(TAG, "data is null");
return;
}
//"this" in the code is a reference to the current activity
PurchaseResultInfo purchaseIntentResult = Iap.GetIapClient(this).ParsePurchaseResultInfoFromIntent(data);
switch (purchaseIntentResult.ReturnCode)
{
case OrderStatusCode.OrderStateCancel:
// User cancel payment.
Toast.MakeText(Android.App.Application.Context, "Payment Cancelled", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderStateFailed:
Toast.MakeText(Android.App.Application.Context, "Order Failed", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderProductOwned:
// check if there exists undelivered products.
Toast.MakeText(Android.App.Application.Context, "Undelivered Products", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderStateSuccess:
// pay success.
Toast.MakeText(Android.App.Application.Context, "Payment Success", ToastLength.Short).Show();
// use the public key of your app to verify the signature.
// If ok, you can deliver your products.
// If the user purchased a consumable product, call the ConsumeOwnedPurchase API to consume it after successfully delivering the product.
String inAppPurchaseDataStr = purchaseIntentResult.InAppPurchaseData;
MakeProductReconsumeable(inAppPurchaseDataStr);
break;
default:
break;
}
return;
}
}
private void MakeProductReconsumeable(String InAppPurchaseDataStr)
{
String purchaseToken = null;
try
{
InAppPurchaseData InAppPurchaseDataBean = new InAppPurchaseData(InAppPurchaseDataStr);
if (InAppPurchaseDataBean.PurchaseStatus != InAppPurchaseData.PurchaseState.Purchased)
{
return;
}
purchaseToken = InAppPurchaseDataBean.PurchaseToken;
}
catch (JSONException e) { }
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
req.PurchaseToken = purchaseToken;
//"this" in the code is a reference to the current activity
Task task = Iap.GetIapClient(this).ConsumeOwnedPurchase(req);
task.AddOnSuccessListener(new ConsumListenerImp()).AddOnFailureListener(new ConsumListenerImp());
}
class ConsumListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
public void OnSuccess(Java.Lang.Object result)
{
// Obtain the result
Log.Info(TAG, "Product available for purchase");
}
public void OnFailure(Java.Lang.Exception e)
{
//get the status code and handle the error
Log.Info(TAG, "Product available for purchase API Failed");
}
}
class BuyListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
private ComapnyActivity activity;
public BuyListenerImp(ComapnyActivity activity)
{
this.activity = activity;
}
public void OnSuccess(Java.Lang.Object result)
{
// Obtain the payment result.
PurchaseIntentResult InResult = (PurchaseIntentResult)result;
if (InResult.Status != null)
{
// 6666 is an int constant defined by the developer.
InResult.Status.StartResolutionForResult(activity, 6666);
}
}
public void OnFailure(Java.Lang.Exception e)
{
//get the status code and handle the error
Toast.MakeText(Android.App.Application.Context, "Purchase Request Failed !", ToastLength.Short).Show();
}
}
public void OnRegister(int position)
{
//Toast.MakeText(Android.App.Application.Context, "Position is :" + position, ToastLength.Short).Show();
Intent intent = new Intent(this, typeof(RegistrationActivity));
ProductInfo pInfo = productList[position];
intent.PutExtra("price_type", pInfo.PriceType);
intent.PutExtra("product_id", pInfo.ProductId);
intent.PutExtra("price", pInfo.Price);
StartActivity(intent);
}
}
}
RegistrationActivity.cs
This activity performs register student data then redirect to the payment screen through In-App purchasing.
Java:
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Support.V7.App;
using Android.Util;
using Android.Views;
using Android.Widget;
using Com.Huawei.Hmf.Tasks;
using Com.Huawei.Hms.Iap;
using Com.Huawei.Hms.Iap.Entity;
using Org.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PlacementApp
{
[Activity(Label = "Registration", Theme = "@style/AppTheme")]
class RegistrationActivity : AppCompatActivity
{
private int priceType;
private String productId, price;
private EditText stdName, stdEmail, phoneNo, place;
private TextView regFee;
private Button btnRegister;
private static String TAG = "RegistrationActivity";
private Spinner spinner, spinnerGender;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_registration);
productId = Intent.GetStringExtra("product_id");
priceType = Intent.GetIntExtra("price_type", 0);
price = Intent.GetStringExtra("price");
stdName = FindViewById<EditText>(Resource.Id.name);
stdEmail = FindViewById<EditText>(Resource.Id.email);
phoneNo = FindViewById<EditText>(Resource.Id.phone);
place = FindViewById<EditText>(Resource.Id.place);
regFee = FindViewById<TextView>(Resource.Id.reg_fee);
btnRegister = FindViewById<Button>(Resource.Id.register);
spinner = FindViewById<Spinner>(Resource.Id.branch);
spinner.ItemSelected += SpinnerItemSelected;
spinnerGender = FindViewById<Spinner>(Resource.Id.year);
ArrayAdapter yearAdapter = ArrayAdapter.CreateFromResource(this, Resource.Array.year_array, Android.Resource.Layout.SimpleSpinnerItem);
yearAdapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
spinnerGender.Adapter = yearAdapter;
ArrayAdapter adapter = ArrayAdapter.CreateFromResource(this, Resource.Array.branch_array, Android.Resource.Layout.SimpleSpinnerItem);
adapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
spinner.Adapter = adapter;
stdName.Text = MainActivity.name;
stdEmail.Text = MainActivity.email;
regFee.Text = "Breakfast Fee : " + price;
btnRegister.Click += delegate
{
CreateRegisterRequest();
};
}
private void SpinnerItemSelected(object sender, AdapterView.ItemSelectedEventArgs e)
{
if (e.Position != 0)
{
Spinner spinner = (Spinner)sender;
string name = spinner.GetItemAtPosition(e.Position).ToString();
Toast.MakeText(Android.App.Application.Context, name, ToastLength.Short).Show();
}
}
private void CreateRegisterRequest()
{
// Constructs a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// The product ID is the same as that set by a developer when configuring product information in AppGallery Connect.
// PriceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
req.PriceType = priceType;
req.ProductId = productId;
//"this" in the code is a reference to the current activity
Task task = Iap.GetIapClient(this).CreatePurchaseIntent(req);
task.AddOnSuccessListener(new BuyListenerImp(this)).AddOnFailureListener(new BuyListenerImp(this));
}
class BuyListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
private RegistrationActivity regActivity;
public BuyListenerImp(RegistrationActivity regActivity)
{
this.regActivity = regActivity;
}
public void OnSuccess(Java.Lang.Object result)
{
// Obtain the payment result.
PurchaseIntentResult InResult = (PurchaseIntentResult)result;
if (InResult.Status != null)
{
// 6666 is an int constant defined by the developer.
InResult.Status.StartResolutionForResult(regActivity, 6666);
}
}
public void OnFailure(Java.Lang.Exception e)
{
//get the status code and handle the error
Toast.MakeText(Android.App.Application.Context, "Purchase Request Failed !", ToastLength.Short).Show();
}
}
protected override void OnActivityResult(int requestCode, Android.App.Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == 6666)
{
if (data == null)
{
Log.Error(TAG, "data is null");
return;
}
//"this" in the code is a reference to the current activity
PurchaseResultInfo purchaseIntentResult = Iap.GetIapClient(this).ParsePurchaseResultInfoFromIntent(data);
switch (purchaseIntentResult.ReturnCode)
{
case OrderStatusCode.OrderStateCancel:
// User cancel payment.
Toast.MakeText(Android.App.Application.Context, "Registration Cancelled", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderStateFailed:
Toast.MakeText(Android.App.Application.Context, "Registration Failed", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderProductOwned:
// check if there exists undelivered products.
Toast.MakeText(Android.App.Application.Context, "Undelivered Products", ToastLength.Short).Show();
break;
case OrderStatusCode.OrderStateSuccess:
// pay success.
Toast.MakeText(Android.App.Application.Context, "Registration Success", ToastLength.Short).Show();
// use the public key of your app to verify the signature.
// If ok, you can deliver your products.
// If the user purchased a consumable product, call the ConsumeOwnedPurchase API to consume it after successfully delivering the product.
String inAppPurchaseDataStr = purchaseIntentResult.InAppPurchaseData;
MakeProductReconsumeable(inAppPurchaseDataStr);
break;
default:
break;
}
return;
}
}
private void MakeProductReconsumeable(String InAppPurchaseDataStr)
{
String purchaseToken = null;
try
{
InAppPurchaseData InAppPurchaseDataBean = new InAppPurchaseData(InAppPurchaseDataStr);
if (InAppPurchaseDataBean.PurchaseStatus != InAppPurchaseData.PurchaseState.Purchased)
{
return;
}
purchaseToken = InAppPurchaseDataBean.PurchaseToken;
}
catch (JSONException e) { }
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
req.PurchaseToken = purchaseToken;
//"this" in the code is a reference to the current activity
Task task = Iap.GetIapClient(this).ConsumeOwnedPurchase(req);
task.AddOnSuccessListener(new ConsumListenerImp(this)).AddOnFailureListener(new ConsumListenerImp(this));
}
class ConsumListenerImp : Java.Lang.Object, IOnSuccessListener, IOnFailureListener
{
private RegistrationActivity registrationActivity;
public ConsumListenerImp(RegistrationActivity registrationActivity)
{
this.registrationActivity = registrationActivity;
}
public void OnSuccess(Java.Lang.Object result)
{
// Obtain the result
Log.Info(TAG, "Product available for purchase");
registrationActivity.Finish();
}
public void OnFailure(Java.Lang.Exception e)
{
//get the status code and handle the error
Log.Info(TAG, "Product available for purchase API Failed");
}
}
}
}
Xamarin App Build Result
1. Navigate to Solution Explore > Project > Right Click > Archive/View Archive to generate SHA-256 for build release and Click on Distribute.
2. Choose Distribution Channel > Ad Hoc to sign apk.
3. Choose Demo Keystore to release apk.
4. Build succeed and Save apk file.
5. Final result.
Analytics Report
1. Navigate to Huawei Analytics > Overview > Real-time Overview.
2. Navigate to Huawei Analytics > Overview > Real-time Overview, then check Event analysis.
3. Navigate to App debugging, then track your events.
Tips and Tricks
1. It is recommended that the app obtains the public payment key from your server in real-time. Do not store it on the app to prevent app version incompatibility caused by the subsequent key upgrade.
2. The sandbox testing function can be used only when the following conditions are met: A sandbox testing account is successfully added, and the value of versionCode of the test package is greater than that of the released package. In the HMS Core IAP SDK 4.0.2, the isSandboxActivated API is added to check whether the current sandbox testing environment is available. If not, the API returns the reason why the environment is unavailable.
3. On mobile phones whose value of targetSdkVersion is 28 or later, ad video assets may fail to be downloaded. In this case, you need to configure the app to allow HTTP network requests. For details, please refer to Configuring Network Permissions.
4. Xamarin requires the ADB daemon to be started over port 5037. If the ADB daemon runs on a different port, Visual Studio will not be able to detect your device.
Conclusion
In this article, we have learned how to integrate HMS In-App Purchase, Ads, Analytics and Account Kit in Xamarin based Android application. Student can easily apply in a listed company which offers campus placement.
Be sure to like and comments on this article, if you found it helpful. It means a lot to me.
References
1. Banner Ads Integration Procedure
2. Reward Ads Integration Procedure
3. Interstitial Ads Integration Procedure
4. Initializing Analytics Kit Procedure
Original Source
"John, why the writing pad is missing again?"
John, programmer at Huawei, has a grandma who loves novelty, and lately she's been obsessed with online shopping. Familiarizing herself with major shopping apps and their functions proved to be a piece of cake, and she had thought that her online shopping experience would be effortless — unfortunately, however, she was hindered by product searching.
John's grandma tended to use handwriting input. When using it, she would often make mistakes, like switching to another input method she found unfamiliar, or tapping on undesired characters or signs.
Except for shopping apps, most mobile apps feature interface designs that are oriented to younger users — it's no wonder that elderly users often struggle to figure out how to use them.
John patiently helped his grandma search for products with handwriting input several times. But then, he decided to use his skills as a veteran coder to give his grandma the best possible online shopping experience. More specifically, instead of helping her adjust to the available input method, he was determined to create an input method that would conform to her usage habits.
Since his grandma tended to err during manual input, John developed an input method that converts speech into text. Grandma was enthusiastic about the new method, because it is remarkably easy to use. All she has to do is to tap on the recording button and say the product's name. The input method then recognizes what she has said, and converts her speech into text.
Actual Effects
Real-time speech recognition and speech to text are ideal for a broad range of apps, including:
Game apps (online): Real-time speech recognition comes to users' aid when they team up with others. It frees up users' hands for controlling the action, sparing them from having to type to communicate with their partners. It can also free users from any potential embarrassment related to voice chatting during gaming.
Work apps: Speech to text can play a vital role during long conferences, where typing to keep meeting minutes can be tedious and inefficient, with key details being missed. Using speech to text is much more efficient: during a conference, users can use this service to convert audio content into text; after the conference, they can simply retouch the text to make it more logical.
Learning apps: Speech to text can offer users an enhanced learning experience. Without the service, users often have to pause audio materials to take notes, resulting in a fragmented learning process. With speech to text, users can concentrate on listening intently to the material while it is being played, and rely on the service to convert the audio content into text. They can then review the text after finishing the entire course, to ensure that they've mastered the content.
How to Implement
Two services in HUAWEI ML Kit: automatic speech recognition (ASR) and audio file transcription, make it easy to implement the above functions.
ASR can recognize speech of up to 60s, and convert the input speech into text in real time, with recognition accuracy of over 95%. It currently supports Mandarin Chinese (including Chinese-English bilingual speech), English, French, German, Spanish, Italian, and Arabic.
l Real-time result output
l Available options: with and without speech pickup UI
l Endpoint detection: Start and end points can be accurately located.
l Silence detection: No voice packet is sent for silent portions.
l Intelligent conversion to digital formats: For example, the year 2021 is recognized from voice input.
Audio file transcription can convert an audio file of up to five hours into text with punctuation, and automatically segment the text for greater clarity. In addition, this service can generate text with timestamps, facilitating further function development. In this version, both Chinese and English are supported.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Development Procedures
1. Preparations
(1) Configure the Huawei Maven repository address, and put the agconnect-services.json file under the app directory.
Open the build.gradle file in the root directory of your Android Studio project.
Add the AppGallery Connect plugin and the Maven repository.
l Go to allprojects > repositories and configure the Maven repository address for the HMS Core SDK.
l Go to buildscript > repositories and configure the Maven repository address for the HMS Core SDK.
l If the agconnect-services.json file has been added to the app, go to buildscript > dependencies and add the AppGallery Connect plugin configuration.
Code:
<p style="line-height: 1.5em;">buildscript {
repositories {
google()
jcenter()
maven { url 'https://developer.huawei.com/repo/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.4'
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
// NOTE: Do not place your app dependencies here; they belong
// in the individual module build.gradle files.
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://developer.huawei.com/repo/' }
}
}</p>
2) Add the build dependencies for the HMS Core SDK.
Code:
<p style="line-height: 1.5em;">dependencies {
//The audio file transcription SDK.
implementation 'com.huawei.hms:ml-computer-voice-aft:2.2.0.300'
// The ASR SDK.
implementation 'com.huawei.hms:ml-computer-voice-asr:2.2.0.300'
// Plugin of ASR.
implementation 'com.huawei.hms:ml-computer-voice-asr-plugin:2.2.0.300'
...
}
apply plugin: 'com.huawei.agconnect' // AppGallery Connect plugin.</p>
(3) Configure the signing certificate in the build.gradle file under the app directory.
Code:
<p style="line-height: 1.5em;">signingConfigs {
release {
storeFile file("xxx.jks")
keyAlias xxx
keyPassword xxxxxx
storePassword xxxxxx
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.release
debuggable true
}
}</p>
(4) Add permissions in the AndroidManifest.xml file.
Code:
<p style="line-height: 1.5em;"><uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:requestLegacyExternalStorage="true"
...
</application>
</p>
2. Integrating the ASR Service
(1) Dynamically apply for the permissions.
Code:
<p style="line-height: 1.5em;">if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
}
private void requestCameraPermission() {
final String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO};
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
ActivityCompat.requestPermissions(this, permissions, Constants.AUDIO_PERMISSION_CODE);
return;
}
}
</p>
(2) Create an Intent to set parameters.
Code:
<p style="line-height: 1.5em;">// Set authentication information for your app.
MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(this).getString("client/api_key"));
//// Use Intent for recognition parameter settings.
Intent intentPlugin = new Intent(this, MLAsrCaptureActivity.class)
// Set the language that can be recognized to English. If this parameter is not set, English is recognized by default. Example: "zh-CN": Chinese; "en-US": English.
.putExtra(MLAsrCaptureConstants.LANGUAGE, MLAsrConstants.LAN_EN_US)
// Set whether to display the recognition result on the speech pickup UI.
.putExtra(MLAsrCaptureConstants.FEATURE, MLAsrCaptureConstants.FEATURE_WORDFLUX);
startActivityForResult(intentPlugin, "1");</p>
(3) Override the onActivityResult method to process the result returned by ASR.
Code:
<p style="line-height: 1.5em;">@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
String text = "";
if (null == data) {
addTagItem("Intent data is null.", true);
}
if (requestCode == "1") {
if (data == null) {
return;
}
Bundle bundle = data.getExtras();
if (bundle == null) {
return;
}
switch (resultCode) {
case MLAsrCaptureConstants.ASR_SUCCESS:
// Obtain the text information recognized from speech.
if (bundle.containsKey(MLAsrCaptureConstants.ASR_RESULT)) {
text = bundle.getString(MLAsrCaptureConstants.ASR_RESULT);
}
if (text == null || "".equals(text)) {
text = "Result is null.";
Log.e(TAG, text);
} else {
// Display the recognition result in the search box.
searchEdit.setText(text);
goSearch(text, true);
}
break;
// MLAsrCaptureConstants.ASR_FAILURE: Recognition fails.
case MLAsrCaptureConstants.ASR_FAILURE:
// Check whether an error code is contained.
if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_CODE)) {
text = text + bundle.getInt(MLAsrCaptureConstants.ASR_ERROR_CODE);
// Troubleshoot based on the error code.
}
// Check whether error information is contained.
if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_MESSAGE)) {
String errorMsg = bundle.getString(MLAsrCaptureConstants.ASR_ERROR_MESSAGE);
// Troubleshoot based on the error information.
if (errorMsg != null && !"".equals(errorMsg)) {
text = "[" + text + "]" + errorMsg;
}
}
// Check whether a sub-error code is contained.
if (bundle.containsKey(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE)) {
int subErrorCode = bundle.getInt(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE);
// Troubleshoot based on the sub-error code.
text = "[" + text + "]" + subErrorCode;
}
Log.e(TAG, text);
break;
default:
break;
}
}
}
</p>
3. Integrating the Audio File Transcription Service
(1) Dynamically apply for the permissions.
Code:
<p style="line-height: 1.5em;">private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static final String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE };
public static void verifyStoragePermissions(Activity activity) {
// Check if the write permission has been granted.
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// The permission has not been granted. Prompt the user to grant it.
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
}
</p>
(2) Create and initialize an audio transcription engine, and create an audio file transcription configurator.
Code:
<p style="line-height: 1.5em;">// Set the API key.
MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(getApplication()).getString("client/api_key"));
MLRemoteAftSetting setting = new MLRemoteAftSetting.Factory()
// Set the transcription language code, complying with the BCP 47 standard. Currently, Mandarin Chinese and English are supported.
.setLanguageCode("zh")
// Set whether to automatically add punctuations to the converted text. The default value is false.
.enablePunctuation(true)
// Set whether to generate the text transcription result of each audio segment and the corresponding audio time shift. The default value is false. (This parameter needs to be set only when the audio duration is less than 1 minute.)
.enableWordTimeOffset(true)
// Set whether to output the time shift of a sentence in the audio file. The default value is false.
.enableSentenceTimeOffset(true)
.create();
// Create an audio transcription engine.
MLRemoteAftEngine engine = MLRemoteAftEngine.getInstance();
engine.init(this);
// Pass the listener callback to the audio transcription engine created beforehand.
engine.setAftListener(aftListener);</p>
(3) Create a listener callback to process the audio file transcription result.
l Transcription of short audio files with a duration of 1 minute or shorter:
Code:
<p style="line-height: 1.5em;">private MLRemoteAftListener aftListener = new MLRemoteAftListener() {
public void onResult(String taskId, MLRemoteAftResult result, Object ext) {
// Obtain the transcription result notification.
if (result.isComplete()) {
// Process the transcription result.
}
}
@Override
public void onError(String taskId, int errorCode, String message) {
// Callback upon a transcription error.
}
@Override
public void onInitComplete(String taskId, Object ext) {
// Reserved.
}
@Override
public void onUploadProgress(String taskId, double progress, Object ext) {
// Reserved.
}
@Override
public void onEvent(String taskId, int eventId, Object ext) {
// Reserved.
}
};
</p>
l Transcription of audio files with a duration longer than 1 minute:
Code:
<p style="line-height: 1.5em;">private MLRemoteAftListener asrListener = new MLRemoteAftListener() {
@Override
public void onInitComplete(String taskId, Object ext) {
Log.e(TAG, "MLAsrCallBack onInitComplete");
// The long audio file is initialized and the transcription starts.
start(taskId);
}
@Override
public void onUploadProgress(String taskId, double progress, Object ext) {
Log.e(TAG, " MLAsrCallBack onUploadProgress");
}
@Override
public void onEvent(String taskId, int eventId, Object ext) {
// Used for the long audio file.
Log.e(TAG, "MLAsrCallBack onEvent" + eventId);
if (MLAftEvents.UPLOADED_EVENT == eventId) { // The file is uploaded successfully.
// Obtain the transcription result.
startQueryResult(taskId);
}
}
@Override
public void onResult(String taskId, MLRemoteAftResult result, Object ext) {
Log.e(TAG, "MLAsrCallBack onResult taskId is :" + taskId + " ");
if (result != null) {
Log.e(TAG, "MLAsrCallBack onResult isComplete: " + result.isComplete());
if (result.isComplete()) {
TimerTask timerTask = timerTaskMap.get(taskId);
if (null != timerTask) {
timerTask.cancel();
timerTaskMap.remove(taskId);
}
if (result.getText() != null) {
Log.e(TAG, taskId + " MLAsrCallBack onResult result is : " + result.getText());
tvText.setText(result.getText());
}
List<MLRemoteAftResult.Segment> words = result.getWords();
if (words != null && words.size() != 0) {
for (MLRemoteAftResult.Segment word : words) {
Log.e(TAG, "MLAsrCallBack word text is : " + word.getText() + ", startTime is : " + word.getStartTime() + ". endTime is : " + word.getEndTime());
}
}
List<MLRemoteAftResult.Segment> sentences = result.getSentences();
if (sentences != null && sentences.size() != 0) {
for (MLRemoteAftResult.Segment sentence : sentences) {
Log.e(TAG, "MLAsrCallBack sentence text is : " + sentence.getText() + ", startTime is : " + sentence.getStartTime() + ". endTime is : " + sentence.getEndTime());
}
}
}
}
}
@Override
public void onError(String taskId, int errorCode, String message) {
Log.i(TAG, "MLAsrCallBack onError : " + message + "errorCode, " + errorCode);
switch (errorCode) {
case MLAftErrors.ERR_AUDIO_FILE_NOTSUPPORTED:
break;
}
}
};
// Upload a transcription task.
private void start(String taskId) {
Log.e(TAG, "start");
engine.setAftListener(asrListener);
engine.startTask(taskId);
}
// Obtain the transcription result.
private Map<String, TimerTask> timerTaskMap = new HashMap<>();
private void startQueryResult(final String taskId) {
Timer mTimer = new Timer();
TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
getResult(taskId);
}
};
// Periodically obtain the long audio file transcription result every 10s.
mTimer.schedule(mTimerTask, 5000, 10000);
// Clear timerTaskMap before destroying the UI.
timerTaskMap.put(taskId, mTimerTask);
}
</p>
(4) Obtain an audio file and upload it to the audio transcription engine.
Code:
<p style="line-height: 1.5em;">// Obtain the URI of an audio file.
Uri uri = getFileUri();
// Obtain the audio duration.
Long audioTime = getAudioFileTimeFromUri(uri);
// Check whether the duration is longer than 60s.
if (audioTime < 60000) {
// uri indicates audio resources read from the local storage or recorder. Only local audio files with a duration not longer than 1 minute are supported.
this.taskId = this.engine.shortRecognize(uri, this.setting);
Log.i(TAG, "Short audio transcription.");
} else {
// longRecognize is an API used to convert audio files with a duration from 1 minute to 5 hours.
this.taskId = this.engine.longRecognize(uri, this.setting);
Log.i(TAG, "Long audio transcription.");
}
private Long getAudioFileTimeFromUri(Uri uri) {
Long time = null;
Cursor cursor = this.getContentResolver()
.query(uri, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
} else {
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(String.valueOf(uri));
mediaPlayer.prepare();
} catch (IOException e) {
Log.e(TAG, "Failed to read the file time.");
}
time = Long.valueOf(mediaPlayer.getDuration());
}
return time;
}</p>
For more details, you can go to:
l Reddit to join our developer discussion
l GitHub to download demos and sample codes
l Stack Overflow to solve any integration problems
| Orignal Source