{
"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"
}
Introduction
Hi everyone, In this article, we’ll explore how scheduling a task to upload files to the Huawei Drive with WorkManager. Also, we will develop a demo app using Kotlin in the Android Studio.
Huawei Drive Kit
Drive Kit (the short form of Huawei Drive Kit) allows developers to create apps that use Drive. Drive Kit gives us cloud storage capabilities for our apps, enabling users to store files that are created while using our apps, including photos, videos, and documents.
Some of the main function of the Drive kit:
Obtaining User Information
Managing and Searching for Files
Storing App Data
Performing Batch Operations
You can find more information in the official documentation link.
We’re not going to go into the details of adding Account Kit and Drive Kit to a project. You can follow the instructions to add Drive Kit to your project via official docs or codelab.
WorkManager
WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. WorkManager gives us a guarantee that our action will be taken, regardless of if the app exits. But, our tasks can be deferrable to wait for some constraints to be met or to save battery life. We will go into details on WorkManager while developing the app.
Our Sample Project DriveWithWorkManager
In this project, we’re going to develop a sample app that uploading users’ files to their drive with WorkManager. Developers can use the users’ drive to save their photos, videos, documents, or app data. With the help of WorkManager, we ensure that our upload process continues even if our application is terminated.
Setup the Project
Add the necessary dependencies to build.gradle (app level)
Code:
// HMS Account Kit
implementation 'com.huawei.hms:hwid:5.1.0.301'
// HMS Drive Kit
implementation 'com.huawei.hms:drive:5.0.0.301'
// WorkManager
implementation "androidx.work:work-runtime-ktx:2.5.0"
// Kotlin Coroutines for asynchronously programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
Layout File
activity_main.xml is the only layout file in our project. There are two buttons here, one button for login with Huawei ID and one button for creating a work. I should note that apps that support Huawei ID sign-in must comply with the Huawei ID Sign-In Button Usage Rules. Also, we used the Drive icon here. But, the icon must comply with the HUAWEI Drive icon specifications. We can download and customize icons in compliance with the specifications. For more information about the specifications, click here.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#D0D0D0"
tools:context=".MainActivity">
<com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/btnCreateWork"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:backgroundTint="#1f70f2"
android:text="Create a Work"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnLogin" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="Upload File to Huawei Drive with WorkManager"
android:textAlignment="center"
android:textColor="#1f70f2"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:srcCompat="@drawable/ic_drive" />
</androidx.constraintlayout.widget.ConstraintLayout>
Permission for Storage
We need permission to access the phone’s storage. Let’s add the necessary permissions to our manifest.
Code:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
We have two code file in our application: MainActivity.kt and UploadWorker.kt
MainActivity
MainActivity.kt -> Drive functions strongly depend on Huawei Id. To use Drive functions, users must sign in with their Huawei IDs. In this file, we perform our login process and create new works.
Let’s interpret the functions on this page one by one.
onCreate() - Starting in API 23, we need to request the user for specific permission on runtime. So, we added a simple permission request. Then, we added our button-click listeners.
signIn() - We create a scope list and added the necessary drive permissions. Then, we started the login process. If you want to use other drive functions, ensure to add permission here.
refreshAt() - We can obtain a new accessToken through the HMS Core SDK.
checkDriveCode() - First, we checked whether the unionId and access token are null or an empty string. Then, we construct a DriveCredential.Builder object and returned the code.
onActivityResult() - We get the authorization result and obtain the authorization code from AuthAccount.
createWorkRequest() - I would like to explain this function after a quick explanation of the Work Request.
Creating Work Request
This is an important part of our application. With the creatingWorkRequest function, we create a work request. There are two types of work requests; OneTimeWorkRequest and PeriodicWorkRequest. OneTimeWorkRequest is run only once. We used it for simplicity in our example. PeriodicWorkRequest is used to run tasks that need to be called periodically until canceled.
createWorkRequest() - We created an OneTimeWorkRequest and added input data as accessToken and unionId to the work. We would also like to make sure that our works only run in certain situations such as we have a network and not a low battery. So, we used constraints to achieve this. Finally, we enqueued our uploadWorkRequest to run.
Code:
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private val REQUEST_SIGN_IN_CODE = 1001
private val REQUEST_STORAGE_PERMISSION_CODE = 1002
private lateinit var btnLogin: HuaweiIdAuthButton
private lateinit var btnCreateWork: Button
private var accessToken: String = ""
private var unionId: String = ""
private lateinit var driveCredential: DriveCredential
private val PERMISSIONS_STORAGE = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnLogin = findViewById(R.id.btnLogin)
btnCreateWork = findViewById(R.id.btnCreateWork)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(PERMISSIONS_STORAGE, REQUEST_STORAGE_PERMISSION_CODE)
}
btnLogin.setOnClickListener {
signIn()
}
btnCreateWork.setOnClickListener {
if (accessToken.isEmpty() || unionId.isEmpty()) {
showToastMessage("AccessToken or UnionId is empty")
} else {
createWorkRequest(accessToken, unionId)
}
}
}
private fun signIn() {
val scopeList: MutableList<Scope> = ArrayList()
scopeList.apply {
add(Scope(DriveScopes.SCOPE_DRIVE))
add(Scope(DriveScopes.SCOPE_DRIVE_FILE))
add(Scope(DriveScopes.SCOPE_DRIVE_APPDATA))
add(HuaweiIdAuthAPIManager.HUAWEIID_BASE_SCOPE)
}
val authParams = HuaweiIdAuthParamsHelper(
HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM
)
.setAccessToken()
.setIdToken()
.setScopeList(scopeList)
.createParams()
val client = HuaweiIdAuthManager.getService(this, authParams)
startActivityForResult(client.signInIntent, REQUEST_SIGN_IN_CODE)
}
private val refreshAT = DriveCredential.AccessMethod {
/**
* Simplified code snippet for demonstration purposes. For the complete code snippet,
* please go to Client Development > Obtaining Authentication Information > Save authentication information
* in the HUAWEI Drive Kit Development Guide.
**/
[email protected] accessToken
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_SIGN_IN_CODE) {
val authAccountTask = AccountAuthManager.parseAuthResultFromIntent(data)
if (authAccountTask.isSuccessful) {
val authAccount = authAccountTask.result
accessToken = authAccount.accessToken
unionId = authAccount.unionId
val driveCode = checkDriveCode(unionId, accessToken, refreshAT)
when (driveCode) {
DriveCode.SUCCESS -> {
showToastMessage("You are Signed In successfully")
}
DriveCode.SERVICE_URL_NOT_ENABLED -> {
showToastMessage("Drive is not Enabled")
}
else -> {
Log.d(TAG, "onActivityResult: Drive SignIn Failed")
}
}
} else {
Log.e(
TAG,
"Sign in Failed : " + (authAccountTask.exception as ApiException).statusCode
)
}
}
}
private fun checkDriveCode(
unionId: String?,
accessToken: String?,
refreshAccessToken: DriveCredential.AccessMethod?
): Int {
if (StringUtils.isNullOrEmpty(unionId) || StringUtils.isNullOrEmpty(accessToken)) {
return DriveCode.ERROR
}
val builder = DriveCredential.Builder(unionId, refreshAccessToken)
driveCredential = builder.build().setAccessToken(accessToken)
return DriveCode.SUCCESS
}
private fun createWorkRequest(accessToken: String, unionId: String) {
val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(
workDataOf(
"access_token" to accessToken,
"union_Id" to unionId
)
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setInitialDelay(1, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(applicationContext).enqueue(uploadWorkRequest)
showToastMessage("Work Request is created")
}
private fun showToastMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
UploadWorker
In time, WorkManager will run a worker. To define a worker, we should create a class that extends the Worker class. For Kotlin users, WorkManager provides first-class support for coroutines. So, we can extend our UploadWorker class from CoroutinesWorker. And, it takes two parameters; context and worker parameters.
Then, we need to override the doWork function. doWork is a suspending function, it means that we can run asynchronous tasks and perform network operations. Also, it handles stoppages and cancellations automatically.
Dispatchers.IO is optimized to perform network I/O outside of the main thread. So, we call the withContext(Dispatchers.IO) to create a block that runs on the IO thread pool.
To gets the accessToken and unionId as input data, we used inputData.getString. Then, we checked the drive status. If our access code is still valid, we can use it in the Drive Kit. Otherwise, we have to get renew our code to use Drive Kit.
createAndUploadFile() - First, we created a folder in the Drive named DriveWithWorkManager. We have already put a photo on Android/data/com.huawei.drivewithwork/files as you see in the below image. Note: Don’t forget to replace the package name with yours and put a sample image.
Then, we got the file path of the image on Android.
There are two ways to upload files: direct upload and resumable upload. We used the direct upload in our sample app. Direct upload allows a file of max. 20 MB and, resumable upload doesn’t have a limit. Direct upload is recommended for files smaller than 5 MB and resumable upload for files larger than 5 MB. You can also see the codes of resumable upload as the comment.
Code:
class UploadWorker(val appContext: Context, val params: WorkerParameters) :
CoroutineWorker(appContext, params) {
private val TAG = "UploadWorker"
private var accessToken: String = ""
private var unionId: String = ""
private lateinit var driveCredential: DriveCredential
companion object {
private val MIME_TYPE_MAP: MutableMap<String, String> = HashMap()
init {
MIME_TYPE_MAP.apply {
MIME_TYPE_MAP[".doc"] = "application/msword"
MIME_TYPE_MAP[".jpg"] = "image/jpeg"
MIME_TYPE_MAP[".mp3"] = "audio/x-mpeg"
MIME_TYPE_MAP[".mp4"] = "video/mp4"
MIME_TYPE_MAP[".pdf"] = "application/pdf"
MIME_TYPE_MAP[".png"] = "image/png"
MIME_TYPE_MAP[".txt"] = "text/plain"
}
}
}
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
try {
accessToken = inputData.getString("access_token") ?: ""
unionId = inputData.getString("union_Id") ?: ""
if (accessToken.isEmpty() || unionId.isEmpty()) {
Result.failure()
} else {
val driveCode = checkDriveCode(unionId, accessToken, refreshAT)
if (driveCode == DriveCode.SUCCESS) {
GlobalScope.launch { createAndUploadFile() }
} else {
Log.d(TAG, "onActivityResult: DriveSignIn Failed")
}
Result.success()
}
} catch (error: Throwable) {
Result.failure()
}
}
}
private fun checkDriveCode(
unionId: String?,
accessToken: String?,
refreshAccessToken: DriveCredential.AccessMethod?
): Int {
if (StringUtils.isNullOrEmpty(unionId) || StringUtils.isNullOrEmpty(accessToken)) {
return DriveCode.ERROR
}
val builder = DriveCredential.Builder(unionId, refreshAccessToken)
driveCredential = builder.build().setAccessToken(accessToken)
return DriveCode.SUCCESS
}
private val refreshAT = DriveCredential.AccessMethod {
/**
* Simplified code snippet for demonstration purposes. For the complete code snippet,
* please go to Client Development > Obtaining Authentication Information > Save authentication information
* in the HUAWEI Drive Kit Development Guide.
**/
[email protected] accessToken
}
private fun buildDrive(): Drive? {
return Drive.Builder(driveCredential, appContext).build()
}
private fun createAndUploadFile() {
try {
val appProperties: MutableMap<String, String> =
HashMap()
appProperties["appProperties"] = "property"
val file = com.huawei.cloud.services.drive.model.File()
.setFileName("DriveWithWorkManager")
.setMimeType("application/vnd.huawei-apps.folder")
.setAppSettings(appProperties)
val directoryCreated = buildDrive()?.files()?.create(file)?.execute()
val path = appContext.getExternalFilesDir(null)
val fileObject = java.io.File(path.toString() + "/NatureAndMan.jpg")
appContext.getExternalFilesDir(null)?.absolutePath
val mimeType = mimeType(fileObject)
val content = com.huawei.cloud.services.drive.model.File()
.setFileName(fileObject.name)
.setMimeType(mimeType)
.setParentFolder(listOf(directoryCreated?.id))
buildDrive()?.files()
?.create(content, FileContent(mimeType, fileObject))
?.setFields("*")
?.execute()
// Resumable upload for files larger than 5 MB.
/*
val fileInputStream = FileInputStream(fileObject)
val inputStreamLength = fileInputStream.available()
val streamContent = InputStreamContent(mimeType, fileInputStream)
streamContent.length = inputStreamLength.toLong()
val content = com.huawei.cloud.services.drive.model.File()
.setFileName(fileObject.name)
.setParentFolder(listOf(directoryCreated?.id))
val drive = buildDrive()
drive!!.files().create(content, streamContent).execute()
*/
} catch (exception: Exception) {
Log.d(TAG, "Error when creating file : $exception")
}
}
private fun mimeType(file: File?): String? {
if (file != null && file.exists() && file.name.contains(".")) {
val fileName = file.name
val suffix = fileName.substring(fileName.lastIndexOf("."))
if (MIME_TYPE_MAP.keys.contains(suffix)) {
return MIME_TYPE_MAP[suffix]
}
}
return "*/*"
}
}
Now, everything is ready. We can upload our file to the Drive. Let’s run our app and see what happens.
Launch the app and login with your Huawei Id. Then click the Create A Work Button. After waiting at least a minute, WorkManager will run our work if the conditions are met. And our photo will be uploaded to the drive.
Tips & Tricks
Your app can save app data, such as configuration files and archives in the app folder inside Drive. This folder stores any files with which the user does not have direct interactions. Also, this folder can be accessed only by your app, and the content in the folder is hidden to the user and other apps using Drive.
If the size of the file you download or upload is big, you can use NetworkType.UNMETERED constraint to reduce the cost to the user.
Conclusion
In this article, we have learned how to use Drive Kit with WorkManager. And, we’ve developed a sample app that uploads images to users’ drives. In addition to uploading a file, Drive Kit offers many functions such as reading, writing, and syncing files in Huawei Drive. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles.
References
Huawei Drive Kit Official Documentation
Huawei Drive Kit Official Codelab
WorkManager Official Documentation
Interesting.
Related
More information like this, you can visit HUAWEI Developer Forum
In this article we are going to take a look at Huawei Mobile Services (HMS) Push Kit Plugin for Xamarin.Android then we will send our first notification and data message by Huawei Console. After that we will also send them by Push Kit APIs.
{
"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"
}
HUAWEI Push Kit
Push Kit, is a messaging service provided by Huawei for developers. It offers sending real time notifications and information messages. This helps developers maintain closer ties with users and increases user awareness and engagement. Furthermore, you are free to use on any different meaningful purposes.
HUAWEI Push Kit Xamarin.Android Integration
First of all, you need to be a Huawei Developer to use Huawei Push Kit. If you do not have a HUAWEI ID, register as a Huawei Developer and complete real-name authentication. For details, see Registering as a Developer.
Next, Sign in to AppGallery Connect using the approved Huawei ID and click on the “My projects” button.
Then click the “Add project” button and enter a name for your project.
After that, set project data storage location. For this Project Setting > General information > Project > Data storage location. As a location choose Germany then submit it. Afterwards, click “Add app” button in the same page then fill all the fields (if you do not know what “Package name” is just copy from Xamarin.Android Solution > Project > Properties >AndroidManifest.xml > package) after that submit.
Adding Push Kit to Your Xamarin.Android App
Firstly, we must generate the “.keystore” file. You can copy following text into batch file and change toolpath and aliasName then run it in “C:\Program Files\Java\jdk1.8.0\bin” folder. For details, see Pre-development Procedure.
Code:
SET toolPath="C:\tmp\tool.jks"
SET aliasName=aliasname
keytool -genkey -keystore %toolPath% -alias %aliasName% -keyalg RSA -dname "o=Huawei"
pause
keytool -list -v -keystore %toolPath%
pause
Keep these Alias name and SHA256 code. Open the project in Visual Studio then Properties of project afterwards fill the fields. It would be better to do this for both Debug and Release because later when you release the project you have to fill these areas.
Now, it is turn to use SHA256 fingerprint. Copy it and paste SHA-256 certificate fingerprint section then click to tick icon to save it.
Next, download “agconnect-services.json” file, afterwards right click on “Assets” folder in your project then Add > Existing Item and choose this file. At last enable the Push Kit.
Adding HUAWEI Push Kit Plugins in Our Project
Firstly, you need to create Android Binding Libraries for Xamarin. For this, see Creating Android Binding Libraries for Xamarin.
Secondly, you need to add these libraries to your project. For this, see Integrating the Xamarin HMS Push Kit Libraries.
Finally, HUAWEI Push Kit requires some permissions to work correctly. For this right click your project >Properties > Android Manifest then scroll to the bottom of the page. Enable following permissions.
· ACCESS_NETWORK_STATE
· INTERNET
· WAKE_LOCK
· DEFAULT (android.intent.category)
· BROWSABLE (android.intent.category)
· DEFAULT (android.permission)
Integrating HMS Core SDK
Add new class as “HmsLazyInputStream.cs” then paste following code.
Code:
using System;
using System.IO;
using Android.Util;
using Android.Content;
using Com.Huawei.Agconnect.Config;
namespace XamarinHmsPushDemo.Hmssample
{
class HmsLazyInputStream : LazyInputStream
{
public HmsLazyInputStream(Context context) : base(context)
{
}
public override Stream Get(Context context)
{
try
{
return context.Assets.Open("agconnect-services.json");
}
catch (Exception e)
{
Log.Error(e.ToString(), "Can't open agconnect file");
return null;
}
}
}
}
Next, add “using Com.Huawei.Agconnect.Config;” library in your MainActivity class afterwards add following code.
Code:
protected override void AttachBaseContext(Context context)
{
base.AttachBaseContext(context);
AGConnectServicesConfig config = AGConnectServicesConfig.FromContext(context);
config.OverlayWith(new HmsLazyInputStream(context));
}
Push Notification
When we send push notification, phone shows this notification on Notification Center. Before we send notification by HUAWEI Push Kit, first the device needs to get a push token.
Add following method into your MainActivity class afterwards call from OnCreate() method then copy the token from Output.
Code:
private void GetToken()
{
System.Threading.Thread thread = new System.Threading.Thread(() =>
{
try
{
string appid = AGConnectServicesConfig.FromContext(this).GetString("client/app_id");
string token = HmsInstanceId.GetInstance(this).GetToken(appid, "HCM");
Log.Info("token", token);
}
catch (Exception e)
{
Log.Info("token", e.ToString());
}
}
);
thread.Start();
}
Open your project in AppGallery Connect then Growing > Push Kit > Add notification. Fill the areas as you wish then click “Test effect” button and paste your token there.
Data Message
When we send data message to a device, OnMessageReceived method will be triggered then we can use this message. In this example we will send data message and show on application.
To receive a push data message and call other related Push Kit APIs, first you need to create a class that implements “HmsMessageService”. You can use all the code in this link but I will just use omitted OnMessageReceived method for clarity. Furthermore, I want to show you this message on Toast therefore we will also add MyBroadcastReceiver class. Moreover you should get instance of MyBroadcastReceiver in OnCreate method.
Code:
using Android.App;
using Android.Content;
using Android.Widget;
using Com.Huawei.Hms.Push;
namespace PushDemo
{
[Service]
[IntentFilter(new[] { "com.huawei.push.action.MESSAGING_EVENT" })]
public class MyMessagingService:HmsMessageService
{
private readonly static string PUSHDEMO_ACTION = "com.companyname.pushdemo.action";
public override void OnMessageReceived(RemoteMessage message)
{
Intent intent = new Intent(PUSHDEMO_ACTION);
intent.PutExtra("msg", message.Data);
SendBroadcast(intent);
}
}
[BroadcastReceiver(Enabled = true)]
[IntentFilter(new[] { "com.companyname.pushdemo.action" })]
public class MyBroadcastReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
Toast.MakeText(context, intent.GetStringExtra("msg"), ToastLength.Long).Show();
}
}
}
You will get the Toast, when you click Test effect then OK.
Using Push Kit APIs
We have sent notification and data message by HUAWEI console, now it is time to send it by HUAWEI Push Kit APIs. At this part main point is showing sample code. You can use this sample code on your other application (WFA, Web APIs, and Web Site etc.). Here is a sample code and little bit modify on GetToken and OnCreate :
Code:
using Android.App;
using Android.OS;
using Android.Support.V7.App;
using Android.Runtime;
using Android.Widget;
using Com.Huawei.Agconnect.Config;
using Android.Content;
using XamarinHmsPushDemo.Hmssample;
using Com.Huawei.Hms.Aaid;
using Android.Util;
using System;
using System.Collections.Generic;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;
namespace PushDemoForArticle
{
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
string tokenFromGetToken;//GetToken()
string appID = "appID";//AppGallery Connect > Project Setting > App information > App ID
string appKey = "APIkey";//AppGallery Connect > Project Setting > App information > App key
private static readonly HttpClient client = new HttpClient();
Button btnGetToken;
Button btnNotification;
Button btnDataMessage;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_main);
btnGetToken = FindViewById<Button>(Resource.Id.btnGetToken);
btnGetToken.Click += (sender, args) => GetToken();
btnNotification = FindViewById<Button>(Resource.Id.btnNotification);
btnNotification.Click += (sender, args) => SendNotification();
btnDataMessage = FindViewById<Button>(Resource.Id.btnDataMessage);
btnDataMessage.Click += (sender, args) => SendDataMessage();
MyBroadcastReceiver myReceiver = new MyBroadcastReceiver();
}
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 GetToken()
{
System.Threading.Thread thread = new System.Threading.Thread(() =>
{
try
{
string appid = AGConnectServicesConfig.FromContext(this).GetString("client/app_id");
tokenFromGetToken = HmsInstanceId.GetInstance(this).GetToken(appid, "HCM");
Log.Info("token", tokenFromGetToken);
RunOnUiThread(() =>
{
btnNotification.Visibility = Android.Views.ViewStates.Visible;
btnDataMessage.Visibility = Android.Views.ViewStates.Visible;
});
}
catch (Exception e)
{
Log.Info("token", e.ToString());
}
}
);
thread.Start();
}
public async Task<string> GetAccessToken()
{
string uri = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
var values = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", appID },
{ "client_secret", appKey }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync(uri, content);
var jsonResponse = JObject.Parse(await response.Content.ReadAsStringAsync()); //Install-Package Newtonsoft.Json
string accessToken = jsonResponse["access_token"].ToString(); //It is valid for 3600 seconds
return accessToken;
}
public async void SendNotification()
{
string uriNot = "https://push-api.cloud.huawei.com/v1/" + appID + "/messages:send";
var jObject = new
{
message = new
{
notification = new
{
title = "This is title",
body = "This is body part"
},
android = new
{
notification = new
{
click_action = new
{
type = 3
}
}
},
token = new[] { tokenFromGetToken }
}
};
string myJson = JsonConvert.SerializeObject(jObject, Formatting.Indented);
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", await GetAccessToken());
var responseData = await client.PostAsync(uriNot, new StringContent(myJson, Encoding.UTF8, "application/json"));
}
public async void SendDataMessage()
{
string uriNot = "https://push-api.cloud.huawei.com/v1/" + appID + "/messages:send";
var jObject = new
{
message = new
{
data = JsonConvert.SerializeObject(new
{
title = "Message Title",
text = "Message Body",
randomKey = "You can write any key and value"
}, Formatting.None),
token = new[] { tokenFromGetToken }
}
};
string myJson = JsonConvert.SerializeObject(jObject, Formatting.Indented);
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", await GetAccessToken());
var responseData = await client.PostAsync(uriNot, new StringContent(myJson, Encoding.UTF8, "application/json"));
}
}
}
copy
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:text="Get Token"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="150px"
android:id="@+id/btnGetToken" />
<Button
android:text="Send Notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="150px"
android:visibility="invisible"
android:id="@+id/btnNotification" />
<Button
android:text="Send Data Message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="150px"
android:visibility="invisible"
android:id="@+id/btnDataMessage" />
</LinearLayout>
Re- How to use Huawei Push Kit on Xamarin.Android
This is quite a helpful guide for the integration process.
Most android applications download it's content from the cloud (commonly a REST API) getting ready to parse and display that information with lists and menus in order to display dynamic content or provide a personalized experience. There are some third party libraries designed to consume a REST API (like Retrofit) or to download media content (like Glide and Picasso). This time, let me introduce you the new Huawei Network Kit.
What is nework kit?
Network kit is the new Huawei's System SDK designed to simplify the communications with web services by providing 2 main connection modes:
Rest Client
HTTP Client
Network kit supports QUIC connections automatically, that means if the Web service supports QUIC or migrates to QUIC, your app will keep working without require any change. In addition, this kit is pretty similar to the well known Retrofit, so, if you have previous experience with Retrofit, you will be able to integrate Network Kit withount complications.
Previously, we made a News client by using the HQUIC kit. In this article we are going to develop a news client application by using the new Huawei Network Kit.
Previous requirements
A developer account in newsapi.org
Android Studio 4.0 or later and the kotlin plugin
Setting up the project
Network kit doesn't require to setup a project in AGC, but you still need to add the Huawei Maven repositories to your project-level build.gradle:
Java:
buildscript {
ext.kotlin_version = "1.4.31"
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Go to the official documents and look for the Network kit latest version under version change history. Once you have found the latest version available, add it to yout app-level build.gradle as follows
Java:
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
We will use Moshi to parse the response from the web service, let's add the related dependencies and the kapt plugin to proccess the annotations.
Java:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android{
...
}
dependencies {
...
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation 'com.squareup.moshi:moshi:1.11.0'
implementation "com.squareup.moshi:moshi-kotlin:1.11.0"
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0'
...
}
To display the news in a list, we must add RecyclerView and CardView to our project and must enable the DataBinding library to make our job easier.
Java:
android {
...
//Enabling DataBinding and ViewBinding
buildFeatures{
viewBinding true
dataBinding true
}
...
}
dependencies {
...
//MVVM dependencies
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
//DataBinding dependency
kapt "com.android.databinding:compiler:3.1.4"
//Layout dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
...
}
We are ready to start the project.
Building the request
First of all, Network kit must be initialized, let's create an Application class to do this job
NetworkApplication.kt
Java:
class NetworkApplication: Application() {
companion object {
const val TAG="Network Application"
}
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
private fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this ,object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}
To make sure this code will be excecuted upon each startup, we must specify this class inside the application element in our AndroidManifest.xml. Let's add the required permissions too.
XML:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".NetworkApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkKitDemo"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Now, we must create the data models wich Moshi will use to parse the response
NewsResponse.kt
Java:
@JsonClass(generateAdapter = true)
data class NewsResponse(
@Json(name = "status") val status: String?,
@Json(name = "totalResults") val totalResults: Int?,
@Json(name = "articles") val articles: List<Article>
)
@JsonClass(generateAdapter = true)
data class Article(
@Json(name = "source") val source: Source?,
@Json(name = "author") val author: String?,
@Json(name = "title") val title: String?,
@Json(name = "description") val description: String?,
@Json(name = "url") val url: String?,
@Json(name = "urlToImage") val urlToImage: String?,
@Json(name = "publishedAt") val publishedAt: String?,
@Json(name = "content") val content: String?
)
@JsonClass(generateAdapter = true)
data class Source(
@Json(name = "id") val id: String?,
@Json(name = "name") val name: String?
)
Network kit porvides 2 operation modes, we will use the REST Client to get the Top headlines in the user's country and the HTTP Client mode to download the picture of each Article. We will create a singleton class called NetworkKitHelper.
Let's take a look to the REST Client mode:
NetworkKitHelper.kt
Java:
object NetworkKitHelper {
const val TAG: String = "HTTPClient"
//Your API key from newsapi.org
val apiKey = Keys.readApiKey()
fun createNewsClient(): NewsService {
val restClient = RestClient.Builder()
.httpClient(HttpClient.Builder().build())
.baseUrl("https://newsapi.org/v2/")//Specify the API base URL, this is useful if you will consume multiple paths of the same API
.build()
return restClient.create(NewsService::class.java)
}
//Declare a Request API
interface NewsService {
//Use the GET annotation to specify the path
@GET("top-headlines/")
fun getTopHeadlines(/* use the Query annotation to specify a query parameter in the request*/
@Query("apiKey") apiKey: String? = "",
@Query("country") country: String
): Submit<String?>?
}
fun loadTopHeadlines(sampleService: NewsService, listener: NewsClientListener?,country:String=Locale.getDefault().country) {
sampleService.getTopHeadlines(apiKey,country)?.enqueue(object : Callback<String?>() {
@Throws(IOException::class)
override fun onResponse(submit: Submit<String?>?, response: Response<String?>) {
// Obtain the response. This method will be called if the request is successful.
val body = response.body
body?.let {
try {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(NewsResponse::class.java)
val news = adapter.fromJson(it)
news?.let { myNews ->
listener?.onNewsDownloaded(myNews.articles)
}
} catch (e: JSONException) {
Log.e("excepion", e.toString())
}
}
}
override fun onFailure(submit: Submit<String?>?, exception: Throwable?) {
// Obtain the response. This method will be called if the request fails.
Log.e("LoadTopHeadlines", "response onFailure = " + exception?.message)
}
})
}
interface NewsClientListener {
fun onNewsDownloaded(news: List<Article>)
}
}
Put special attention to the loadTopHeadlines function. As you can see, there aren't coroutines or threads defined, we are using the enqueue API instead. By this way Network Kit will handle the request in asynchronous mode for us.
If the API call is successfull, we will use Moshi to parse the response into data objects. By other way, we will be notified about the error in the onFailure callback. Once the response has been parsed, NetworkKitHelper will repor the news to the specified NewsClientListener.
Let's add the code to download the preview pics:
NetworkKitHelper.kt (Adding)
Java:
object NetworkKitHelper {
private val httpClient: HttpClient = createClient()
private fun createClient(): HttpClient {
return HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()
}
fun createRequest(url: String): Request {
return httpClient.newRequest()
.url(url)
.method("GET")
.build()
}
fun httpClientEnqueue(request: Request, listener: HttpClientListener? = null) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
// Process the response if the request is successful.
Log.i(TAG, "response code:" + response.code)
response.body?.let {
listener?.onSuccess(it.bytes())
}
}
override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable?) {
// Process the exception if the request fails.
Log.w(TAG, "response onFailure = ${throwable?.message}")
}
})
}
interface HttpClientListener {
fun onSuccess(body: ByteArray)
}
}
As well as with the REST Client mode, we are able to enqueue HTTP Requests and define a callback for each one. In this case, we are receiving a byte array which will be used to create and display a bitmap.
Here we will face a complication. If we try to store the bitmap in the same data class as the Article, Moshi will cause a reflection error at compilation time. To solve this, we will define a new class to store the article and be responsible to load the bitmap, by doing so, we will be able to load the news as soon as we get them and then using the observer pattern, the bitmap will be added to the view as soon as it's ready.
ArticleModel.kt
Java:
class ArticleModel(val article: Article) : NetworkKitHelper.HttpClientListener {
private val _bitmap= MutableLiveData<Bitmap?>().apply{postValue(null)}
val bitmap: LiveData<Bitmap?> =_bitmap
init {
loadBitmap()
}
fun loadBitmap() {
article.urlToImage?.let{
val request=NetworkKitHelper.createRequest(it)
NetworkKitHelper.httpClientEnqueue(request, this)
}
}
override fun onSuccess(body: ByteArray) {
val bitmap= BitmapFactory.decodeByteArray(body, 0, body.size)
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 1000, 600, true)
_bitmap.postValue(resizedBitmap)
}
}
As soon as any instance of ArticleModel is created, it will enqueue an HTTP request async for the preview pic. If the call is successfull, we will receive a ByteArray in the onSuccess callback to create our bitmap from it and let the observer know the bitmap is ready to be displayed.
Sending the request
Let's create a ViewModel which will be responsible to invoke the API and store the data. Here we will use the observer pattern to let the observer know the Articles are ready to be displayed.
MainViewModel.kt
Java:
class MainViewModel : ViewModel(), NetworkKitHelper.NewsClientListener {
private val _articles = MutableLiveData<ArrayList<ArticleModel>>().apply { value = ArrayList() }
val articles: LiveData<ArrayList<ArticleModel>> = _articles
fun loadTopHeadlines(){
articles.value?.let{
if(it.isEmpty()) getTopHeadlines()
else return
}
}
private fun getTopHeadlines() {
NetworkKitHelper.loadTopHeadlines(NetworkKitHelper.createNewsClient(),this)
}
override fun onNewsDownloaded(news: List<Article>) {
val list=ArrayList<ArticleModel>()
for (article: Article in news) {
list.add(ArticleModel(article))
}
_articles.postValue(list)
}
}
To avoid downloading the news again when the user rotates the screen, we are defining the loadTopHeadlines function. It will only make the request if the list of articles is empty.
Displaying the Articles
We will use DataBinding to quicly display our news in a RecyclerView on the MainActivity, let's take a look to the main layout
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>
<data class="MainBinding"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Now we must define the card wich will be rendered for each article
{
"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"
}
article_card.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="ArticleBinding">
<variable
name="item"
type="com.hms.demo.networkkitdemo.ArticleModel" />
</data>
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="5dp"
card_view:cardCornerRadius="15dp"
card_view:cardElevation="20dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/pic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:contentDescription="@string/desc"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="1.0"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/articleTitle"
card_view:shapeAppearanceOverlay="@style/roundedImageView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/articleTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.article.title}"
android:textAlignment="viewStart"
android:textSize="24sp"
android:textStyle="bold"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="0.498"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@{item.article.description}"
android:textSize="20sp"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/pic" />
<TextView
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{item.article.source.name}"
card_view:layout_constraintBottom_toBottomOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/desc"
card_view:layout_constraintVertical_bias="0.09" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
The ArticleBinding class will be responsible to fill the view with the values in it's ArticleModel instance for us. That's the magic of DataBinding.
As you may know, to display elements in a RecyclerView we need an Adapter, so let's define it
NewsAdapter.kt
Java:
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
var articles:List<ArticleModel> =ArrayList()
class NewsViewHolder(private val binding:ArticleBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item:ArticleModel){
binding.item=item
item.bitmap.observe(binding.root.context as LifecycleOwner){
it?.let{
binding.pic.setImageBitmap(it)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val inflater=LayoutInflater.from(parent.context)
val binding=ArticleBinding.inflate(inflater,parent,false)
return NewsViewHolder(binding)
}
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.bind(articles[position])
}
override fun getItemCount(): Int {
return articles.size
}
}
Put special attention to the bind function of the NewsViewHolder class, from here we are telling to the ArticleBinding instance what is the information we want to display in the view. Also, we are using the observer pattern to update the ImageView once the article's preview pic has been downloaded.
Finally, is time to join everything through the MainActivity
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG="Main"
}
private lateinit var binding:MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= MainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
setContentView(binding.root)
val viewModel:MainViewModel=ViewModelProvider(this).get(MainViewModel::class.java)
val adapter=NewsAdapter()
viewModel.articles.observe(this){
adapter.articles=it
adapter.notifyDataSetChanged()
}
binding.recycler.adapter=adapter
viewModel.loadTopHeadlines()
}
}
Final result
Tips & Tricks
If your app will consume a REST API with Kotlin, is better to use Moshi instead of gson because Moshi can understand the kotlin's not-nullable types.
If you will use API keys to authenticate your client with the server, is better to use the NDK to hide your KEY and prevent it from being obtained by using reverse engineering. Let's use the Rahul Sharma's hidding method. (Make sure to download the Android NDK from the SDK Manager)
1. Swithch to the Project view and create a jni directory under the main directory.
2. Under the jni directory add the next 3 files:
Android.mk
Code:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := keys
LOCAL_SRC_FILES := keys.c
include $(BUILD_SHARED_LIBRARY)
Application.mk
Code:
APP_ABI := all
Keys.c (Put here your API key)
Code:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_hms_demo_networkkitdemo_Keys_getApiKey(JNIEnv *env, jclass instance) {
return (*env)->NewStringUTF(env, "PUT_HERE_YOUR_API_KEY");
}
3. Switch back to the Android View and create a Keys kotlin object
Keys.kt
Java:
object Keys {
init {
System.loadLibrary("keys")
}
private external fun getApiKey(): String?
public fun readApiKey(): String? { //use this method for String
return getApiKey()
}
}
4. Tell gradle you will use NDK by adding the next code inside android
build.gradle (app-level)
Java:
plugins {
...
}
android {
...
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
dependencies {
...
}
Finally, modify the NetworkKitHelper object to read the API key from the native library.
NetworkKitHelper.kt (Modifying)
Code:
object NetworkKitHelper {
val apiKey = Keys.readApiKey()
}
Conclusion
By using Network kit your app will be ready to perform requests over QUIC or HTTP/2 without writting extra code. The REST Client mode and it's annotations are helpful to to quickly consume a REST API without taking care about Threads or Coroutines. And finally, the HTTP Client mode is useful to download preview images or any other stuff which is not a JSON.
References
Read In Forum
Network Kit official Docs
Hiding Secret/Api key from reverse engineering in Android using NDK
Moshi
Hi, i have one question if we use network kit then we no need to use any third-party like volley, Retrofit?
Is it faster and easy to use than Retrofit library?
The new Huawei Network kit provides you with convenient and easy to use APIs to perform HTTP operations. You can use it as HTTP Client to quickly download many kind of files from internet. In this article we are going to use it to download files from Huawei Drive by using the Public APIs, by this way we will be able to browse and download files, even from non-huawei devices.
Note: This article is a continuation of my previous post called "Accessing to Huawei Drive by using AppAuth and REST APIs". Is highly recommended to read it first before continue with this one.
Integrating Drive Kit
We will use the new Huawei Drive kit to download the user's files stored in Huawei Drive, by doing so, the kit will handle the HTTP calls in asynchronous mode for us. Add the Network Kit SDK under dependencies in your app-level build.gradle. We will also use CardView to display the user's files in a list, so let us add both dependencies.
Code:
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation "androidx.cardview:cardview:1.0.0"
Network Kit must be initialized before calling it's APIs, we can ensure the initialization upon each startup by defining our own Application class.
MyApplication.kt
Code:
class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this, object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
val TAG = "NetworkKit"
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}
Do not forget to register your Application class in your AndroidManifest.xml
AndroidManifest.xml (Modifying)
Code:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppAuthDriveKit">
Modifying the Contract
Let us add the file management related methods to our abstract class, this will be useful if we want to add more Drive providers in the future (like Microsoft's OneDrive).
DriveService.kt (Modifying)
Code:
abstract class DriveService (val service: AuthorizationService,
val authState: AuthState){
var driveListener:OnDriveEventListener?=null
abstract fun startService()
abstract fun listFiles()
abstract fun downloadFile(context: Context, cloudFile: CloudFile)
abstract fun uploadFile(file: File)
interface OnDriveEventListener{
fun onServiceReady(storageData: String)
fun onServiceFailure(info: String)
fun onFilesListed(files: List<CloudFile>)
fun onFileUploaded()
fun onDownloadStarted(cloudFile: CloudFile)
fun onDownloadProgress(cloudFile: CloudFile, progress: Int)
fun onFileDownloaded(cloudFile: CloudFile)
}
}
Listing the Files
We will call the Files.List API to get the details of all the stored user's files on his Root directory. First, we need a class to store all the information of a file in the cloud. A data class will be useful to achieve this.
Note: A file object in the cloud has a lot of information, for demonstration purposes we will just keep the most important properties.
Code:
data class CloudFile(
val filename: String,
val fileSuffix: String,
val createdTime: String,
val downloadLink: String,
val mimeType:String,
val fileSize:Int
){
val stringSizeInKb="${fileSize/1024} KB"
}
Previously, we have called he About API to get some general information about the user's Drive, into that information, it came the "domain", that's the name of the host which is able to dispatch all the Drive requests related to the current user. We will use the "domain" to query the file list. This information will be obtained by using a normal HTTP call.
Note: The next code can be improved by using the REST Client mode of Network Kit, but is not the focus of this article.
HuaweiDriveService.kt (modifying)
Code:
override fun listFiles() {
CoroutineScope(Dispatchers.IO).launch {
authState.performActionWithFreshTokens(service){accessToken,_,_->
val url="$domain/drive/v1/files?fields=*"
val conn=URL(url).openConnection() as HttpURLConnection
conn.addRequestProperty("Authorization","Bearer $accessToken")
conn.addRequestProperty("Cache-Control","no-cache")
val response=if(conn.responseCode<400) convertStreamToString(conn.inputStream)
else convertStreamToString(conn.errorStream)
if(conn.responseCode==200){
val json=JSONObject(response)
buildList(json)
}else driveListener?.onServiceFailure(response)
}
}
}
As you can see, the registered OnDriveEventListener will receive the list of available files in the Cloud. In this case, the DriveViewModel, which will store the files list into a MutableLiveData.
DriveViewModel.kt(Modifying)
Code:
var list=MutableLiveData<ArrayList<CloudFile>>()
override fun onFilesListed(files: List<CloudFile>) {
list.apply {
value?.run {
clear()
addAll(files)
}
postValue(value)
}
}
Let us define the view which will be used to display our file items.
{
"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"
}
file_item.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:card_view="http://schemas.android.com/apk/res-auto">
<data class="FileItemBinding"
><variable
name="item"
type="com.hms.demo.appauthdrivekit.CloudFile" /></data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
card_view:cardElevation="20dp"
card_view:cardCornerRadius="15dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:padding="5dp"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/icon"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_insert_drive_file_black_18dp"
android:src="@drawable/ic_insert_drive_file_black_18dp"
android:layout_marginEnd="5dp"/>
<TextView
android:id="@+id/fileName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:text="@{item.filename}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/fileType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{item.fileSuffix}"
app:layout_constraintBottom_toTopOf="@+id/fileSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/fileName" />
<TextView
android:id="@+id/fileSize"
android:layout_width="0dp"
android:layout_height="19dp"
android:layout_marginStart="5dp"
android:layout_marginBottom="5dp"
android:text="@{item.stringSizeInKb}"
android:textAlignment="textEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>
Is time to create the adapter, a download request will be triggered if the user performs a long click on any item.
FileAdapter.kt
Code:
class FileAdapter : RecyclerView.Adapter<FileAdapter.FileVH>() {
lateinit var items:ArrayList<CloudFile>
var cloudFileEventListener:CloudFileEventListener?=null
class FileVH(private val binding: FileItemBinding): RecyclerView.ViewHolder(binding.root),View.OnLongClickListener {
var fileEventListener:CloudFileEventListener?=null
fun bind(item:CloudFile){
binding.item=item
binding.root.setOnLongClickListener(this)
}
override fun onLongClick(v: View?): Boolean {
binding.item?.let {
fileEventListener?.onDownloadRequest(it)
}
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileVH {
val layoutInflater=LayoutInflater.from(parent.context)
val binding=FileItemBinding.inflate(layoutInflater,parent,false)
return FileVH(binding).apply { fileEventListener= cloudFileEventListener}
}
override fun onBindViewHolder(holder: FileVH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
interface CloudFileEventListener{
fun onDownloadRequest(item:CloudFile)
}
}
Downloading a file
Before performing any File operation, we must make sure to have the related permissions granted, by othe way we will fall in a SecurityException. the DriveViewModel will listen for the download requests from the Recycler Adapter, and this Viewmodel is connected to our activity by the DriveNavigator interface, by using that connections, we can ask to the activity to check if the storage permissions are granted upon any download request.
DriveVM.kt (Modifying)
Code:
override fun onDownloadRequest(item:CloudFile) {
navigator?.apply {
if(checkStoragePermissions()){
val context=navigator as Context
driveService?.downloadFile(context,item)
} else requestStoragePermissions()
}
}
From the DriveActivity, we must check if the permissions are granted and asking for them if not.
DriveActivity.kt (Modifying)
Code:
override fun checkStoragePermissions(): Boolean {
val read=ContextCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE)
val write=ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)
return read==PackageManager.PERMISSION_GRANTED&&write==PackageManager.PERMISSION_GRANTED
}
override fun requestStoragePermissions() {
val permissions= arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermissions(permissions, STORAGE_PERMISSIONS)
}
Since Android 10, the access to the user's storage must be performed by using MediaStore. As the file creation process is the same no matter the Drive provider (Huawei, Microsoft, Google, etc.), let's add to our DriveService class the capability of creating new Files, so any subclass of this will be able to create new files in the local storage.
DriveService.kt (Modifying)
Code:
open fun createFileUri(context: Context, item: CloudFile):Uri?{
val resolver=context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, item.filename)
put(MediaStore.MediaColumns.MIME_TYPE, item.mimeType)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
}
}
return resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
}
Is time to download the file, at this point you may think we can easily download files by using the Network Kit file downloading capability. The problem here is the files.get web API of Drive Kit, doesn't provide you a file by definition, it provides only the file content and we must use that content as well as the file metadata to build the file by ourselves.
Let's suppose the APIs from other Drive providers have the same behavior (provide only the file content), in that case, we can add the download capability to the DriveService class. to achieve our goal, we must create an HTTP Client and a request.
DriveService.kt (Modifying)
Code:
private val httpClient:HttpClient by lazy { HttpClient.Builder()
.connectTimeout(10000)
.build() }
open fun buildGetRequest(url: String, headers: Map<String, String>):Request{
return httpClient.newRequest().apply {
url(url)
method("GET")
for(key in headers.keys){
addHeader(key,headers[key])
}
}.build()
}
Now we are ready to develop the file downloading capability.
DrveService.kt (Modifying)
Code:
open fun enqueueDownload(output:OutputStream,cloudFile: CloudFile, request: Request) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if(response.isOK){
driveListener?.onDownloadStarted(cloudFile)
response.body?.inputStream?.let {input->
val fileWriter= DataOutputStream(output)
val bytes=ByteArray(1024)
var downloaded=0
var readed=0
while (input.read(bytes,0,bytes.size).also { readed=it }>0){
downloaded+=readed
fileWriter.apply {
write(bytes,0,readed)
flush()
val progress=(downloaded*100)/cloudFile.fileSize
driveListener?.onDownloadProgress(cloudFile,progress)
}
}
fileWriter.close()
driveListener?.onFileDownloaded(cloudFile)
}
}
}
override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable) {
// Process the exception if the request fails.
Log.w("Download", "response onFailure = " + throwable.message)
}
})
}
As you can see, we are using the OnDriveEventListener interface to notify the download state and the download progress. Is time to override the downloadFile function in our HuaweiDriveService class.
HuaweiDriveService.kt (Modifying)
Code:
override fun downloadFile(context: Context, cloudFile: CloudFile) {
authState.performActionWithFreshTokens(service){ accessToken, _, _->
//Creating the file in the local storage
createFileUri(context,cloudFile)?.let {
val output=context.contentResolver.openOutputStream(it)
//Adding the authorization information for the request
val headers= mapOf("Content-Type" to "application/json","Authorization" to "Bearer $accessToken")
//Building the request
val request=buildGetRequest(cloudFile.downloadLink,headers)
//Starting the download
output?.let { stream -> enqueueDownload(stream,cloudFile,request) }
}
}
}
Starting and listening the download
Let's go back to the DriveVM class, from here, if a download request is received and the proper permissions are granted, it will call the driveService.downloadFile function to start the download task. The ViewModel will be notified about the download status changes thorugh the DriveService.OnDriveEventListener interface.
DriveVM.kt (Modifying)
Code:
override fun onDownloadStarted(cloudFile: CloudFile) {
Log.i("File","onDownloadStarted ${cloudFile.filename}")
}
override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
navigator?.onDownloadProgress(cloudFile,progress)
}
override fun onFileDownloaded(cloudFile: CloudFile) {
navigator?.onFileDownloaded(cloudFile)
}
From this callbacks we will notify the activity (DriveNavigator) to update the user interface upon any download update.
DriveActivity.kt (Modifying)
Code:
private var progressDialog:AlertDialog?=null
override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
runOnUiThread{
if(progressDialog==null) setupDialog(cloudFile.filename)
progressDialog?.apply {
setMessage("Progress: ${progress}% of ${cloudFile.fileSize/1000}KB")
show()
}
}
}
override fun onFileDownloaded(cloudFile: CloudFile) {
runOnUiThread {
progressDialog?.let {
if(it.isShowing) it.dismiss()
it.cancel()
}
progressDialog=null
AlertDialog.Builder(this)
.setTitle("Download Complete")
.setMessage("File ${cloudFile.filename} downloaded successfully!")
.setPositiveButton("Ok"){dialogInterface,_->
dialogInterface.dismiss()
}
.create().show()
}
}
private fun setupDialog(filename:String) {
progressDialog=AlertDialog.Builder(this).setTitle("Downloading: $filename").setCancelable(false).create()
}
Final Result
Conclusion
We have used Network Kit to download files from Huawei Drive without depending on the HMSCore APK, by this way, our app will be able to work even in non-Huawei Devices.
Network kit provides powerful APIs as it's File Upload/Download feature or it's REST Client mode, but is also useful to perform lower-level operations if is used in HttpClient mode, this versatility can help you to build any HTTP operation from your app with support for QUIC and Asynchronous mode.
Reference
Network Kit: URL Request
Drive Kit: Files.get
Original Source
{
"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"
}
Introduction
Hi everyone, In this article, we’ll take a look at the Huawei Network Kit and how to use it with Rest APIs. Then, we will develop a demo app using Kotlin in the Android Studio. Finally, we’ll talk about the most common types of errors when making network operations on Android and how you can avoid them.
Huawei Network Kit
Network Kit is a service suite that allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Also, it allows us to quickly and easily upload or download files with additional features such as multitasking, multithreading, resumable uploads, and downloads. Lastly, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.
Our Sample Project
In this application, we'll get a user list from a Rest Service and show the user information on the list. When we are developing the app, we'll use these libraries:
RecyclerView
DiffUtil
Kotlinx Serialization
ViewBinding
To make it simple, we don't use an application architecture like MVVM and a progress bar to show the loading status of the data.
The file structure of our sample app:
Website for Rest API
JsonPlaceHolder is a free online Rest API that we can use whenever we need some fake data. We’ll use the fake user data from the below link. And, it gives us the user list as Json, click Here.
Why we are going to use Kotlin Serialization instead of Gson ?
Firstly, we need a serialization library to convert JSON data to objects in our app. Gson is a very popular library for serializing and deserializing Java objects and JSON. But, we are using the Kotlin language and Gson is not suitable for Kotlin. Because Gson doesn’t respect non-null types in Kotlin.
If we try to parse such as a string with GSON, we’ll find out that it doesn’t know anything about Kotlin default values, so we’ll get the NullPointerExceptions as an error. Instead of Kotlinx Serialization, you can also use serialization libraries that offer Kotlin-support, like Jackson or Moshi. We will go into more detail on the implementation of the Kotlinx Serialization.
Setup the Project
We are not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.
Add the necessary dependencies to build.gradle (app level).
Code:
plugins {
id 'com.huawei.agconnect' // HUAWEI agconnect Gradle plugin'
id 'org.jetbrains.kotlin.plugin.serialization' // Kotlinx Serialization
}
android {
buildFeatures {
// Enable ViewBinding
viewBinding true
}
}
dependencies {
// HMS Network Kit
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
// Kotlinx Serialization
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1'
}
We’ll use viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.
We used the kotlinx-servialization-json:1.01 version instead of the latest version 1.1.0 in our project. If you use version 1.1.0 and your Kotlin version is smaller than 1.4.30-M1, you will get an error like this:
Code:
Your current Kotlin version is 1.4.10, while kotlinx.serialization core runtime 1.1.0 requires at least Kotlin 1.4.30-M1.
Therefore, if you want to use the latest version of Kotlinx Serialization, please make sure that your Kotlin version is higher than 1.4.30-M1.
Add the necessary dependencies to build.gradle (project level)
Code:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath 'com.huawei.agconnect:agcp:1.4.1.300' // HUAWEI Agcp plugin
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" // Kotlinx Serialization
}
}
Declaring Required Network Permissions
To use functions of Network Kit, we need to declare required permissions in the AndroidManifest.xml file.
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Initialize the Network Kit
Let’s create an Application class and initialize the Network Kit here.
Code:
class App : Application() {
private val TAG = "Application"
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
private fun initNetworkKit() {
NetworkKit.init(applicationContext, object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "NetworkKit init success")
} else {
Log.i(TAG, "NetworkKit init failed")
}
}
})
}
}
Note: Don’t forget to add the App class to the Android Manifest file.
Code:
<manifest ...>
...
<application
android:name=".App"
...
</application>
</manifest>
ApiClient
getApiClient(): It returns the RestClient instance as a Singleton. We can set the connection time out value here. Also, we specified the base URL.
Code:
const val BASE_URL = "https://jsonplaceholder.typicode.com/"
class ApiClient {
companion object {
private var restClient: RestClient? = null
fun getApiClient(): RestClient {
val httpClient = HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()
if (restClient == null) {
restClient = RestClient.Builder()
.baseUrl(BASE_URL)
.httpClient(httpClient)
.build()
}
return restClient!!
}
}
}
ApiInterface
We specified the request type as GET and pass the relative URL as “users”. And, it returns us the results as String.
Code:
interface ApiInterface {
@GET("users")
fun fetchUsers(): Submit<String>
}
User - Model Class
As I mentioned earlier, we get the data as a string. Then, we’ll convert data to User object help of the Kotlinx Serialization library. To perform this process, we have to add some annotations to our data class.
@serializable -> We can make a class serializable by annotating it.
@SerialName() -> The variable name in our data must be the same as we use in the data class. If we want to set different variable names, we should use @SerialName annotation.
Code:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class User(
@SerialName("id")
val Id: Int = 0,
val name: String = "",
val username: String = "",
val email: String = "",
)
UserDiffUtil
To tell the RecyclerView that an item in the list has changed, we’ll use the DiffUtil instead of the notifyDataSetChanged().
DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one. And, it uses The Myers Difference Algorithm to do this calculation.
What makes notifyDataSetChanged() inefficient is that it forces to recreate all visible views as opposed to just the items that have changed. So, it is an expensive operation.
Code:
class UserDiffUtil(
private val oldList: List<User>,
private val newList: List<User>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].Id == newList[newItemPosition].Id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
row_user.xml
We have two TextView to show userId and the userName. We’ll use this layout in the RecylerView.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/tv_userId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1" />
<View
android:id="@+id/divider_vertical"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_userId"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_userName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/divider_vertical"
app:layout_constraintTop_toTopOf="parent"
tools:text="Antonio Vivaldi" />
<View
android:id="@+id/divider_horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
UserAdapter
It contains the adapter and the ViewHolder class.
Code:
class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
private var oldUserList = emptyList<User>()
class UserViewHolder(val binding: RowUserBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
RowUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.binding.tvUserId.text = oldUserList[position].Id.toString()
holder.binding.tvUserName.text = oldUserList[position].name
}
override fun getItemCount(): Int = oldUserList.size
fun setData(newUserList: List<User>) {
val diffUtil = UserDiffUtil(oldUserList, newUserList)
val diffResults = DiffUtil.calculateDiff(diffUtil)
oldUserList = newUserList
diffResults.dispatchUpdatesTo(this)
}
}
activity_main.xml
It contains only a recyclerview to show the user list.
Code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
userAdapter: We create a adapter for the RecyclerView.
apiClient: We create a request API object using the RestClient object (ApiClient).
Network Kit provides two ways to send network request: synchronous and asynchronous.
Synchronous requests block the client until the operation completes. We can only get data after it finishes its task.
An asynchronous request doesn’t block the client and we can receive a callback when the data has been received.
getUsersAsSynchronous(): We use synchronous requests here. Firstly, we get the response from RestApi. Then, we need to convert the JSON data to User objects. We use the decodeFromString function to do this. Also, we set ignoreUnknownKeys = true, because we don’t use all user information inside the JSON file. We just get the id, name, username, and email. If you don’t put all information inside your Model Class (User), you have to set this parameter as true. Otherwise, you will get an error like:
Code:
Use ‘ignoreUnknownKeys = true’ in ‘Json {}’ builder to ignore unknown keys.
We call this function inside the onCreate. But, we are in the main thread, and we cannot call this function directly from the main thread. If we try to do this, it will crash and give an error like:
Code:
Caused by: android.os.NetworkOnMainThreadException
We should change our thread. So, we call getUsersAsSynchronous() function inside the tread. Then, we get the data successfully. But, there is still one problem. We changed our thread and we cannot change any view without switching to the main thread. If we try to change a view before switching the main thread, it will give an error:
Code:
D/MainActivity: onFailure: Only the original thread that created a view hierarchy can touch its views.
So, we use the runOnUiThread function to run our code in the main thread. Finally, we send our data to the recyclerview adapter to show on the screen as a list.
getUsersAsAsynchronous() - We use asynchronous requests here. We send a network request and wait for the response without blocking the thread. When we get the response, we can show the user list on the screen. Also, we don’t need to call our asynchronous function inside a different thread. But, if we want to use any view, we should switch to the main thread. So, we use the runOnUiThread function to run our code in the main thread again.
Code:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val TAG = "MainActivity"
private val userAdapter by lazy { UserAdapter() }
private val apiClient by lazy {
ApiClient.getApiClient().create(ApiInterface::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.recyclerView.apply {
layoutManager = LinearLayoutManager([email protected])
adapter = userAdapter
}
getUsersAsAsynchronous()
/*
thread(start = true) {
getUsersAsSynchronous()
}
*/
}
private fun getUsersAsSynchronous() {
val response = apiClient.fetchUsers().execute()
if (response.isSuccessful) {
val userList =
Json { ignoreUnknownKeys = true }.decodeFromString<List<User>>(response.body)
runOnUiThread {
userAdapter.setData(userList)
}
}
}
private fun getUsersAsAsynchronous() {
apiClient.fetchUsers().enqueue(object : Callback<String>() {
override fun onResponse(p0: Submit<String>?, response: Response<String>?) {
if (response?.isSuccessful == true) {
val userList = Json {
ignoreUnknownKeys = true
}.decodeFromString<List<User>>(response.body)
runOnUiThread {
userAdapter.setData(userList)
}
}
}
override fun onFailure(p0: Submit<String>?, p1: Throwable?) {
Log.d(TAG, "onFailure: ${p1?.message.toString()}")
}
})
}
}
Tips and Tricks
You can use Coroutines to manage your thread operations and perform your asynchronous operations easily.
You can use Sealed Result Class to handle the network response result based on whether it was a success or failure.
Before sending network requests, you can check that you’re connected to the internet using the ConnectivityManager.
Conclusion
In this article, we have learned how to use Network Kit in your network operations. And, we’ve developed a sample app that lists user information obtained from the REST Server. In addition to sending requests using either an HttpClient object or a RestClient object, Network Kit offers file upload and download featuring. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles.
References
Huawei Network Kit Official Documentation
Huawei Network Kit Official Codelab
Original Source
Is it available for cross platform(Xamarin)?
{
"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"
}
Introduction
In this article, we can learn how to search the hospitals located by source and destination address with HMS Core Kits such as Map, Site, and Location Kits. Map kit is to display maps, it covers map data of more than 200 countries and regions for searching any location address. Location kit provides to get the current location and location updates, and it provides flexible location based services globally to the users. Site kit provides with convenient and secure access to diverse, place-related services to users.
So, I will provide a series of articles on this Patient Tracking App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles.
https://forums.developer.huawei.com/forumPortal/en/topic/0201902220661040078
https://forums.developer.huawei.com/forumPortal/en/topic/0201908355251870119
https://forums.developer.huawei.com/forumPortal/en/topic/0202914346246890032
https://forums.developer.huawei.com/forumPortal/en/topic/0202920411340450018
Map Kit
Map Kit covers map data of more than 200 countries and regions, and supports over 70 languages. User can easily integrate map-based functions into your apps using SDK. It optimizes and enriches the map detail display capability. Map Kit supports gestures including zoom, rotation, moving and tilt gestures to ensure smooth interaction experience.
Location Kit
Location Kit combines the GPS, Wi-Fi and base station location functionalities in your app to build up global positioning capabilities, allows to provide flexible location-based services targeted at users around globally. Currently, it provides three main capabilities: fused location, activity identification and geo-fence. You can call one or more of these capabilities as required.
Site Kit
Site Kit provides the place related services for apps. It provides that to search places with keywords, find nearby place, place suggestion for user search, and find the place details using the unique id.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable Map Kit, Site Kit and Location Kit.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: 'com.huawei.agconnect'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Huawei Map
implementation 'com.huawei.hms:maps:6.2.0.301'
// Huawei Site Kit
implementation 'com.huawei.hms:site:6.2.0.301'
// Huawei Location Kit
implementation 'com.huawei.hms:location:6.2.0.300'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
Java:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
// To obtain the coarse longitude and latitude of a user with Wi-Fi network or base station.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
// To receive location information from satellites through the GPS chip.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
In the SearchActivity.kt we can find the business logic.
Java:
class SearchActivity : AppCompatActivity(), OnMapReadyCallback, View.OnClickListener {
private lateinit var hmap: HuaweiMap
private lateinit var mMapView: MapView
private var mMarker: Marker? = null
private var mCircle: Circle? = null
private var isSourceAddressField: Boolean = false
private var pickupLat: Double = 0.0
private var pickupLng: Double = 0.0
private var dropLat: Double = 0.0
private var dropLng: Double = 0.0
private var searchService: SearchService? = null
private var searchIntent: SearchIntent? = null
private lateinit var mFusedLocationProviderClient: FusedLocationProviderClient
companion object {
private const val TAG = "MapViewDemoActivity"
private const val MAPVIEW_BUNDLE_KEY = "MapViewBundleKey"
private val LAT_LNG = LatLng(12.9716, 77.5946)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
// Get mapView
mMapView = findViewById(R.id.mapView)
var mapViewBundle: Bundle? = null
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAPVIEW_BUNDLE_KEY)
}
// Add "Your API key" in api_key field value
MapsInitializer.setApiKey("DAEDADRgIFzXbAJpOqImvjRAGRkmm3wGTux0O6JBiaddIPMNTJ4SawIN8ZHWu28dtc1f1H3Cqzh0LC1cgYIvBnl1edWVuWkjciH4NA==")
mMapView.onCreate(mapViewBundle)
// get map by async method
mMapView.getMapAsync(this)
//Checking permission
checkLocationPermission()
// Location service
mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
//Initialize Search Service
getLocationFromService()
//Initialize OnClickListener
customCurrentLocation.setOnClickListener(this)
pickUpLocation.setOnClickListener(this)
dropLocation.setOnClickListener(this)
}
override fun onMapReady(map: HuaweiMap?) {
Log.d(TAG, "onMapReady: ")
// Get the HuaweiMap instance in this call back method.
hmap = map!!
// Move camera by CameraPosition param, latlag and zoom params can set here.
val build = CameraPosition.Builder().target(LatLng(13.0827, 80.2707)).zoom(10f).build()
val cameraUpdate = CameraUpdateFactory.newCameraPosition(build)
hmap.animateCamera(cameraUpdate)
hmap.setMaxZoomPreference(10f)
hmap.setMinZoomPreference(1f)
// Marker can be add by HuaweiMap
mMarker = hmap.addMarker(
MarkerOptions().position(LAT_LNG)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.garden_icon))
.clusterable(true))
mMarker?.showInfoWindow()
// circle can be add by HuaweiMap
mCircle = hmap.addCircle(
CircleOptions().center(LatLng(28.7041, 77.1025)).radius(45000.0).fillColor(Color.GREEN))
mCircle?.fillColor = Color.TRANSPARENT
}
override fun onStart() {
super.onStart()
mMapView.onStart()
}
override fun onStop() {
super.onStop()
mMapView.onStop()
}
override fun onDestroy() {
super.onDestroy()
mMapView.onDestroy()
}
override fun onPause() {
mMapView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
mMapView.onResume()
}
private fun checkLocationPermission() {
// check location permission
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
val strings = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
ActivityCompat.requestPermissions(this, strings, 1)
}
} else {
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this,ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this,"android.permission.ACCESS_BACKGROUND_LOCATION") != PackageManager.PERMISSION_GRANTED) {
val strings = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION,
"android.permission.ACCESS_BACKGROUND_LOCATION")
ActivityCompat.requestPermissions(this, strings, 2)
}
}
}
private fun getLocationFromService() {
// Add your API Key in encode
searchService = SearchServiceFactory.create(this,
URLEncoder.encode("Add your api_key"))
}
override fun onClick(v: View?) {
val id = v?.id
if (id == R.id.pickUpLocation) {
locationBox(100)
} else if (id == R.id.dropLocation) {
locationBox(101)
} else if (id == R.id.customCurrentLocation) {
getLastLocation()
}
}
private fun locationBox(requestcode: Int) {
searchIntent = SearchIntent()
searchIntent!!.setApiKey(URLEncoder.encode("DAEDADRgIFzXbAJpOqImvjRAGRkmm3wGTux0O6JBiaddIPMNTJ4SawIN8ZHWu28dtc1f1H3Cqzh0LC1cgYIvBnl1edWVuWkjciH4NA==", "utf-8"))
val intent = searchIntent!!.getIntent(this)
startActivityForResult(intent, requestcode)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 100) {
if (SearchIntent.isSuccess(resultCode)) {
isSourceAddressField = true
val site: Site = searchIntent!!.getSiteFromIntent(data)
pickUpLocation.setText(site.name)
querySuggestion()
}
}
if (requestCode == 101) {
if (SearchIntent.isSuccess(resultCode)) {
isSourceAddressField = false
val site: Site = searchIntent!!.getSiteFromIntent(data)
dropLocation.setText(site.name)
querySuggestion()
}
}
}
private fun querySuggestion() {
val request = QuerySuggestionRequest()
val query: String?
if (isSourceAddressField) {
query = pickUpLocation?.text.toString()
}else{
query = dropLocation?.text.toString()
}
if (!TextUtils.isEmpty(query)) {
request.query = query
}
searchService?.querySuggestion(
request,
searchResultListener as SearchResultListener<QuerySuggestionResponse>?
)
}
private var searchResultListener =
object : SearchResultListener<QuerySuggestionResponse> {
override fun onSearchResult(results: QuerySuggestionResponse?) {
val stringBuilder = StringBuilder()
results?.let {
val sites = results.sites
if (sites != null && sites.size > 0) {
for (site in sites) {
val location = site.location
if (isSourceAddressField) {
pickupLat = location.lat
pickupLng = location.lng
moveCamera(LatLng(pickupLat, pickupLng))
} else {
dropLat = location.lat
dropLng = location.lng
moveCamera(LatLng(dropLat, dropLng))
}
break
}
} else {
stringBuilder.append("0 results")
}
}
}
override fun onSearchError(status: SearchStatus) {
}
}
private fun getLastLocation() {
try {
val lastLocation = mFusedLocationProviderClient.lastLocation
lastLocation.addOnSuccessListener(OnSuccessListener { location ->
if (location == null) {
[email protected]
}
moveCamera(LatLng(location.latitude, location.longitude))
[email protected]
}).addOnFailureListener { e ->
}
} catch (e: Exception) {
}
}
private fun moveCamera(latLng: LatLng) {
hmap!!.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f))
}
}
In the activity_search.xml we can create the UI screen.
Java:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".search.SearchActivity">
<com.huawei.hms.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.huawei.hms.maps.MapView>
<RelativeLayout
android:id="@+id/startpoint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="42dp"
android:layout_marginTop="42dp"
android:layout_marginRight="62dp"
android:background="@drawable/blue_border_rounded_cornwe">
<EditText
android:id="@+id/pickUpLocation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/start_loc"
android:background="@android:color/transparent"
android:hint="Choose starting point "
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/start_loc"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/start_icon" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="42dp"
android:layout_marginTop="23dp"
android:layout_marginRight="62dp"
android:layout_below="@+id/startpoint"
android:background="@drawable/blue_border_rounded_cornwe">
<EditText
android:id="@+id/dropLocation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/dest_loc"
android:background="@android:color/transparent"
android:hint="Password"
android:maxLines="1"
android:paddingLeft="17dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="13sp">
</EditText>
<ImageView
android:id="@+id/dest_loc"
android:layout_width="20dp"
android:layout_height="17dp"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:src="@drawable/dest_icon" />
</RelativeLayout>
<ImageView
android:id="@+id/customCurrentLocation"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginRight="10dp"
android:baselineAlignBottom="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:src="@drawable/location_icon"/>
</RelativeLayout>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we can learn how to search the hospitals located by source and destination address with HMS Core Kits such as Map, Site, and Location Kits. Map kit is to display maps, it covers map data of more than 200 countries and regions for searching any location address. Location kit provides to get the current location and location updates, and it provides flexible location based services globally to the users. Site kit provides with convenient and secure access to diverse, place-related services to users.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Map Kit - Documentation
Map Kit – Training Video
Location Kit – Documentation
Location Kit – Training Video
Site Kit