Hi everyone,
In this article, I will talk about the security SDK development in a single code base structure for Huawei Safety Detect and Google Safety Net services, which will make your applications more secure. Thanks to this SDK, Huawei Safety Detect service with HMS (Huawei Mobile Services) and stages with GMS (Google Mobile Services) can be run compatible with Google Safety Net package for 2 platforms.
{
"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"
}
Within the scope of the SDK, we will include the following features;
User Detect: With this feature, you can make your application more secure by checking whether the users using our application are fake users. This feature is a very important and frequently used feature for banks and many e-commerce applications.
Root Detection: With this feature, it allows you to make the application more secure by checking whether the device running the application is a rooted device. It is critically important, especially for applications in the banking industry.
Huawei Safety Detect ServiceHuawei Safety Detect service, as I mentioned at the beginning of my article, is a security service that allows you to make your applications more secure and protect against security threats.You can find detailed information about Huawei Safety Detect service here.
Google Safety Net ServiceSafety Net is a service that provides a set of services and APIs that help protect your app against security threats, including bad URLs, potentially harmful apps, and rogue users.You can find detailed information about the Google Safety Net service here.
Adding a Module
First, let’s create a new module where we will do all the improvements. Let’s create a new module by choosing File -> New -> New Module -> Android Library and name it safety. After this step, we need to add dependencies to the build.gradle file of our module.
build.gradle(safety)
Code:
implementation 'com.huawei.hms:safetydetect:5.0.5.302'
implementation 'com.google.android.gms:play-services safetynet:17.0.0'
After creating a new module and adding the necessary dependencies, we can now start SDK development.
First of all, we create our interface, which contains the functions that we will use jointly for both platforms (Google-Huawei). Next, we will create the Device class, which will allow us to find the Mapper class and the mobile service type installed on the device.
Code:
interface SafetyService {
fun userDetect(appKey : String,callback: SafetyServiceCallback<SafetyServiceResponse>)
fun rootDetection(appKey: String,callback: SafetyRootDetectionCallback<RootDetectionResponse> )
interface SafetyServiceCallback<T>{
fun onSuccessUserDetect(result: T? = null)
fun onFailUserDetect(e: java.lang.Exception)
}
interface SafetyRootDetectionCallback<T>{
fun onSuccessRootDetect(result: T? = null)
fun onFailRootDetect(e: java.lang.Exception)
}
object Factory {
fun create(context: Context): SafetyService {
return when (Device.getMobileServiceType(context)) {
MobileServiceType.GMS -> {
GoogleSafetyServiceImpl(context)
}
MobileServiceType.HMS -> {
HuaweiSafetyServiceImpl(context)
}
else -> {
throw Exception("Unknown service")
}
}
}
}
}
As seen above, we create methods and callbacks that will perform both user detect and root detection. Then, in the create method, we check the service availability on the device from the Device class and work the Google or Huawei SafetyServiceImpl classes.
Code:
enum class MobileServiceType {
HMS,
GMS,
NON
}
object Device {
/**
* Mobile services availability of devices
*
* @return Device mobile service type enum
*/
fun getMobileServiceType(
context: Context,
firstPriority: MobileServiceType? = null
): MobileServiceType {
val gms: Boolean = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(context) == com.google.android.gms.common.ConnectionResult.SUCCESS
val hms: Boolean = HuaweiApiAvailability.getInstance()
.isHuaweiMobileServicesAvailable(context) == com.huawei.hms.api.ConnectionResult.SUCCESS
return if (gms && hms) {
firstPriority ?: MobileServiceType.HMS
} else if (gms) {
MobileServiceType.GMS
} else if (hms) {
MobileServiceType.HMS
} else {
MobileServiceType.NON
}
}
After these operations, we must create the Mapper class in order to parse the objects that we send and receive from the services as parameters, and the other classes we need to define.
Code:
abstract class Mapper<I, O> {
abstract fun map(from: I): O
}
After this step, we must define separate Mapper classes for Google and Huawei. For example, as a result of user detect operation, responseToken object is returned to us as Google Safety Net API and Huawei Safety Detect service return parameter. Thanks to our mapper class, we will be able to parse it into our response class, which we will create when it returns from Google or Huawei service.
Code:
class GoogleSafetyMapper: Mapper<SafetyNetApi.RecaptchaTokenResponse, SafetyServiceResponse>() {
override fun map(from: SafetyNetApi.RecaptchaTokenResponse): SafetyServiceResponse = SafetyServiceResponse(
responseToken = from.tokenResult
)
}
Code:
class HuaweiSafetyMapper : Mapper<UserDetectResponse, SafetyServiceResponse>() {
override fun map(from: UserDetectResponse): SafetyServiceResponse = SafetyServiceResponse(
responseToken = from.responseToken
)
}
As seen in the codes above, Huawei Safety Detect service returns UserDetectResponse and Google Safety Net service returns RecaptchaTokenResponse objects for the result of user detect operation. We will return the SafetyServiceResponse object in our SDK.
Code:
data class SafetyServiceResponse(
val responseToken: String
)
We should do the same for the root detection feature. We will create our RootDetectionResponse class, which will enable us to parse objects returned from Google or Huawei service by creating mapper classes for root detection.
Code:
data class RootDetectionResponse(
val apkDigestSha256: String,
val apkPackageName: String,
val basicIntegrity: Boolean,
val nonce: String,
val timestampMs: Long
)
Next we need to create our mapper classes for Google and Huawei. SafetyNet and Safety Detect services return Json object as response parameter. Here, instead of sending a json object, we will use the parsed version of the json object in our SDK.
Code:
class GoogleRootDetectMapper : Mapper<JSONObject,RootDetectionResponse>() {
override fun map(from: JSONObject): RootDetectionResponse = RootDetectionResponse(
apkDigestSha256 = from.getString("apkDigestSha256"),
apkPackageName = from.getString("apkPackageName"),
basicIntegrity = from.getBoolean("basicIntegrity"),
nonce = from.getString("nonce"),
timestampMs = from.getLong("timestampMs")
)
}
Code:
class HuaweiRootDetectMapper : Mapper<JSONObject, RootDetectionResponse>(){
override fun map(from: JSONObject): RootDetectionResponse = RootDetectionResponse(
apkDigestSha256 = from.getString("apkDigestSha256"),
apkPackageName = from.getString("apkPackageName"),
basicIntegrity = from.getBoolean("basicIntegrity"),
nonce = from.getString("nonce"),
timestampMs = from.getLong("timestampMs")
)
}
After all these steps, we will now create our SafetyServiceImpl classes, which we will implement our interface and add functionality to our functions. It must be created separately for both Google and Huawei.
Code:
class GoogleSafetyServiceImpl(private val context: Context): SafetyService {
private val mapper: Mapper<SafetyNetApi.RecaptchaTokenResponse, SafetyServiceResponse> = GoogleSafetyMapper()
private val rootDetectMapper: Mapper<JSONObject, RootDetectionResponse> = GoogleRootDetectMapper()
override fun userDetect(appKey: String,callback: SafetyService.SafetyServiceCallback<SafetyServiceResponse>){
/**
* App key value is the SITE_API_KEY value in Google Mobile Services.
*/
SafetyNet.getClient(context).verifyWithRecaptcha(appKey)
.addOnSuccessListener(){
val responseToken = it.tokenResult
if(responseToken.isNotEmpty()){
callback.onSuccessUserDetect(mapper.map(it))
}
}.addOnFailureListener(){
callback.onFailUserDetect(it)
}
}
override fun rootDetection(
appKey: String,
callback: SafetyService.SafetyRootDetectionCallback<RootDetectionResponse>
){
val nonce = ByteArray(24)
try {
val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SecureRandom.getInstanceStrong()
} else {
SecureRandom.getInstance("SHA1PRNG")
}
random.nextBytes(nonce)
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, e.message!!)
}
SafetyNet.getClient(context).attest(nonce, appKey)
.addOnSuccessListener{ result ->
val jwsStr = result.jwsResult
val jwsSplit = jwsStr.split(".").toTypedArray()
val jwsPayloadStr = jwsSplit[1]
val payloadDetail = String(Base64.decode(jwsPayloadStr.toByteArray(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8)
val jsonObject = JSONObject(payloadDetail)
callback.onSuccessRootDetect(rootDetectMapper.map(jsonObject))
}.addOnFailureListener{ e->
callback.onFailRootDetect(e)
}
}
}
As can be seen in our SafetyServiceImpl class created for the Google side above, functionality has been added to the user detection and root detection methods by implementing the methods we created in the interface. In cases where there is onSuccess() in user detect and root detection processes, we transfer the response returned from the SafetyNet API to our own response class with mapper, thanks to the callbacks we created in our interface. As a result of this process, objects returned from services are transferred to our response class that we created in our SDK.
The important issue here is that the appKey value is different in Google and Huawei services. It corresponds to the SITE_API_KEY value on the Google side. The SITE_API_KEY value needs to be generated by the reCAPTCHA API Console. Thanks to this console, you can prevent risky login attempts in your application, etc. You can track many metrics.
For the Huawei side, we should also create our Huawei Safety ServiceImpl class.
Code:
class HuaweiSafetyServiceImpl(private val context: Context): SafetyService {
private val mapper: Mapper<UserDetectResponse, SafetyServiceResponse> = HuaweiSafetyMapper()
private val rootDetectMapper: Mapper<JSONObject, RootDetectionResponse> = HuaweiRootDetectMapper()
val TAG = "CommonMobileServicesSafetySDK"
/**
App key value is the app_id value in Huawei Mobile Services.
*/
override fun userDetect(
appKey: String,
callback: SafetyService.SafetyServiceCallback<SafetyServiceResponse>
){
val client = SafetyDetect.getClient(context)
client.userDetection(appKey).addOnSuccessListener {
val responseToken = it.responseToken
if(responseToken.isNotEmpty()){
callback.onSuccessUserDetect(mapper.map(it))
}
}.addOnFailureListener {
callback.onFailUserDetect(it)
}
}
@SuppressLint("LongLogTag")
override fun rootDetection(
appKey: String,
callback: SafetyService.SafetyRootDetectionCallback<RootDetectionResponse>
) {
val nonce = ByteArray(24)
try {
val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SecureRandom.getInstanceStrong()
} else {
SecureRandom.getInstance("SHA1PRNG")
}
random.nextBytes(nonce)
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, e.message!!)
}
SafetyDetect.getClient(context)
.sysIntegrity(nonce, appKey)
.addOnSuccessListener { result ->
val jwsStr = result.result
val jwsSplit = jwsStr.split(".").toTypedArray()
val jwsPayloadStr = jwsSplit[1]
val payloadDetail = String(Base64.decode(jwsPayloadStr.toByteArray(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8)
val jsonObject = JSONObject(payloadDetail)
callback.onSuccessRootDetect(rootDetectMapper.map(jsonObject))
}
.addOnFailureListener { e ->
callback.onFailRootDetect(e)
}
}
}
In the Huawei Safety Detect service, the appKey value corresponds to the appId value.
On the root detection side, the SITE_API_KEY value is created from the Google API Console. In the Huawei Safety service, it also corresponds to the appId value.
After all these steps, we have completed the developments on the SDK side. After implementing the SDK in a different project so that we can test it, you can use it as follows.
Code:
private var safetyService = SafetyService.Factory.create(requireContext())
appKey = if(Device.getMobileServiceType(requireContext())== MobileServiceType.GMS){
this.getString(R.string.google_site_api_key)
}
else{
this.getString(R.string.app_id)
}
safetyService?.userDetect(appKey, object : SafetyService.SafetyServiceCallback<SafetyServiceResponse> {
override fun onFailUserDetect(e: Exception) {
Toast.makeText(requireContext(), e.toString(), Toast.LENGTH_SHORT).show()
}
override fun onSuccessUserDetect(result: SafetyServiceResponse?) {
viewModel.signInWithEmail(email, password)
}
})
safetyService?.rootDetection(appKey, object : SafetyService.SafetyRootDetectionCallback<RootDetectionResponse> {
override fun onFailRootDetect(e: Exception) {
Toast.makeText(applicationContext,e.toString(),Toast.LENGTH_SHORT).show()
}
override fun onSuccessRootDetect(result: RootDetectionResponse?) {
if(result!= null){
if(result.basicIntegrity){
showSecurityAlertMessage(getString(R.string.root_device_info),"Info",true)
}
else{
showSecurityAlertMessage(getString(R.string.no_root_device_error),"Security Warning",false)
}
}
}
})
As seen in the example code above, we set our appKey value according to the service availability on the device, thanks to the Device class we created in the SDK. Here, if the device is GMS, we set the API_KEY values that we created from Google reCaptcha and Api Console, and set the appId value to the appKey value if it is HMS. We can easily use the user detect and root detection features by calling the methods we will use in the next step, thanks to the interface we created in our SDK.
You can find screenshots of user detect and root detect features of a different application using the SDK.
Tips and Tricks
During SDK development, all common methods should be handled by interfaces.
App key value is app id for Huawei services, SITE_API_KEY value generated from Google Api Console for Google services.
Conclusion
In this article, I tried to explain how the services used for security in Google and Huawei services can be developed by making them compatible with both GMS and HMS devices and combining them under a single SDK. I hope it was a useful article for everyone. Thank you for taking the time to read.
References
Google Safety Net API
Huawei Safety Detect Service
Original Source
Related
This article is originally from HUAWEI Developer Forum
Forum link: https://forums.developer.huawei.com/forumPortal/en/home
This is all about integration of HMS Safety Detect API in the Android app using MVVM RxAndroid.
What is HMS Safety Detect API?
Ø The Safety Detect provides system integrity check (SysIntegrity), app security check (AppsCheck), malicious URL check (URLCheck), and fake user detection (UserDetect), helping you prevent security threats to your app.
{
"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"
}
Let’s create a Demo Project:
HUAWEI HMS Safety Detect integration requires the following preparations
Ø Creating an AGC Application.
Ø Creating an Android Studio Project.
Ø Generating a signature certificate.
Ø Generating a signature certificate fingerprint.
Ø Configuring the signature certificate fingerprint.
Ø Adding the application package name and save the configuration file.
Ø Configure the Maven address and AGC gradle plug-in.
Ø Configure the signature file in Android Studio.
In this article, we will implement SysIntegrity API in demo project using with RxAndroid and MVVM.
Call the API and handle responses.
Verify the certificate chain, signature, and domain name on the server.
1. Open AppGallery Console:
1. We need to create an application inside console.
2. We need to enable the Safety Detect api.
Go to Console > AppGallery Connect > My apps, click your app, and go to Develop > Manage APIs.
Now enable Safety Detect Api
Download the agconnect-services.json
Move the downloaded agconnect-services.json file to the app root directory of your Android Studio project.
We need to add HMS SDK dependency in app:gradle file
Code:
implementation 'com.huawei.hms:safetydetect:4.0.0.300'
We need to add maven dependency inside project:gradle file
Code:
maven { url 'http://developer.huawei.com/repo/' }
We need to add two more dependencies in app:gradle file
Code:
// MVVM
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
// RxAndroid
implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
Enable Data Binding
Code:
dataBinding {
enabled = true
}
2. Let’s implement api :
I have created following classes.
1. SysIntegrityDataSource : Which invoke the System Integrity Api with help of RxJava.
2. SysIntegrityViewModel : Which handle the response from System Integrity api and provide LiveData for view componets.
3. SysIntegrityFragment : Which observe the livedata from viewmodel class and set values in views such as textviews and button.
Note: If you are not familiar with MVVM or RxAndroid then I would like to suggest you to please go through my following articles:
· Android MyShows App — Rxandroid MVVM LiveData ViewModel DataBinding, Networking with Retrofit, Gson & Glide — Series
· Demystifying Data Binding — Android Jetpack — Series
Let’s see the implementation of SysIntegrityDataSource.java class.
Code:
public class SysIntegrityDataSource {
private static final String APP_ID = "XXXXXXXX";
private Context context;
public SysIntegrityDataSource(Context context) {
this.context = context;
}
public Single<SysIntegrityResp> executeSystemIntegrity() {
return Single.create(this::invokeSysIntegrity);
}
private void invokeSysIntegrity(SingleEmitter<SysIntegrityResp> emitter) {
byte[] nonce = ("Sample" + System.currentTimeMillis()).getBytes();
SafetyDetect.getClient(context)
.sysIntegrity(nonce, APP_ID)
.addOnSuccessListener(emitter::onSuccess)
.addOnFailureListener(emitter::onError);
}
}
invokeSysIntegrity() : This method invoke the System Integrity api and emit the data onSuccess/OnError and past it to Single<SysIntegrityResp> observable.
executeSystemIntegrity() : This method will create Single observable and return the response from invokeSysIntegrity() method.
3. Let’s implement ViewModel :
I have created SysIntegrityViewModel.java class.
Code:
public class SysIntegrityViewModel extends AndroidViewModel {
private final CompositeDisposable disposables = new CompositeDisposable();
private SysIntegrityDataSource sysIntegrityDataSource;
private MutableLiveData<SysIntegrityResp> systemIntegrityLiveData;
private MutableLiveData<String> error;
public SysIntegrityViewModel(Application app) {
super(app);
sysIntegrityDataSource = new SysIntegrityDataSource(app.getBaseContext());
systemIntegrityLiveData = new MutableLiveData<>();
error = new MutableLiveData<>();
}
public LiveData<SysIntegrityResp> observerSystemIntegrity() {
sysIntegrityDataSource.executeSystemIntegrity()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<SysIntegrityResp>() {
@Override
public void onSubscribe(Disposable d) {
disposables.add(d);
}
@Override
public void onSuccess(SysIntegrityResp response) {
systemIntegrityLiveData.setValue(response);
}
@Override
public void onError(Throwable e) {
error.setValue(e.getMessage());
}
});
return systemIntegrityLiveData;
}
public LiveData<String> getError() {
return error;
}
@Override
protected void onCleared() {
disposables.clear();
}
}
MutableLiveData<SysIntegrityResp> systemintegrityLiveData: This field which provide the live data and return the value from viewmodel to fragment class.
observerSysIntegrity() : Which observe RxAndroid’s Single(observable) on main thread and set the value in systemIntegrityLiveData. If we got error while observing it will post the error in MutableLiveData<String> error.
4. Let’s implement Fragment :
I have created SysIntegrityFragment.java class Which obaserve the System Integrity api’s reponse and set the values in views.
Code:
public class SysIntegrityFragment extends Fragment {
private SysIntegrityViewModel sysIntegrityViewModel;
private FragmentSysBinding sysBinding;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
sysBinding=DataBindingUtil.inflate(inflater, R.layout.fragment_sys, container, false);
sysIntegrityViewModel = ViewModelProviders.of(this).get(SysIntegrityViewModel.class);
sysBinding.btnSys.setOnClickListener(v->{
processView();
sysIntegrityViewModel.observerSystemIntegrity().observe(getViewLifecycleOwner(), this::setSystemIntegrity);
sysIntegrityViewModel.getError().observe(getViewLifecycleOwner(),this::showError);
});
return sysBinding.getRoot();
}
private void setSystemIntegrity(SysIntegrityResp response){
String jwsStr = response.getResult();
String[] jwsSplit = jwsStr.split("\\.");
String jwsPayloadStr = jwsSplit[1];
String payloadDetail = new String(Base64.decode(jwsPayloadStr.getBytes(), Base64.URL_SAFE));
try {
final JSONObject jsonObject = new JSONObject(payloadDetail);
final boolean basicIntegrity = jsonObject.getBoolean("basicIntegrity");
sysBinding.btnSys.setBackgroundResource(basicIntegrity ? R.drawable.btn_round_green : R.drawable.btn_round_red);
sysBinding.btnSys.setText(R.string.rerun);
String isBasicIntegrity = String.valueOf(basicIntegrity);
String basicIntegrityResult = "Basic Integrity: " + isBasicIntegrity;
sysBinding.txtBasicIntegrityTitle.setText(basicIntegrityResult);
if (!basicIntegrity) {
String advice = "Advice: " + jsonObject.getString("advice");
sysBinding.txtPayloadAdvice.setText(advice);
}
} catch (JSONException e) {
}
}
private void showError(String error){
Toast.makeText(getActivity().getApplicationContext(), error, Toast.LENGTH_SHORT).show();
sysBinding.btnSys.setBackgroundResource(R.drawable.btn_round_yellow);
sysBinding.btnSys.setText(R.string.rerun);
}
private void processView() {
sysBinding.txtBasicIntegrityTitle.setText("");
sysBinding.txtPayloadBasicIntegrity.setText("");
sysBinding.btnSys.setText(R.string.processing);
sysBinding.btnSys.setBackgroundResource(R.drawable.btn_round_processing);
}
}
We have instantiated instance of view model using ViewModel factory method.
We will consume the response on button click’s event.
If we got success response then we will display inside textviews and button otherwise we will show the error toast.
5. Let’s see the result:
Build the app and hit run button.
Click > RunDetection Case 1: Success Case 2: SDK Error Case 3: Integrity false (Rooted)
I hope you have learnt something new today. If you have any query regarding this article, please feel free to post any comments.
Any questions about this, you can try to acquire answers from HUAWEI Developer Forum.
Useful sharing,thanks
Thank you so much for sharing, very useful.
Thank you so much for sharing, very useful.
Thank you so much for sharing, very useful.
Does it work offline?
useful sharing,thanks!
On an Enterprise environment maybe you want to perform some console operations from a custom plaform, for example, to manage your app information, to automatize the download of the app finance report, or to automatically release an app allocated in your own server. If this is your case, yo may be interested on App Gallery Connect API.
Previous requirements
A developer account
At least one project on AGC
What will we do in this article?
We will generate our client credentials to obtain an app level access token which will give us access to the App Gallery Connect API. After that we wiil use the Connect API to perform the next operations.
Obtaining the App Id related to an app package name
Obtaining the basic app information
Obtaining the upload URL to upload a new version of our app
Obtaining the URL to download your App Report
All the API requests on this article will be performed by using Kotlin, so you can use it to develop your own management platform for mobile, desktop or web.
Generating the client credentials
Sign into AppGallery Connect and then go to Users and permissionfrr4qwq
{
"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"
}
From the left side panel, select Connect API and click on the Create button to generate your client credentials.
In the pop up dialog, choose a name, project and applicable roles for your API client. In this example, I will use Administrator.
Note: Some APIs require project-level authorization. Please specify the projects you want to access through these APIs. Select N/A if the APIs only require team-level authorization.
Once you have confirmed your settings, your client ID and secret key will appear on the Connect API screen.
Copy your Client ID and Key. Make sure to keep them safe.
Request Helper
I wrote a helper class to perform the REST calls to the Connect API
Code:
data class ResponseObject(val code:Int,val message: String,var responseBody:JSONObject?)
class RequestHelper {
companion object{
fun sendRequest(host:String, headers: HashMap<String,String>?, body:JSONObject?, requestType:String="GET"):ResponseObject{
try {
val conn = URL(host)
.openConnection() as HttpURLConnection
conn.apply {
requestMethod = requestType
headers?.apply {
for(key in keys)
setRequestProperty(key, get(key))
}
doOutput = requestType == "POST"
doInput = true
}
if(requestType!="GET"){
conn.outputStream.let{
body?.apply { it.write(toString().toByteArray()) }
}
}
val result = when (conn.responseCode) {
in 0..300 -> convertStreamToString(conn.inputStream)
else -> convertStreamToString(conn.errorStream)
}
//Returns the access token, or an empty String if something fails
return ResponseObject(conn.responseCode,conn.responseMessage, JSONObject(result)).also { conn.disconnect() }
} catch (e: Exception) {
return ResponseObject(400,e.toString(),null)
}
}
private fun convertStreamToString(input: InputStream): String {
return BufferedReader(InputStreamReader(input)).use {
val response = StringBuffer()
var inputLine = it.readLine()
while (inputLine != null) {
response.append(inputLine)
inputLine = it.readLine()
}
it.close()
response.toString()
}
}
}
}
Obtaining the App Level Access Token
This is a mandatory step, the access token contains information about the scope level provided to your client credentials.
Perform the following POST request:
Hostname: connect-api.cloud.huawei.com
Path: /api/oauth2/v1/token
Headers:
Content-Type: application/json
Body:
Code:
{
"grant_type":"client_credentials",
"client_id":"YOUR_CLIENT ID",
"client_secret":"YOUR_CLIENT_KEY"
}
If everything goes fine, the request will return an access token and the validity period of the token. You can use the same access token for any of the following operations until the expiration time. If the token expires, you must apply for a new one.
Example:
Code:
data class AGCredential(val clientId: String, val key: String)
class AGConnectAPI(credential: AGCredential) {
companion object {
@JvmField
val HOST = "https://connect-api.cloud.huawei.com/api"
private val MISSING_CREDENTIALS = "must setup the client credentials first"
val MISSING_CREDENTIALS_RESPONSE=ResponseObject(403, MISSING_CREDENTIALS, null)
}
var token: String? = null
var credential: AGCredential= credential
set(value) {
field = value
getToken()
}
init {
getToken()
}
private fun getToken(): Int {
val host = "$HOST/oauth2/v1/token"
val headers = HashMap<String, String>().apply {
put("Content-Type", "application/json")
}
val body = JSONObject().apply {
put("client_id", credential.clientId)
put("client_secret", credential.key)
put("grant_type", "client_credentials")
}
val response = RequestHelper.sendRequest(host, headers, body, "POST")
val token = response.responseBody?.let {
if (it.has("access_token")) {
it.getString("access_token")
} else null
}
return if (token != null) {
this.token = token
200
} else response.code
}
}
Obtaining the App Id for a given package name
The App Id is required as a unique identifier for app management operations.
Perform the following GET request:
Hostname: connect-api.cloud.huawei.com
Path: /api/publish/v2/appid-list?packageName=$packageName
Headers:
Authorization: Bearer $token
client_id: clientId
Example
Code:
fun queryAppId(packageName: String): ResponseObject {
return if (!token.isNullOrEmpty()) {
val url = "$HOST/publish/v2/appid-list?packageName=$packageName"
RequestHelper.sendRequest(url, getClientHeaders(), null)
} else MISSING_CREDENTIALS_RESPONSE
}
private fun getClientHeaders(): HashMap<String, String> {
return HashMap<String, String>().apply {
put("Authorization", "Bearer $token")
put("client_id", credential.clientId)
}
}
Obtaining the upload URL to upload a new version of our app
The Connect API provides URLs to upload different files of your app configuration in AppGallery. For this example we will get the URL to upload the APK. Currently, files with the following extensions can be uploaded:
apk/rpk/pdf/jpg/jpeg/png/bmp/mp4/mov/aab.
Perform the following GET request:
Hostname: connect-api.cloud.huawei.com
Path: /api//publish/v2/upload-url?appId=$appId&suffix=$(apk/rpk/pdf/jpg/jpeg/png/bmp/mp4/mov/aab)
Headers:
Content-Type: application/json
Authorization: Bearer $token
client_id: clientId
Example:
Code:
fun getUploadURL(appId: String,suffix: String):ResponseObject{
return if (!token.isNullOrEmpty()){
val url="$HOST/publish/v2/upload-url?appId=$appId&suffix=$suffix"
RequestHelper.sendRequest(url,getClientHeaders(),null)
} else MISSING_CREDENTIALS_RESPONSE
}
Obtaining the URL to download your App Report
You can download detailed reports about your app downloads and installations, purchases made in your app and if your app is a paid app, you can get the Paid Download report.
For the Paid Download report the hostname is different for every region
Hostname: https://{domain}
Domain name for China: connect-api.cloud.huawei.com
Domain name for Europe: connect-api-dre.cloud.huawei.com
Domain name for Asia Pacific: connect-api-dra.cloud.huawei.com
Domain name for Russia: connect-api-drru.cloud.huawei.com
Path: /api/report/distribution-operation-quality/v1/orderDetailExport/$appId
Headers:
Authorization: Bearer $token
client_id: clientId
This API require some querys to generate the report, to se the query details, refer to the official documentation
Example:
Code:
fun getReportUrl(
appId: String,
lang: String,
startTime: String,
endTime: String,
filterConditions:HashMap<String,String>
):ResponseObject {
return if (!token.isNullOrEmpty()){
val fc = StringBuilder().apply {
for (key in filterConditions.keys) {
append("&filterCondition=")
append(key)
append("&filterConditionValue=")
append(filterConditions[key])
}
}
val url =
"$HOST/report/distribution-operation-quality/v1/orderDetailExport/$appId?language=$lang&startTime=$startTime&endTime=$endTime${fc}"
RequestHelper.sendRequest(url,getClientHeaders(),null)
} else MISSING_CREDENTIALS_RESPONSE
}
Conclusion
Now you know what is the Connect API and how you can use it to automatize some management operations or to develop yur own custom console. There are also a lot of things what you can do with the Connect API. For more information you can refer to the official documentation.
Check this and other demos: https://github.com/danms07/dummyapp
Using this API can we customize Roles and permissions user basis?
{
"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"
}
These days mobile devices are part of our life. We do many operations from our mobile phones such as making payment, logging in to social media accounts, checking our bank accounts.
These are the operations which need high security level. If our device will have malicious apps or something like that, our accounts will have trouble and we may suffer many financial and moral damages.
In this article, I will talk about how to improve app security by using HMS Safety Detect Kit.
To do that, I have developed a simple secure web browser app. Because in web browsers, we can use bank websites, we can login to our social media, we can make some payment and use our credit/bank card information. We wouldn’t like to our information to be stolen.
App Preparations
I use Koin framework for dependency injection in my application.
To use Koin Framework in our application, we should add 3 dependencies to our app. In the above, you can find dependencies which you need to add in app-level build.gradle file.
Code:
def koinVersion = "2.2.0-rc-4"
dependencies {
....
// Koin for Android
implementation "org.koin:koin-android:$koinVersion"
// Koin Android Scope feature
implementation "org.koin:koin-android-scope:$koinVersion"
// Koin Android ViewModel feature
implementation "org.koin:koin-android-viewmodel:$koinVersion"
}
After we have implemented the Koin dependencies, we need to create our modules which we will add in our application class.
We will get necessary objects with the help of these modules. I prefer to define different module files for different works.
Code:
val applicationModule = module {
single(named("appContext")){ androidApplication().applicationContext }
factory { HmsHelper() }
factory { SystemHelper() }
}
Code:
val dataModule = module {
factory<ErrorItem>(named("HmsNotAvailable")) { ErrorItem(
icon = ContextCompat.getDrawable(get(named("appContext")), R.drawable.huawei)!!,
title = androidContext().getString(R.string.hms_not_available),
message = androidContext().getString(R.string.download_hms_core)) }
factory<ErrorItem>(named("DeviceNotSecure")) { ErrorItem(
icon = ContextCompat.getDrawable(get(named("appContext")), R.drawable.ic_device_not_secure)!!,
title = androidContext().getString(R.string.device_not_secure),
message = androidContext().getString(R.string.device_not_secure_message)) }
factory<ErrorItem>(named("MaliciousApps")) { ErrorItem(
icon = ContextCompat.getDrawable(get(named("appContext")), R.drawable.ic_malicious_apps)!!,
title = androidContext().getString(R.string.device_not_secure),
message = androidContext().getString(R.string.malicious_apps_message)) }
}
Code:
val viewModelModule = module {
viewModel { SplashViewModel() }
}
After we have defined our modules, we need to setup Koin in our application class.
While starting Koin, we should add our modules which we have defined above, and if we want to use app context, we should androidContext value.
XML:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.berkberber.hms_securewebbrowser">
....
<application
android:name=".SecureWebBrowserApp"
....
>
....
</application>
</manifest>
Code:
class SecureWebBrowserApp: Application(){
override fun onCreate() {
super.onCreate()
setup()
}
private fun setupKoin(){
startKoin {
androidContext([email protected])
modules(
applicationModule,
viewModelModule,
dataModule
)
}
}
private fun setup(){
setupKoin()
}
}
To get more information about app and to see how I used other things such as Navigation Component, MVVM, and etc. you can visit my GitHub repository.
HMS Safety Detect
Safety Detect Kit helps us to improve the security level of our apps. There are 5 different APIs we can use with HMS Safety Detect Kit.
SysIntegrity API: Helps us to check device security. We can determine that device has been rooted or has not.
AppsCheck API: Helps us to determine and list malicious apps which have installed to device.
URLCheck API: Helps us check whether websites are safe.
UserDetect API: Helps us to determine that user is fake or is not.
WifiDetect API: Helps us to check whether Wi-Fi which the device has connected is secure.
Note: UserDetect API is available outside of Chinese mainland. WifiDetect API is available only in the Chinese mainland.
In this article, I have been focused on app security. So, I used SysIntegrity API and AppsCheck API and I will give you informations about these APIs.
Checking is HMS available on device (optional)
We will use Safety Detect Kit in our application. Safety Detect Kit requires HMS Core to be installed on the device.
We don’t have to make this control, but if device doesn’t have HMS, we can’t use HMS Safety Detect Kit. That’s why I recommend you to check HMS Core availability on device and if device doesn’t have HMS, it is better to show an error screen to user.
To check HMS availability we need to add base HMS dependency to our app-level build.gradle file.
To check that device has HMS support or has not, we can write very basic function called as isHmsAvailable().
Code:
def hmsBaseVersion = "5.0.3.300"
dependencies {
...
// HMS Base
implementation "com.huawei.hms:base:${hmsBaseVersion}"
}
Code:
class HmsHelper: KoinComponent{
private val appContext: Context by inject(named("appContext"))
fun isHmsAvailable(): Boolean {
val isAvailable = HuaweiApiAvailability.getInstance().isHuaweiMobileNoticeAvailable(appContext)
return (ConnectionResult.SUCCESS == isAvailable)
}
}
If this function returns true, that means device has HMS support and we can start our application.
If this function returns false, that means device doesn’t have HMS support and we shouldn’t start our application. We may show an error screen to user.
SysIntegrity API
SysIntegrity API helps us to check that the user’s device is secure or is not. Even if the device has been rooted, SysIntegrity API will tell us that device is not secure.
To check the device security, we can call our isDeviceSecure() function.
As you see, this function will create a nonce value with an algorithm and pass this value to checkDeviceSecurity() function.
You may ask that, what is the algorithm value which I have used as “Constants.SAFETY_DETECT_ALGORITHM”. You can define this algorithm value as shown in below:
Code:
object Constants{
const val BASIC_INTEGRITY = "basicIntegrity"
const val SAFETY_DETECT_ALGORITHM = "SHA1PRNG"
}
As you see, we have defined two different values. We will use these values while checking device security.
You already know where to use SAFETY_DETECT_ALGORITHM value.
We will use BASIC_INTEGRITY value to get device security situation from JSON.
If this value returns true, that means user’s device is secure.
If this value returns false, that means device is not secure or device has been rooted.
Code:
object SafetyDetectService : KoinComponent {
private val appContext: Context by inject(named("appContext"))
private val client: SafetyDetectClient = SafetyDetect.getClient(appContext)
fun isDeviceSecure(serviceListener: IServiceListener<Boolean>) {
val nonce = ByteArray(24)
try {
val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
SecureRandom.getInstanceStrong()
else
SecureRandom.getInstance(Constants.SAFETY_DETECT_ALGORITHM)
random.nextBytes(nonce)
} catch (error: NoSuchAlgorithmException) {
serviceListener.onError(ErrorType.NO_SUCH_OBJECT)
}
checkDeviceSecurity(nonce, serviceListener)
}
private fun checkDeviceSecurity(nonce: ByteArray, serviceListener: IServiceListener<Boolean>){
client.sysIntegrity(nonce, BuildConfig.APP_ID)
.addOnSuccessListener { sysIntegrityResp ->
SafetyDetectHelper.getPayloadDetailAsJson(sysIntegrityResp)?.let { jsonObject ->
serviceListener.onSuccess(jsonObject.getBoolean(Constants.BASIC_INTEGRITY))
} ?: kotlin.run {
serviceListener.onError(ErrorType.SERVICE_FAILURE)
}
}
.addOnFailureListener {
serviceListener.onError(ErrorType.SERVICE_FAILURE)
}
}
}
As I talked about above, we need to get a json object from SysIntegrityResp object which has been returned by SysIntegrity API. To get this value, we can define a helper object and we can add all operations about getting json in here.
As you see in the below, we will send a SysIntegrityResp object as parameter and with the help of this function, we can get json object about our device security.
Code:
object SafetyDetectHelper {
fun getPayloadDetailAsJson(sysIntegrityResp: SysIntegrityResp): JSONObject? {
val jwsStr = sysIntegrityResp.result
val jwsSplit = jwsStr.split(".").toTypedArray()
val jwsPayloadStr = jwsSplit[1]
val payloadDetail = String(Base64.decode(
jwsPayloadStr.toByteArray(StandardCharsets.UTF_8), Base64.URL_SAFE),
StandardCharsets.UTF_8)
return try {
JSONObject(payloadDetail)
}catch (jsonError: JSONException){
null
}
}
}
If device is secure, we can do our next operations which we need to do. If device is not secure, we should show an error screen to user and we shouldn’t let user to start our application.
AppsCheck API
AppsCheck API helps us to determine malicious apps in user’s device. Thus, if device has some malicious apps, we will not let user to start our application for user’s security.
getMaliciousAppsList() function gives us a list of malicious app and it uses MaliciousAppsData class which has been defined by Huawei as a model class.
This API will return us a response object and this object will have the malicious apps list. If there is not any malicious apps on the device, we can return null and let user to use our application.
But if there are some malicious apps, we shouldn’t let user to start our application and we can show an error screen to user. If we would like to we can list malicious apps to user.
Note: It is better to list malicious apps and let user to delete these applications from device. That is what I am doing in my app. Also, if we would like to do more operations about malicious apps, we can define our own class like I talked about below.
Code:
object SafetyDetectService : KoinComponent {
private val appContext: Context by inject(named("appContext"))
private val client: SafetyDetectClient = SafetyDetect.getClient(appContext)
fun checkMaliciousApps(serviceListener: IServiceListener<ArrayList<MaliciousApps>?>){
client.maliciousAppsList
.addOnSuccessListener { maliciousAppsListResp ->
if(maliciousAppsListResp.rtnCode == CommonCode.OK){
val maliciousAppsList: List<MaliciousAppsData> = maliciousAppsListResp.maliciousAppsList
if(maliciousAppsList.isEmpty())
serviceListener.onSuccess(null)
else{
var maliciousApps = arrayListOf<MaliciousApps>()
for(maliciousApp in maliciousAppsList){
maliciousApp.apply {
maliciousApps.add(MaliciousApps(packageName = apkPackageName,
sha256 = apkSha256,
apkCategory = apkCategory))
}
}
serviceListener.onSuccess(maliciousApps)
}
}
}
.addOnFailureListener {
serviceListener.onError(ErrorType.SERVICE_FAILURE)
}
}
}
If we would like to do more operations like getting app icon, app name and etc. we can define our own data class.
I defined my own data class as shown in the below to do more specific operations with malicious apps.
Code:
data class MaliciousApps(
val packageName: String,
val sha256: String,
val apkCategory: Int
): KoinComponent{
private val appContext: Context = get(named("appContext"))
private val systemHelper: SystemHelper = get()
fun getAppIcon(): Drawable = systemHelper.getAppIconByPackageName(packageName)
fun getAppName(): String = systemHelper.getAppNameByPackageName(packageName)
fun getThreatDescription(): String {
return when(apkCategory){
1 -> appContext.getString(R.string.risky_app_description)
2 -> appContext.getString(R.string.virus_app_description)
else -> ""
}
}
}
Here I am just using same values with Huawei’s MaliciousAppsData class. But I added my own functions in here to get app icon, app name and threat description.
To get more information about application by package name, we can define new object called as SystemHelper and we can do these operations in here.
Code:
class SystemHelper: KoinComponent {
private val appContext: Context by inject(named("appContext"))
/**
* Getting application information by package name
* @param packageName: Package name of the app that we want to get information about
* @return ApplicationInfo class to get app icons, app names and etc. by package name
*/
private fun getAppByPackageName(packageName: String): ApplicationInfo{
return appContext.packageManager.getApplicationInfo(packageName, 0)
}
/**
* Getting application icon by package name
* @param packageName: Package name of the app which we want to get icon
* @return Icon of the application as drawable
*/
fun getAppIconByPackageName(packageName: String): Drawable{
val app = getAppByPackageName(packageName)
return appContext.packageManager.getApplicationIcon(app)
}
/**
* Getting application name by package name
* @param packageName: Package name of the app which we want to get name
* @return Name of the application as drawable
*/
fun getAppNameByPackageName(packageName: String): String{
val app = getAppByPackageName(packageName)
return appContext.packageManager.getApplicationLabel(app).toString()
}
}
When API founds malicious apps we need to list these apps to user and let user to delete these apps from device.
To do that, we can use selectedApp() function. This function will take the malicious app and ask user to delete them.
We need to detect that user has accepted to deleting application or has not. We need to start activity with result and we need to listen this result. If user really delete the application, we need to remove it from list. If there is not any malicious app on list after removing it, we can navigate user to our app.
Code:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == DELETE_REQUEST_CODE){
when(resultCode){
Activity.RESULT_OK -> {
maliciousApps.remove(selectedMaliciousApp)
setRecyclerView()
}
Activity.RESULT_CANCELED -> {
Toast.makeText(requireContext(), requireContext().getString(R.string.should_delete_app), Toast.LENGTH_LONG).show()
}
}
}
}
private var deleteClickListener = object: DeleteClickListener{
override fun selectedApp(maliciousApp: MaliciousApps) {
var deleteIntent = Intent(Intent.ACTION_DELETE).apply {
data = Uri.parse("package:${maliciousApp.packageName}")
putExtra(Intent.EXTRA_RETURN_RESULT, true)
}
startActivityForResult(deleteIntent, DELETE_REQUEST_CODE)
}
}
To learn more about the app and examine it, you can visit my GitHub repository.
References
berkberberr/HMS-SecureWebBrowserExample: This repository is a secure web browser app which is using Huawei Mobile Services. (github.com)
Safety Detect: SysIntegrity, URLCheck, AppsCheck, UserDetect - HUAWEI Developer
{
"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 Search Kit includes device-side SDK and cloud-side APIs to use all features of Petal Search capabilities. It helps developers to integrate mobile app search experience into their application.
Huawei Search Kit offers to developers so much different and helpful features. It decreases our development cost with SDKs and APIs, it returns responses quickly and it helps us to develop our application faster.
As a developer, we have some responsibilities and function restrictions while using Huawei Search Kit. If you would like to learn about these responsibilities and function restrictions, I recommend you to visit following website.
https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
Also, Huawei Search Kit supports limited countries and regions. If you wonder about these countries and regions, you can visit the following website.
https://developer.huawei.com/consum...HMSCore-Guides-V5/regions-0000001056871703-V5
How to use Huawei Search Kit?
First of all, we need to create an app on AppGallery Connect and add related details about HMS Core to our project.
If you don’t know about how to integrate HMS Core to our project, you can learn all details from following Medium article.
https://medium.com/huawei-developers/android-integrating-your-apps-with-huawei-hms-core-1f1e2a090e98
After we have done all steps in above Medium article, we can focus on special steps of integrating Huawei Search Kit.
Our minSdkVersion should be 24 at minimum.
We need to add following dependency to our app level build.gradle file.
Code:
implementation "com.huawei.hms:searchkit:5.0.4.303"
Then, we need to do some changes on AppGallery Connect. We need to define a data storage location on AppGallery Connect.
Note: If we don’t define a data storage location, all responses will return null.
We need to initialize the SearchKit instance on our application which we have extended from android.app.Application class. To initialize the SearchKit instance, we need to set the app id on second parameter which has mentioned as Constants.APP_ID.
While adding our application class to AndroidManifest.xml file, we need to set android:usesCleartextTraffic as true. You can do all these steps as mentioned in red rectangles.
Getting Access Token
For each request on Search Kit, we need to use access token. I prefer to get this access token on splash screen of the application. Thus, we will be able to save access token and save it with SharedPreferences.
First of all, we need to create our methods and objects about network operations. I am using Koin Framework for dependency injection on this project.
For creating objects about network operations, I have created following single objects and methods.
Note: In above picture, I have initialized the koin framework and added network module. Check this step to use this module in the app.
Java:
val networkModule = module {
single { getOkHttpClient(androidContext()) }
single { getRetrofit(get()) }
single { getService<AccessTokenService>(get()) }
}
fun getRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder().baseUrl("https://oauth-login.cloud.huawei.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun getOkHttpClient(context: Context): OkHttpClient {
return OkHttpClient().newBuilder()
.sslSocketFactory(SecureSSLSocketFactory.getInstance(context), SecureX509TrustManager(context))
.hostnameVerifier(StrictHostnameVerifier())
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(1, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
inline fun <reified T> getService(retrofit: Retrofit): T = retrofit.create(T::class.java)
We have defined methods to create OkHttpClient and Retrofit objects. These objects have used as single to create Singleton objects. Also, we have defined one generic method to use Retrofit with our services.
To get an access token, our base URL will be “https://oauth-login.cloud.huawei.com/".
To get response from access token request, we need to define an object for response. The best way to do that is creating data class which is as shown in the below.
Java:
data class AccessTokenResponse(
@SerializedName("access_token") val accessToken: String?,
@SerializedName("expires_in") val expiresIn: Int?,
@SerializedName("token_type") val tokenType: String?
)
Now, all we need to do is, creating an interface to send requests with Retrofit. To get access token, our total URL is “https://oauth-login.cloud.huawei.com/oauth2/v3/token". We need to send 3 parameters as x-www-form-url encoded. Let’s examine these parameters.
grant_type: This parameter will not change depends on our application. Value should be, “client_credentials”.
client_id: This parameter will be app id of our project.
client_secret: This parameter will be app secret of our project.
Java:
interface AccessTokenService {
@FormUrlEncoded
@POST("oauth2/v3/token")
fun getAccessToken(
@Field("grant_type") grantType: String,
@Field("client_id") appId: String,
@Field("client_secret") clientSecret: String
): Call<AccessTokenResponse>
}
Now, everything is ready to get an access token. We just need to send the request and save the access token with SharedPreferences.
To work with SharedPreferences, I have created a helper class as shown in the below.
Java:
class CacheHelper {
companion object {
private lateinit var instance: CacheHelper
private var gson: Gson = Gson()
private const val PREFERENCES_NAME = BuildConfig.APPLICATION_ID
private const val PREFERENCES_MODE = AppCompatActivity.MODE_PRIVATE
fun getInstance(context: Context): CacheHelper {
instance = CacheHelper(context)
return instance
}
}
private var context: Context
private var sharedPreferences: SharedPreferences
private var sharedPreferencesEditor: SharedPreferences.Editor
private constructor(context: Context) {
this.context = context
sharedPreferences = this.context.getSharedPreferences(PREFERENCES_NAME, PREFERENCES_MODE)
sharedPreferencesEditor = sharedPreferences.edit()
}
fun putObject(key: String, `object`: Any) {
sharedPreferencesEditor.apply {
putString(key, gson.toJson(`object`))
commit()
}
}
fun <T> getObject(key: String, `object`: Class<T>): T? {
return sharedPreferences.getString(key, null)?.let {
gson.fromJson(it, `object`)
} ?: kotlin.run {
null
}
}
}
With the help of this class, we will be able to work with SharedPreferences easier.
Now, all we need to do it, sending request and getting access token.
Java:
object SearchKitService: KoinComponent {
private val accessTokenService: AccessTokenService by inject()
private val cacheHelper: CacheHelper by inject()
fun initAccessToken(requestListener: IRequestListener<Boolean, Boolean>) {
accessTokenService.getAccessToken(
"client_credentials",
Constants.APP_ID,
Constants.APP_SECRET
).enqueue(object: retrofit2.Callback<AccessTokenResponse> {
override fun onResponse(call: Call<AccessTokenResponse>, response: Response<AccessTokenResponse>) {
response.body()?.accessToken?.let { accessToken ->
cacheHelper.putObject(Constants.ACCESS_TOKEN_KEY, accessToken)
requestListener.onSuccess(true)
} ?: kotlin.run {
requestListener.onError(true)
}
}
override fun onFailure(call: Call<AccessTokenResponse>, t: Throwable) {
requestListener.onError(false)
}
})
}
}
If API returns as access token successfully, we will save this access token to device using SharedPreferences. And on our SplashFragment, we need to listen IRequestListener and if onSuccess method returns true, that means we got the access token successfully and we can navigate application to BrowserFragment.
Huawei Search Kit
In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.
In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.
To send requests for News Search, Image Search and Video Search, we need a CommonSearchRequest object.
In this app, I will get results about Corona in English. I have created the following method to return to CommonSearchRequest object.
Java:
private fun returnCommonRequest(): CommonSearchRequest {
return CommonSearchRequest().apply {
setQ("Corona Virus")
setLang(Language.ENGLISH)
setSregion(Region.WHOLEWORLD)
setPs(20)
setPn(1)
}
}
Here, we have setted some informations. Let’s examine this setter methods.
setQ(): Setting the keyword for search.
setLang(): Setting the language for search. Search Kit has it’s own model for language. If you would like examine this enum and learn about which Languages are supporting by Search Kit, you can visit the following website.
Huawei Search Kit — Language Model
setSregion(): Setting the region for search. Search Kit has it’s own model for region. If you would like examine this enum and learn about which Regions are supporting by Search Kit, you can visit the following website.
Huawei Search Kit — Region Model
setPn(): Setting the number about how much items will be in current page. The value ranges from 1 to 100, and the default value is 1.
setPs(): Setting the number of search results that will be returned on a page. The value ranges from 1 to 100, and the default value is 10.
Now, all we need to do is getting news, images, videos and show the results for these on the screen.
News Search
To get news, we can use the following method.
Java:
fun newsSearch(requestListener: IRequestListener<List<NewsItem>, String>) {
SearchKitInstance.getInstance().newsSearcher.setCredential(SearchKitService.accessToken)
var newsList = SearchKitInstance.getInstance().newsSearcher.search(SearchKitService.returnCommonRequest())
newsList?.getData()?.let { newsItems ->
requestListener.onSuccess(newsItems)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Image Search
To get images, we can use the following method.
Java:
fun imageSearch(requestListener: IRequestListener<List<ImageItem>, String>) {
SearchKitInstance.getInstance().imageSearcher.setCredential(SearchKitService.accessToken)
var imageList = SearchKitInstance.getInstance().imageSearcher.search(SearchKitService.returnCommonRequest())
imageList?.getData()?.let { imageItems ->
requestListener.onSuccess(imageItems)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Video Search
To get images, we can use the following method.
Java:
fun videoSearch(requestListener: IRequestListener<List<VideoItem>, String>) {
SearchKitInstance.getInstance().videoSearcher.setCredential(SearchKitService.accessToken)
var videoList = SearchKitInstance.getInstance().videoSearcher.search(SearchKitService.returnCommonRequest())
videoList?.getData()?.let { videoList ->
requestListener.onSuccess(videoList)
} ?: kotlin.run {
requestListener.onError("No value returned")
}
}
Showing on screen
All these results return a clickable url for each one. We can create an intent to open these URLs on the browser which has installed to device before.
To do that and other operations, I will share BrowserFragment codes for fragment and the SearchItemAdapter codes for recyclerview.
Java:
class BrowserFragment: Fragment() {
private lateinit var viewBinding: FragmentBrowserBinding
private lateinit var searchOptionsTextViews: ArrayList<TextView>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewBinding = FragmentBrowserBinding.inflate(inflater, container, false)
searchOptionsTextViews = arrayListOf(viewBinding.news, viewBinding.images, viewBinding.videos)
return viewBinding.root
}
private fun setListeners() {
viewBinding.news.setOnClickListener { getNews() }
viewBinding.images.setOnClickListener { getImages() }
viewBinding.videos.setOnClickListener { getVideos() }
}
private fun getNews() {
SearchKitService.newsSearch(object: IRequestListener<List<NewsItem>, String>{
override fun onSuccess(newsItemList: List<NewsItem>) {
setupRecyclerView(newsItemList, viewBinding.news)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private fun getImages(){
SearchKitService.imageSearch(object: IRequestListener<List<ImageItem>, String>{
override fun onSuccess(imageItemList: List<ImageItem>) {
setupRecyclerView(imageItemList, viewBinding.images)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private fun getVideos() {
SearchKitService.videoSearch(object: IRequestListener<List<VideoItem>, String>{
override fun onSuccess(videoItemList: List<VideoItem>) {
setupRecyclerView(videoItemList, viewBinding.videos)
}
override fun onError(errorMessage: String) {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
})
}
private val clickListener = object: IClickListener<String> {
override fun onClick(clickedInfo: String) {
var intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(clickedInfo)
}
startActivity(intent)
}
}
private fun <T> setupRecyclerView(itemList: List<T>, selectedSearchOption: TextView) {
viewBinding.searchKitRecyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = SearchItemAdapter<T>(itemList, clickListener)
}
changeSelectedTextUi(selectedSearchOption)
}
private fun changeSelectedTextUi(selectedSearchOption: TextView) {
for (textView in searchOptionsTextViews)
if (textView == selectedSearchOption) {
textView.background = requireContext().getDrawable(R.drawable.selected_text)
} else {
textView.background = requireContext().getDrawable(R.drawable.unselected_text)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setListeners()
getNews()
}
}
Java:
class SearchItemAdapter<T>(private val searchItemList: List<T>,
private val clickListener: IClickListener<String>):
RecyclerView.Adapter<SearchItemAdapter.SearchItemHolder<T>>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchItemHolder<T> {
val itemBinding = ItemSearchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchItemHolder<T>(itemBinding)
}
override fun onBindViewHolder(holder: SearchItemHolder<T>, position: Int) {
val item = searchItemList[position]
var isLast = (position == searchItemList.size - 1)
holder.bind(item, isLast, clickListener)
}
override fun getItemCount(): Int = searchItemList.size
override fun getItemViewType(position: Int): Int = position
class SearchItemHolder<T>(private val itemBinding: ItemSearchBinding): RecyclerView.ViewHolder(itemBinding.root) {
fun bind(item: T, isLast: Boolean, clickListener: IClickListener<String>) {
if (isLast)
itemBinding.itemSeparator.visibility = View.GONE
lateinit var clickUrl: String
var imageUrl = "https://www.who.int/images/default-source/infographics/who-emblem.png?sfvrsn=877bb56a_2"
when(item){
is NewsItem -> {
itemBinding.searchResultTitle.text = item.title
itemBinding.searchResultDetail.text = item.provider.siteName
clickUrl = item.clickUrl
item.provider.logo?.let { imageUrl = it }
}
is ImageItem -> {
itemBinding.searchResultTitle.text = item.title
clickUrl = item.clickUrl
item.sourceImage.image_content_url?.let { imageUrl = it }
}
is VideoItem -> {
itemBinding.searchResultTitle.text = item.title
itemBinding.searchResultDetail.text = item.provider.siteName
clickUrl = item.clickUrl
item.provider.logo?.let { imageUrl = it }
}
}
itemBinding.searchItemRoot.setOnClickListener {
clickListener.onClick(clickUrl)
}
getImageFromUrl(imageUrl, itemBinding.searchResultImage)
}
private fun getImageFromUrl(url: String, imageView: ImageView) {
Glide.with(itemBinding.root)
.load(url)
.centerCrop()
.into(imageView);
}
}
}
End
If you would like to learn more about Search Kit and see the Codelab, you can visit the following websites:
https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
https://developer.huawei.com/consumer/en/codelab/HMSSearchKit/index.html#0
Very nice guide.
Amazing.
Thank you very much
It's a very nice example.
{
"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"
}
Hello friends and welcome back to my series of integrating various Huawei services. In this article I will show the integration of Geocoding API using Retrofit to get the coordinates from a format address followed by the integration of Directions API where we input the aforementioned coordinates to get the directions and steps from origin to destination together with the distance and time calculation.
Introduction
As explained in the previous section, we have to perform various API requests and integrate them using Retrofit. We will take them step by step, starting from explaining these services and how we use them. To start your app development in Huawei, you first have to perform some configurations needed to use the Kits and Services it provides, by following this post.
Geocoding APIGeocoding API is a service providing two main functionalities:
Forward Geocoding: a service that enables the retrieval of spatial coordinates (latitude, longitude) from a structured address. It can return up to 10 results for any given address, ranking them according to importance and accuracy.
Reverse Geocoding: does the opposite of forward geocoding by providing formatted addresses when given a set of coordinates. This service can return up to 11 formatted addresses for the coordinates given, again according to importance and accuracy.
For the purpose of this article, we will be using Forward Geocoding to retrieve the coordinates of a site based on the formatted address.
Integration
The first thing we need to do after performing the necessary configurations, would be to add the dependencies in the app-level build gradle.
Code:
//Retrofit
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation("com.squareup.okhttp3:logging-interceptor:4.2.2")
After that we will set our Geocoding Retrofit Requests and Response data classes to determine what we need to send as a parameter and retrieve as a response.
Code:
data class GeocodingRequest(
u/SerializedName("address") val address: String?,
u/SerializedName("language") val language: String?
)
data class Location(
u/SerializedName("lng") val lng: Double?,
u/SerializedName("lat") val lat: Double?
)
data class GeocodingResponse(
u/SerializedName("returnCode") val returnCode: String? ,
u/SerializedName("sites") val sites: Array<Sites>?,
u/SerializedName("returnDesc") val returnDesc: String?
)
data class Sites(
u/SerializedName("formatAddress") val formatAddress: String?,
u/SerializedName("location") val location: Location?
)
You can determine the request and response parameters based on the rules of the API requests and our needs.
After setting the data classes, we will need to establish a Retrofit client that will serve as an authenticator and interactor with the API and send network requests.
Code:
class GeocodeRetrofit {
val BASE_URL_DIRECTIONS = "https://siteapi.cloud.huawei.com/mapApi/v1/siteService/"
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL_DIRECTIONS)
.client(setInterceptors())
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <S> createService(serviceClass: Class<S>?): S {
return retrofit.create(serviceClass)
}
private fun setInterceptors() : okhttp3.OkHttpClient {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY
return okhttp3.OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain ->
val url: okhttp3.HttpUrl = chain.request().url.newBuilder()
.addQueryParameter("key", API_KEY)
.build()
val request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.url(url)
.build()
chain.proceed(request)
}
.addInterceptor(logger)
.build()
}
}
The base URL is given as below, and we should keep in mind to add the API Key of the agconnect-services.json file.
val BASE_URL_DIRECTIONS = "https://siteapi.cloud.huawei.com/mapApi/v1/siteService/"
The next step would be to create a repo;
Code:
class GeocodingBaseRepo {
private var geocodingApis : GeocodingInterface? = null
fun getInstance(): GeocodingInterface?{
if(geocodingApis==null)
setMainApis()
return geocodingApis
}
private fun setMainApis(){
geocodingApis = GeocodeRetrofit().createService(GeocodingInterface::class.java)
}
}
We proceed by creating an interface that will serve as exactly that, an interface between the API and the Retrofit Client.
Code:
interface GeocodingInterface {
@Headers("Content-Type: application/json; charset=UTF-8")
@POST("geocode")
fun listPost (
@Body geocodingRequest: GeocodingRequest
): Call<GeocodingResponse>
}
Once we have stablished all of the above, we can finally request the API in our activity or fragment. To adapt it to our case, we have created to editable text fields where user can insert origin and destination addresses. Based on that we make two geocode API calls, for origin and destination respectively, and observe their results through callbacks.
Code:
fun performGeocoding(type: String, geocodingRequest: GeocodingRequest, callback: (ResultData<GeocodingResponse>) -> Unit){
GeocodingBaseRepo().getInstance()?.listPost(geocodingRequest)?.enqueue(
object : Callback<GeocodingResponse> {
override fun onFailure(call: Call<GeocodingResponse>, t: Throwable) {
Log.d(TAG, "ERROR GEOCODING" + t.message)
}
override fun onResponse(
call: Call<GeocodingResponse>,
response: Response<GeocodingResponse>
) {
if (response.isSuccessful) {
Log.d(TAG, "SUCCESS GEOCODING" + response.message())
response.body()?.let {
if(type == "parting"){
callback.invoke(ResultData.Success(response.body()))
}
if(type == "destination"){
callback.invoke(ResultData.Success(response.body()))
}
}
}
}
})
}
private fun callOriginData(){
geocodingRequest = GeocodingRequest(partingaddress, "EN")
performGeocoding("parting" ,geocodingRequest, callback = {
it.handleSuccess {
var startingLatitude = it.data?.sites?.get(0)?.location?.lat
var startingLongitude = it.data?.sites?.get(0)?.location?.lng
origin = startingLatitude?.let { it1 -> startingLongitude?.let { it2 ->
LatLngData(it1,
it2
)
} }!!
callDestinationData()
}
})
}
private fun callDestinationData(){
geocodingRequest = GeocodingRequest(destinationaddress, "EN")
performGeocoding("destination", geocodingRequest, {
it.handleSuccess {
var endingLatitude = it.data?.sites?.get(0)?.location?.lat
var endingLongitude = it.data?.sites?.get(0)?.location?.lng
destination = endingLatitude?.let { it1 -> endingLongitude?.let { it2 ->
LatLngData(it1,
it2
)
} }!!
callDirections()
}
})
}
Geocoding Origin Results
Geocoding Destination Results
Directions APIDirections API is a Huawei service that provides three main functionalities:
Walking Route Planning: Plans an available walking route between two points within 150 km.
Cycling Route Planning: Plans an available cycling route between two points within 500 km.
Driving Route Planning: Plans an available driving route between two points.
IntegrationAfter being done with Geocoding, we need to use the results data from it and insert it into Directions API requests to be able to get all three route planning available between origin and destination coordinates. Similar to Geocode, we first establish the request and response data classes.
Code:
data class DirectionsRequest(
@SerializedName("origin") val origin: LatLngData,
@SerializedName("destination") val destination: LatLngData )
data class LatLngData (
@SerializedName("lat") val lat: Double,
@SerializedName("lng") val lng: Double )
data class DirectionsResponse (@SerializedName("routes") val routes: List<Routes>,
@SerializedName("returnCode") val returnCode: String,
@SerializedName("returnDesc") val returnDesc: String)
data class Routes (@SerializedName("paths") val paths: List<Paths>,
@SerializedName("bounds") val bounds: Bounds)
data class Paths (@SerializedName("duration") val duration: Double,
@SerializedName("durationText") val durationText: String,
@SerializedName("durationInTraffic") val durationInTraffic: Double,
@SerializedName("distance") val distance: Double,
@SerializedName("startLocation") val startLocation: LatLngData,
@SerializedName("startAddress") val startAddress: String,
@SerializedName("distanceText") val distanceText: String,
@SerializedName("steps") val steps: List<Steps>,
@SerializedName("endLocation") val endLocation: LatLngData,
@SerializedName("endAddress") val endAddress: String)
data class Bounds (@SerializedName("southwest") val southwest: LatLngData,
@SerializedName("northeast") val northeast: LatLngData)
data class Steps (@SerializedName("duration") val duration: Double,
@SerializedName("orientation") val orientation: Double,
@SerializedName("durationText") val durationText: String,
@SerializedName("distance") val distance: Double,
@SerializedName("startLocation") val startLocation: LatLngData,
@SerializedName("instruction") val instruction: String,
@SerializedName("action") val action: String,
@SerializedName("distanceText") val distanceText: String,
@SerializedName("endLocation") val endLocation: LatLngData,
@SerializedName("polyline") val polyline: List<LatLngData>,
@SerializedName("roadName") val roadName: String)
We then create a Retrofit Client for Directions API.
Code:
class DirectionsRetrofit {
val BASE_URL_DIRECTIONS = "https://mapapi.cloud.huawei.com/mapApi/v1/"
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL_DIRECTIONS)
.client(setInterceptors())
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <S> createService(serviceClass: Class<S>?): S {
return retrofit.create(serviceClass)
}
private fun setInterceptors() : okhttp3.OkHttpClient {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY
return okhttp3.OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain ->
val url: okhttp3.HttpUrl = chain.request().url.newBuilder()
.addQueryParameter("key", API_KEY)
.build()
val request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.url(url)
.build()
chain.proceed(request)
}
.addInterceptor(logger)
.build()
}
}
In this case, what will serve as our Base URL will be the URL below:
val BASE_URL_DIRECTIONS = "https://mapapi.cloud.huawei.com/mapApi/v1/"
We create a repo once again;
Code:
open class DirectionsBaseRepo {
private var directionsApis : DirectionsInterface? = null
fun getInstance(): DirectionsInterface?{
if(directionsApis==null)
setMainApis()
return directionsApis
}
private fun setMainApis(){
directionsApis = DirectionsRetrofit().createService(DirectionsInterface::class.java)
}
}
And similarly to the previous process we followed in Geocode we need an interface:
Code:
interface DirectionsInterface {
@POST("routeService/{type}")
fun getDirectionsWithType(
@Path(value = "type",encoded = true) type : String,
@Body directionRequest: DirectionsRequest
): Call<DirectionsResponse>
}
The only part that is extra from the previous API request is that we need an enumerating class to store the different direction types which will be determined from the user.
Code:
enum class DirectionType(val type: String) {
WALKING("walking"),
BICYCLING("bicycling"),
DRIVING("driving")
}
The only thing left for us to do now is to make the API call within the activity / fragment.
For this part we have created three image buttons for three direction types, and we call the direction API based on the type users selected. Basically if user wants to see the driving route, they select the driving type and a Direction API request with type driving is made.
Code:
fun getDirections(type: String, directionRequest: DirectionsRequest, callback: (ResultData<DirectionsResponse>) -> Unit){
DirectionsBaseRepo().getInstance()?.getDirectionsWithType(type,directionRequest)?.enqueue(object : Callback<DirectionsResponse>{
override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {
Log.d(TAG, "ERROR DIRECTIONS" + t.message)
}
override fun onResponse(call: Call<DirectionsResponse>, response: Response<DirectionsResponse>) {
Log.d(TAG, "success DIRECTIONS" + response.message())
if(response.isSuccessful){
response.body()?.let {
callback.invoke(ResultData.Success(it))
}
}
}
})
}
Code:
getDirections(DirectionType.DRIVING.type, directionRequest, {
it.handleSuccess {
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.get(0)?.startLocation?.lat?.let { it1 ->
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.get(0)?.startLocation?.lng?.let { it2 ->
commonMap.animateCamera(
it1, it2, 10f )
}
}
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.forEach {
it.polyline.forEach{
commonPolylineCoordinates.add(CommonLatLng(it.lat, it.lng))
}
}
drawPolyline(commonPolylineCoordinates)
carDistance = it.data?.routes?.get(0)?.paths?.get(0)?.distanceText.toString()
binding.distanceByCar.setText(carDistance)
carTime = it.data?.routes?.get(0)?.paths?.get(0)?.durationText.toString()
binding.timebyCar.setText(carTime)
}
})
As a result you can make use of all the response fields, including the steps needed to reach a place, the distance and time, or take the polyline coordinates and draw a route on the map. For this project I have decided to draw the route on the map and calculate the time and distance between the coordinates.
The final result is displayed below:
Geocoding API + Directions API Results
Geocoding API + Directions API Results
Tips and Tricks
It is a little tricky to work with asynchronous data since you never know when they will return their responses. We need to call geocode APIs for both origin and destination, and we want to make sure that the destination is called after the origin. To perform this you can call the destination geocoding API in the handle success part of the origin geocoding API, this way you make sure when you get a destination, you will definitely have an origin.
Similarly, you want to call the directions API when you have both origin and destination coordinates, hence you can call it in the handle success part of the destination geocoding call. This way you can be sure directions API call will not have empty or static coordinates.
Be careful to clean the polyline after switching between navigation types.
ConclusionIn this article, we talked about the integration of Geocoding API and performing Forward Geocoding to get the coordinates of a place of origin and destination, based on the formatted addresses. We proceeded by retrieving the origin and destination coordinates and ‘feeding’ them to the Directions API requests to get the route planning for navigation types of driving, cycling and walking. Afterwards we get the response of the Directions API call and use the result data as needed from our use cases. In my case I used the polyline data to draw on the map, and display the distance and time from two places. I hope you give it a shot, let me know what you think. Stay healthy and happy, see you in other articles.
Referencehttps://developer.huawei.com/consum...iretions-api-introduction-0000001050178120-V5
https://developer.huawei.com/consum...ces-V5/webapi-forward-geo-0000001050163921-V5
https://github.com/HMS-Core/hms-mapkit-demo
What are the required permission?