{
"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
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!
Huawei's Nearby SDK is sort of like Android Beam on steroids. Apps implementing it can use it for local file transfer, but that's not all. Nearby also enables apps to do realtime local communication, which is useful for things like locak multiplayer gaming. Finally, it also supports messaging, in the form of "beacons" that an implementing app can use to retrieve relevant localized information.
If any of this seems like it could be useful for you, read on, cause we're going to implement it.
Preparation
First up, make sure you have a Huawei Developer Account. This process can take a couple days, and you'll need one to use this SDK, so be sure to start that as soon as possible. You can sign up at https://developer.huawei.com.
Next, you'll want to obtain the SHA-256 representation of your app's signing key. If you don't have a signing key yet, be sure to create one before continuing. To obtain your signing key's SHA-256, you'll need to use Keytool which is part of the JDK installation. Keytool is a command-line program. If you're on Windows, open CMD. If you're on Linux, open Terminal.
On Windows, you'll need to "cd" into the directory containing the Keytool executable. For example, if you have JDK 1.8 v231 installed, Keytool will be located at the following path:
Code:
C:\Program Files\Java\jdk1.8.0_231\bin\
Once you find the directory, "cd" into it:
Code:
C: #Make sure you're in the right drive
cd C:\Program Files\Java\jdk1.8.0_231\bin\
Next, you need to find the location of your keystore. Using Android's debug keystore as an example, where the Android SDK is hosted on the "E:" drive in Windows, the path will be as follows:
Code:
E:\AndroidSDK\.android\debug.keystore
(Keytool also supports JKS-format keystores.)
Now you're ready to run the command. On Windows, it'll look something like this:
Code:
keytool -list -v -keystore E:\AndroidSDK\.android\debug.keystore
On Linux, the command should be similar, just using UNIX-style paths instead.
Enter the keystore password, and the key name (if applicable), and you'll be presented with something similar to the following:
{
"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"
}
Make note of the SHA256 field.
SDK Setup
Now we're ready to add the Nearby SDK to your Android Studio project. Go to your Huawei Developer Console and click the HUAWEI AppGallery tile. Agree to the terms of use if prompted.
Click the "My projects" tile here. If you haven't already added your project to the AppGallery, add it now. You'll be asked for a project name. Make it something descriptive so you know what it's for.
Now, you should be on a screen that looks something like the following:
Click the "Add app" button. Here, you'll need to provide some details about your app, like its name and package name.
Once you click OK, some SDK setup instructions will be displayed. Follow them to get everything added to your project. You'll also need to add the following to the "dependencies" section of your app-level build.gradle file:
Code:
implementation 'com.huawei.hms:nearby:4.0.4.300'
If you ever need to come back to these instructions, you can always click the "Add SDK" button after "App information" on the "Project setting" page.
Now you should be back on the "Project setting" page. Find the "SHA-256 certificate fingerprint" field under "App information," click the "+" button, and paste your SHA-256.
Now, go to the Manage APIs tab on the "Project setting" page. Scroll down until you find "Nearby Service" and make sure it's enabled.
Now, if you're using obfuscation in your app, you'll need to whitelist a few things for HMS to work properly.
For ProGuard:
Code:
-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
For AndResGuard:
Code:
"R.string.hms*",
"R.string.agc*",
"R.string.connect_server_fail_prompt_toast",
"R.string.getting_message_fail_prompt_toast",
"R.string.no_available_network_prompt_toast",
"R.string.third_app_*",
"R.string.upsdk_*",
"R.layout.hms*",
"R.layout.upsdk_*",
"R.drawable.upsdk*",
"R.color.upsdk*",
"R.dimen.upsdk*",
"R.style.upsdk*
That's it! The Nearby SDK should now be available in your project.
Basic Usage
There are currently three ways to use the Nearby SDK: Nearby Connection, Nearby Message, and Beacon Management.
Nearby Connection
Nearby Connection is the API that allows you to locally transmit and receive data to and from another device. The first thing you'll need to do to implement this is declare the use of quite a few permissions. These should go in your AndroidManifest.xml.
XML:
<!-- Required for Nearby Discovery and Nearby Transfer -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- If you don't care about devices running Android 10 or later, this can be replaced with ACCESS_COARSE_LOCATION -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Required for FILE payloads -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Some of these permissions, like "ACCESS_FINE_LOCATION" are "dangerous"-level permissions, so make sure you request access to them on Android Marshmallow and above.
Now, there are three usage "modes" for this API: Mesh, Star, and P2P.
Mesh allows all involved devices to send and receive data to and from all other involved devices. This comes at the cost of bandwidth speed, though, so this should only be used for things with low data throughput, like game-state sharing.
Star is similar to Mesh, but with a twist: the bandwidth is higher. What's the trade-off? Well, one device has to act as a "hub" of sorts. That hub can send to and receive from all other involved devices, but the rest can only communicate to the hub itself. This is useful for things like mass video and file sharing.
Finally, there's P2P. This is a simple 1-on-1 connection, where one device sends a file to another. This method has the highest bandwidth, but obviously the strictest constraints.
Below is an example of how to broadcast the ability to connect using the Star policy. This would be used on potential clients for the "hub" to connect to.
Code:
//Begin broadcasting the ability to receive connections.
fun startBroadcasting() {
val policy = Policy.POLICY_STAR
val option = BroadcastOption.Builder()
.setPolicy(policy)
.build()
val dataCallback = object : DataCallback() {
override fun onReceived(p0: String?, p1: Data?) {
//We'll get to the implementation of this later.
}
override fun onTransferUpdate(p0: String?, p1: TransferStateUpdate?) {
}
}
val connectionCallback = object : ConnectCallback() {
override fun onDisconnected(endpointId: String) {
//The connection has been terminated
}
override fun onEstablish(endpointId: String, connectInfo: ConnectInfo) {
//A connection has been established.
//This is where you either accept or reject the connection.
//Both parties need to accept the connection. You can either
//present a confirmation to the user or silently accept.
//Similarly to Bluetooth pairing, you can display an auth code
//on both devices. Obtain the auth code:
val authCode = connectInfo.authCode
//Silently accept:
Nearby.getDiscoveryEngine(context)
.acceptConnect(endpointId, dataCallback)
}
override fun onResult(endpointId: String, connectResult: ConnectResult) {
//Handle the result of a connection request.
when (connectResult.status.statusCode) {
StatusCode.STATUS_SUCCESS -> {
//The connection was accepted.
//We can start exchanging data.
}
StatusCode.STATUS_CONNECT_REJECTED -> {
//The connection was rejected.
//Notify the user.
}
else -> {
//This shouldn't happen...
}
}
}
}
Nearby.getDiscoveryEngine(context)
//"NAME" should be something descriptive (what is this device?)
//"SERVICE_ID" should be your app's package name
.startBroadcasting("NAME", "SERVICE_ID", connectionCallback, option)
.addOnSuccessListener {
//Broadcasting successfully started
}
.addOnFailureListener {
//Broadcasting failed to start
}
}
//This device is no longer accepting connections
fun stopBroadcasting() {
Nearby.getDiscoveryEngine(context)
.stopBroadcasting()
}
Next, a device needs to scan for potential connections. The following code shows how to do that.
Code:
//Start scanning for available connections.
fun startScanning() {
val policy = Policy.POLICY_STAR
val option = ScanOption.Builder()
.setPolicy(policy)
.build()
val scanEndpointCallback = object : ScanEndpointCallback() {
override fun onFound(endpointId: String?, endpointInfo: ScanEndpointInfo?) {
//A device has been found. Use this opportunity to either automatically
//connect to it, or add it to a list for the user to select from.
}
override fun onLost(endpointId: String?) {
//A device has gone out of range, been turned off, etc.
//It's no longer available to connect to, so if you're
//presenting a list, make sure to remove it.
}
}
Nearby.getDiscoveryEngine(context)
.startScan("SERVICE_ID", scanEndpointCallback, option)
.addOnSuccessListener {
//Scanning started successfully
}
.addOnFailureListener {
//Scanning couldn't start
}
}
//Stop scanning for new devices
fun stopScanning() {
Nearby.getDiscoveryEngine(context)
.stopScan()
}
Finally, once the scan is complete and the user (or your code) has selected a device, you'll need to initiate the connection:
Code:
//Connect to a device. This should be called
//from the scanner.
//endpointId comes from the onFound() method of
//the ScanEndpointCallback
fun startConnection(endpointId: String) {
val dataCallback = object : DataCallback() {
override fun onReceived(endpointId: String, data: Data) {
//We'll get to the implementation of this later.
}
override fun onTransferUpdate(endpointId: String, update: TransferStateUpdate) {
}
}
val connectionCallback = object : ConnectCallback() {
override fun onDisconnected(endpointId: String) {
//The connection has been terminated
}
override fun onEstablish(endpointId: String, connectInfo: ConnectInfo) {
//A connection has been established.
//This is where you either accept or reject the connection.
//Both parties need to accept the connection. You can either
//present a confirmation to the user or silently accept.
//Similarly to Bluetooth pairing, you can display an auth code
//on both devices. Obtain the auth code:
val authCode = connectInfo.authCode
//Silently accept:
Nearby.getDiscoveryEngine(context)
.acceptConnect(endpointId, dataCallback)
}
override fun onResult(endpointId: String, connectResult: ConnectResult) {
//Handle the result of a connection request.
when (connectResult.status.statusCode) {
StatusCode.STATUS_SUCCESS -> {
//The connection was accepted.
//We can start exchanging data.
}
StatusCode.STATUS_CONNECT_REJECTED -> {
//The connection was rejected.
//Notify the user.
}
else -> {
//This shouldn't happen...
}
}
}
}
Nearby.getDiscoveryEngine(context)
.requestConnect("NAME", endpointId, connectionCallback)
.addOnSuccessListener {
//Request was sent successfully
}
.addOnFailureListener {
//Request failed to send
}
}
//End a connection
fun stopConnection(endpointId: String) {
Nearby.getDiscoveryEngine(context)
.disconnect(endpointId)
}
Finally, once everything is connected, it's time to start transfering data. There are currently three forms of data transfer: bytes, files, and streams.
Bytes
Nearby Connection allows you to send small packets of data in the form of byte arrays. This could be useful for if you only need to send some simple data, like a chat message, or a game-state update. The size limit is 32KB.
To send a byte array, use the following code:
Code:
//Send some data to a client in the form
//of a byte array.
fun sendByteData(endpointId: String, data: ByteArray) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromBytes(data))
}
Files
If you have a file you want to send (e.g. a video or music file), use this method.
Sending a file is similar to sending a byte array:
Code:
//Send some data to a client in the form
//of a File.
//Files received with this method are stored
//in the receiving device's Download folder.
fun sendFileData(endpointId: String, file: File) {
try {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromFile(file))
} catch (e: FileNotFoundException) {
//Handle accordingly
}
}
//Send some data to a client in the form
//of a File using ParcelFileDescriptor
//Files received with this method are stored
//in the receiving device's Download folder.
fun sendFileData(endpointId: String, file: ParcelFileDescriptor) {
try {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromFile(file))
} catch (e: FileNotFoundException) {
//Handle accordingly
}
}
Streams
If it's easier for you to send your data in the form of a stream,
you can also do that.
Yet again, the process is very similar to the previous two methods.
Code:
//Send some data to a client in the form
//of a stream.
fun sendStreamData(endpointId: String, stream: InputStream) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromStream(stream))
}
//Send soe data to a client in the form
//of a stream using ParcelFileDescriptor.
fun sendStreamData(endpointId: String, stream: ParcelFileDescriptor) {
Nearby.getTransferEngine(context)
.sendData(endpointId, Data.fromStream(stream))
}
_______
If you want to cancel a transfer, it's also fairly simple:
Code:
//Cancel the transmission of data.
//The dataId can be obtained from the
//Data instance being sent.
fun cancelTransmission(dataId: Long) {
Nearby.getTransferEngine(context)
.cancelDataTransfer(dataId)
}
Now that you know how to send data, it's time to go over receiving it. In the code examples above for broadcasting and connecting, there's an unimplemented DataCallback. Well, it's time to implement it. Below is an example of how you might do that.
Code:
//An example implementation of a DataCallback
class DataReceiver : DataCallback() {
//A method to hold received data until we can
//properly retrieve files and streams.
private val receivedData = HashMap<Long, Data>()
override fun onReceived(endpointId: String, data: Data) {
//There's some new data.
when (data.type) {
Data.Type.BYTES -> {
//The data received is in the byte array format.
//Retrieve it as such, and handle accordingly.
//This is the only format where it's safe to retrieve the data here.
val bytes = data.asBytes()
//However, in this implementation, we're going to temporarily store
//the data reference and retrieve it the same way as we do files
//and streams.
receivedData[data.id] = data
}
Data.Type.FILE, Data.Type.STREAM -> {
//Temporarily store the data reference until the transfer is complete.
receivedData[data.id] = data
}
}
}
override fun onTransferUpdate(endpointId: String, update: TransferStateUpdate) {
when (update.status) {
TransferStateUpdate.Status.TRANSFER_STATE_SUCCESS -> {
//The transfer is complete. Retrieve data and handle it.
val data = receivedData[update.dataId]
when (data?.type) {
Data.Type.BYTES -> {
val bytes = data.asBytes()
}
Data.Type.FILE -> {
val file = data.asFile()
}
Data.Type.STREAM -> {
val stream = data.asStream()
}
}
}
TransferStateUpdate.Status.TRANSFER_STATE_CANCELED -> {
//The transfer was canceled
}
TransferStateUpdate.Status.TRANSFER_STATE_FAILURE -> {
//The transfer failed
}
TransferStateUpdate.Status.TRANSFER_STATE_IN_PROGRESS -> {
//The transfer is still in progress. You can use this event to display and
//update a progress indicator.
val progressPercent = (update.bytesTransferred.toFloat() / update.totalBytes.toFloat() * 100).toInt()
}
}
}
}
Nearby Message
Next up is the Nearby Message API. This is a way to publish and subscribe to messages generated by other devices or by dedicated "beacons."
The first thing to do is declare permissions. In order to properly use this API, the following permissions must be requested and granted:
XML:
<uses-permission android:name="android.permission.INTERNET " />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Now, it's time to actually use it. First let's talk about publishing messages. Publishing and unpublishing is fairly simple:
Code:
//Publish a message for other devices to see.
//You can publish up to 50 messages within a
//5 second timespan.
//You can also specify a type string and/or
//a namespace string.
fun publishMessage(msg: String) {
Nearby.getMessageEngine(context)
.put(Message(msg.toByteArray()))
}
//Unpublish a message. The Message class
//takes care of checking message equality
//for you, so you only need to save the
//string itself.
//Make sure to call this in your component's
//onDestroy() method.
fun unpublishMessage(msg: String) {
Nearby.getMessageEngine(context)
.unput(Message(msg.toByteArray()))
}
Subscribing to messages is a bit more complex (obviously). There are two ways to subscribe: in the foreground, and in the background.
The following code is an example of how to subscribe in the foreground:
Code:
val handler = object : MessageHandler() {
override fun onFound(message: Message) {
//A new Message has been found.
}
override fun onLost(message: Message) {
//An existing message was lost.
}
//The following methods can be used to estimate the distance from a beacon
//or publisher, based on signal strength.
override fun onBleSignalChanged(message: Message, bleSignal: BleSignal) {
val strength = bleSignal.rssi
}
override fun onDistanceChanged(message: Message, distance: Distance) {
val dist = distance.meters
val precision = distance.precision
}
}
//Subscribe to new messages in the foreground.
//This should be run in a foreground Service or an Activity.
fun subscribeToMessagesForeground() {
val options = GetOption.Builder()
.setPicker(MessagePicker.Builder()
//MessagePicker.Builder has various options
//for filtering messages.
.build())
.setPolicy(com.huawei.hms.nearby.message.Policy.Builder()
//Policy.Builder has various options
//for filtering messages.
.build())
.build()
Nearby.getMessageEngine(context)
.get(handler, options)
}
Here's an example for subscribing in the background:
Code:
val intent = PendingIntent.getService(context, 0, Intent(context, MessageReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
//Subscribe to new messages in the background.
//To receive these messages, you'll need to set up
//an IntentService to handle a PendingIntent.
fun subscribeToMessagesBackground() {
val options = GetOption.Builder()
.setPicker(MessagePicker.Builder()
//MessagePicker.Builder has various options
//for filtering messages.
.build())
.setPolicy(com.huawei.hms.nearby.message.Policy.Builder()
//Policy.Builder has various options
//for filtering messages.
.build())
.build()
Nearby.getMessageEngine(context)
.get(intent, options)
}
//Usubscribe from new background messages.
//Make sure to call this on app exit.
fun unsubscribeFromMessagesBackground() {
Nearby.getMessageEngine(context)
.unget(intent)
}
class MessageReceiver : JobIntentService() {
override fun onHandleWork(intent: Intent) {
//Pass the data to the messaging API and let it
//call the appropriate callback methods.
Nearby.getMessageEngine(this)
.handleIntent(intent, object : MessageHandler() {
override fun onFound(message: Message) {
//A new Message has been found.
}
override fun onLost(message: Message) {
//An existing message was lost.
}
//The following methods can be used to estimate the distance from a beacon
//or publisher, based on signal strength.
override fun onBleSignalChanged(message: Message, bleSignal: BleSignal) {
val strength = bleSignal.rssi
}
override fun onDistanceChanged(message: Message, distance: Distance) {
val dist = distance.meters
val precision = distance.precision
}
})
}
}
Beacon Management
To set up beacons, please refer to Huawei's documentation.
Conclusion
And that's it! Be sure to check out Huawei's full documentation for more details.
{
"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 everyone,
In this article, I would like to tell you about Cloud DB, which online storage that Huawei offers to developers.
What is Cloud DB ?
Although Cloud DB is still in beta version, it is a successful and seamless database structure. In addition to the ease of use, attracts developers with its management and a user-friendly interface. In addition to providing data availability, consistency and security, CloudDB provides seamless data synchronization between the device and the cloud.
If you do not have a server when developing applications, Cloud DB server easily solves our data storage, maintenance and distribution. Also CloudDB is free.
Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections. Looking at their counterparts, it is understood how large and how important these numbers are.
Cloud DB Structure
Object Type: It represents each table in the standard database. In other words, each table containing data and columns is called Object Type.
Cloud DB Zone : Represents the data zone on the cloud side. According to the classic database, Cloud DB Zone is the name of the database or schema name.
Data Entires : It is the area that shows the added data. Here, you can add, update and delete data. When you add data, you will realize that the tables you are used to are in the same way. And while using this technology, it prevents you from suffering from strangeness.
How To Using Cloud DB ?
Now let’s see how to use Cloud DB. Since Cloud DB is still in beta, you have to send a mail to activate this service in your app. To use Cloud DB, you need to create an app after creating your Huawei Developer account. After completing all the necessary steps, you have to request the activation of the service by sending an e-mail to [email protected] with the sample header below. The service will be activated within 1–3 business days after your mail arrives. And you can start using it freely.
Cloud DB –Company Name — Developer ID — App ID
After your service activated, log in to AppGallery Connect and select your app under the heading “My Apps”. You can then access the “Cloud DB” panel under the “Build” tab from the menu on the left side of the page, by moving to the “Develop” tab in the upper left. After the page is loaded, activate the service by clicking the “Enable Now” button in the upper right corner.
A Cloud DB Zone must first be created. After then object types should be created. When creating the object type, column names must be entered and the primary key must be identified in the window that opens. Also, at this step, you can edit access control options for the object type. In the image of the below you can find, which users have which permissions should be given. After creating DB Zone and Object Type, Cloud DB provides to export data models and Helper class as JSON or Java for use them in the app without wasting time. After exporting the model classes as Java, you have to add these classes to the relevant directory of the app and start communicate with Cloud DB.
Cloud DB library should be added to the build.gradle file under the project’s app directory and the compatibility mode of the Java source code should be set as 1.8. For this, the following codes should be added to the gradle file and wait downloading the necessary dependencies by click Sync Now.
Code:
dependencies {
implementation 'com.huawei.agconnect:agconnect-database:1.2.1.301'
}
compileOptions {
targetCompatibility = 1.8
}
Let’s create a class named CloudDBZoneWrapper for all database operations. By defining all the upsert, query operations in this class, call these methods in the activity/fragment to be used. Thanks to this metods you can coding your app without clutter.
Firstly, Cloud DB objects should created to be used in this class.
Code:
private AGConnectCloudDB mCloudDB;
private CloudDBZone mCloudDBZone;
private ListenerHandler mRegister;
private CloudDBZoneConfig mConfig;
After then create an instance from the AGConnectCloudDB object in a constructor method.
Code:
public CloudDBZoneWrapper() {
mCloudDB = AGConnectCloudDB.getInstance();
}
Then, initAGConnectCloudDB method must be created for calling on the app landing page. This method must be run before the app is opened, before starting all DB operations. I follow the code by adding a log in every process step to follow the errors more easily. In this way, you can easily find out which line is wrong.
Code:
public static void initAGConnectCloudDB(Context context) {
AGConnectCloudDB.initialize(context);
Log.w(Constants.DB_ZONE_WRAPPER, "initAGConnectCloudDB" );
}
Next, creating Object Type, and open/close DBZone operations should be coding. These methods will be used before upsert and query operations for open DBZone and create Object Types.
Code:
public void createObjectType() {
try {
mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo());
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeError: " + e.getMessage());
}
}
public void openCloudDBZone() {
mConfig = new CloudDBZoneConfig("DB ZONE NAME HERE",
CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
mConfig.setPersistenceEnabled(true);
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneSuccess " );
try {
mCloudDBZone = mCloudDB.openCloudDBZone(mConfig, true);
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneError: " + e.getMessage());
}
}
public void closeCloudDBZone() {
try {
mCloudDB.closeCloudDBZone(mCloudDBZone);
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneError: " + e.getMessage());
}
}
Now, the necessary methods for upsert and query operations should be written. But first, a few callbacks have to be added to get the results of these actions to the activities and fragments where the actions are operated. In this way, all DB operations will be gathered in a single class, without the activity being too tired and without crowd of code.
Code:
public interface UiCallBack {
void onAddOrQuery(List<TableUser> userList);
void isLastID(int lastID);.
void isDataUpsert(Boolean state);
}
public void addCallBacks(UiCallBack uiCallBack) {
mUiCallBack = uiCallBack;
}
Now the necessary method for upsert operation should be written. Upsert contains both insert and update operations. If upsert with a certain ID, the data in the related line will update. If it is upsert by new ID, a new line will added. So, both insert and update are carried out with the same method.
First, it should be checked whether DBZone is created or not. If DBZone has an error, will not upsert data. Then, upsert with CloudDBZoneTask object. Since I will add data to the User table, I gave the user object as a parameter to this method. If you need to add data to other tables, you should create a new method and give the object of the related table as a parameter. When upsert operation complated , if upsert is successful, it must return true, if occured an error, return false. For this, at the beginning of the method, a variable named Boolean state was defined and its first value was set as false. Then, if upsert is successful, state is set to true, and if error occurs, method will return false.
Code:
public void insertUser(TableUser user) {
boolean state = false;
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<Integer> upsertTask = mCloudDBZone.executeUpsert(user);
if (mUiCallBack == null) {
return;
}
upsertTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer cloudDBZoneResult) {
state = true;
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : upsert " + cloudDBZoneResult + " records");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
state = false;
mUiCallBack.updateUiOnError("INSERT USER : Insert user info failed");
}
});
if (mUiCallBack != null) {
mUiCallBack.isDataUpsert(state);
}
}
Now, let’s make a query in the this class. For this, I will get the list of users which I added to the database with the same model class. Two methods are required when making query request. The first is the getAllUsers method for DB operations, and the other is called userListResult method, for add data to array. Firstly, CloudDBZone control should be done in getAllUsers method. Then, query request will make by creating a task. If the request is successful, the userListResult method is calling with the user object. If the request is successful, the userListResult method will called with the user object. An arrayList is created in the userListResult method, and all results are thrown into this list. Then, by adding a callback, the results can be called up in the activity or fragment.
Code:
public void getAllUsers() {
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<CloudDBZoneSnapshot<TableUser>> queryTask = mCloudDBZone.executeQuery(
CloudDBZoneQuery.where(TableUser.class),
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
queryTask.addOnSuccessListener(new OnSuccessListener<CloudDBZoneSnapshot<TableUser>>() {
@Override
public void onSuccess(CloudDBZoneSnapshot<TableUser> snapshot) {
userListResult (snapshot);
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : GoResults: ");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (mUiCallBack != null) {
mUiCallBack.updateUiOnError("GET USER DETAIL : Query user list from cloud failed");
}
}
});
}
private void userListResult (CloudDBZoneSnapshot<TableUser> snapshot) {
CloudDBZoneObjectList<TableUser> userInfoCursor = snapshot.getSnapshotObjects();
List<TableUser> userInfoList = new ArrayList<>();
try {
while (userInfoCursor.hasNext()) {
TableUser userInfo = userInfoCursor.next();
userInfoList.add(userInfo);
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + userInfo.getUser_city());
}
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + e.getMessage());
}
snapshot.release();
if (mUiCallBack != null) {
mUiCallBack.onAddOrQuery(userInfoList);
}
}
Thus, all database operations within the CloudDBZoneWrapper class have been completed. Now let’s examine how to data upsert or query in activity or fragment.
The UiCallBack method in the CloudDBZoneWrapper class should be implement as the CloudDBZoneWrapper.UiCallBack in the class you which want to do database operations. In this way, all added call back methods will override in this class. Then the CloduDBZoneWrapper object and a new Handler need to be created in the activity. The CloudDBZoneWrapper object must be called within the constructor method. Sample codes are as follows.
Code:
private MyHandler mHandler = new MyHandler();
private CloudDBZoneWrapper mCloudDBZoneWrapper;
private static final class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
// dummy
}
}
public ProfileFragment() {
mCloudDBZoneWrapper = new CloudDBZoneWrapper();
}
Then the object type must be created in onCreate and Cloud DB Zone must be opened. If query is not related to an event, if the data should be loaded while the page is opening, call the getAllUsers method after creating the object type and opening the DB Zone in onCreate.
Code:
mHandler.post(() -> {
mCloudDBZoneWrapper.addCallBacks(ProfileFragment.this);
mCloudDBZoneWrapper.createObjectType();
mCloudDBZoneWrapper.openCloudDBZone();
mCloudDBZoneWrapper.getAllUsers();
});
The callback method, which was added to the getAllUsers method in the CloudDBZoneWrapper class, was override in the fragment. In this override method, all data can be used. For example, if wants to access the information of a user with ID = 3 in the user list, the turned list by insert a for loop, user information with ID = 3 is obtained.
Code:
@Override
public void onAddOrQuery(List<TableUser> userList) {
for(int i = 0; i <= userList.size()-1; i++){
if(userList.get(i).getId().equals(“3”)){
userName = userList.get(i).getUser_name());
userPhone = userList.get(i).getUser_phone();
userMail = userList.get(i).getUser_mail();
userAge = userList.get(i).getUser_age();
userGender = userList.get(i).getUser_gender();
}
}
}
Now let’s make an upsert. As I writed before, Upsert includes update and insert operations. Both operations are uses the same method. If you want to update a row of data, you must post with the ID information of the data in that row. If a new data is to be added, it should be posted with a new ID.
At this point, Cloud DB has a lack. Unfortunately, the auto increment don’t have when creating the object type. In other words, ID value does not increase automatically when data is added. It has to be given manually. I solved this problem by getting the last ID in the table and increasing it.
Now, create a method called updateUser to update and send back the user information I have previously got in this fragment. Next, a new user object should created here and the values should set. If there is an not to be changed data (as in the example, age and gender ), old values must be set in them. Finally, make post request by calling the insertUser method created in the CloudDBZoneWrapper class.
Code:
public void updateProfile(){
TableUser user = new TableUser();
user.setUser_id(“3”);
user.setUser_name(“Yeni İsim”));
user.setUser_phone(“Yeni Telefon”);
user.setUser_mail(“Yeni Mail”);
user.setUser_age(userAge);
user.setUser_gender(userGender);
mHandler.post(() -> {
mCloudDBZoneWrapper.insertUser(user);
});
}
In the fragment, help from call back methods should be obtained to find out whether the transaction is successful. Status check can be done in call back method added to insertUser method.
Code:
@Override
public void isDataUpsert(Boolean state) {
if(state){
//successful
}else{
//unsuccessful
}
}
Finally, it is worth mentioning that there is an authentication requirement to upsert. Since Cloud DB is still in beta, absolutely has some errors. But as you can see, all of them are solved easily. For authentication, Auth Service offered by Huawei to developers should be used. A service that is very easy to use. You can find the Auth Service link on the below. After the authentication, your upsert will work. If authentication is not done, the result of upsert will return false.
"https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-auth-service-introduction"
Well explained, can we store large amount of data into cloud Db, is there any limitations.
sujith.e said:
Well explained, can we store large amount of data into cloud Db, is there any limitations.
Click to expand...
Click to collapse
Yes, Cloud DB has a limit but I think you can store large data in Cloud DB. Because Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections.
Introduction
Whether you are tracking down a weird behavior in your app or chasing a crash in app making the user frustrated, getting a precise and real time information is important. Huawei crash analytics is a primary crash reporting solution for mobile. It monitors and captures your crashes, intelligently analyses them, and then groups them into manageable issues. And it does this through lightweight SDK that won’t bloat your app. You can integrate Huawei crash analytics SDK with a single line of code before you publish.
{
"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"
}
In this article, we will change app theme using Huawei Remote configuration and if something goes wrong while fetching data from remote config, we will report crash/exception using Huawei Crash Service.
To learn how to change app theme using Huawei Dark mode Awareness service, refer this.
Prerequisite
If you want to use Huawei Remote Configuration and Crash Service, you must have a developer account from AppGallery Connect. You need to create an application from your developer account and then integrate the HMS SDK into your project. I will not write these steps so that the article doesn’t lose its purpose and I will assume that it is already integrated in your project. You can find the guide from the link below.
HMS Integration Guide
Integration
1. Enable Remote Configuration and Crash Service in Manage APIs. Refer to Service Enabling.
2. Add AGC connect plugin in app-level build.gradle.
Code:
apply plugin: 'com.huawei.agconnect'
3. Integrate Crash Service and Remote configuration SDK by adding following code in app-level build.gradle.
Code:
implementation 'com.huawei.agconnect:agconnect-remoteconfig:1.5.2.300'
implementation 'com.huawei.agconnect:agconnect-crash:1.5.2.300'4.
4. Add following code in root-level build.gradle.
Code:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
// Configure the Maven repository address for the HMS Core SDK.
maven {url 'https://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
// Add AppGallery Connect plugin configurations.
classpath 'com.huawei.agconnect:agcp:1.4.2.300'
}
}
allprojects {
repositories {
// Configure the Maven repository address for the HMS Core SDK.
maven {url 'https://developer.huawei.com/repo/'}
}
}
5. Declare the following permissions in Androidmanifest.xml
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Development
We will define JSON which will have mode value as 0 or 1.
1. If the value of mode is 0, we will use system setting to change app theme. For example, if device has dark mode enabled in system setting, our app theme will be dark.
2. If the value of mode is 1, we will force our app to use day theme.
Code:
{
"jsonmode": [{
"mode": 0,
"details": "system_settings_mode"
}]
}
Open AGC, select your project. Choose Growing > Remote Config and enable Remote Config service. Once the remote config is enabled, define the key-value parameters.
Key : “mode_status”
Value : {
"jsonmode": [{
"mode": "0",
"details": "system_settings_mode"
}]
}
Note: mode value should be int, however we are intentionally adding value as String, so that our app throws JSONException which we can monitor on AGC dashboard.
Implementation
Let’s create instance of AGConnectConfig and add the default value to hashmap before connecting to remote config service.
Java:
private void initializeRemoteConfig() {
agConnectConfig = AGConnectConfig.getInstance();
Map<String, Object> map = new HashMap<>();
map.put("mode_status", "NA");
agConnectConfig.applyDefault(map);
}
To fetch parameter values from Remote Configuration.
Java:
agConnectConfig.fetch(5).addOnSuccessListener(new OnSuccessListener<ConfigValues>() {
@Override
public void onSuccess(ConfigValues configValues) {
agConnectConfig.apply(configValues);
String value = agConnectConfig.getValueAsString("mode_status");
Log.d(TAG, "remoteconfig value : " + value);
try {
int mode = parseMode(value);
Log.d(TAG, "mode value : " + mode);
if(mode == 0) {
initilizeDarkModeListner();
}
else if(mode == 1) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
} catch (JSONException e) {
Log.e(TAG,"JSONException : " +e.getMessage());
AGConnectCrash.getInstance().recordException(e);
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, " error: " + e.getMessage());
}
});
To parse the JSON received from Remote config.
Code:
private int parseMode(String json) throws JSONException {
if(json != null) {
JSONObject jsonObj = new JSONObject(json);
JSONArray jsonArrayMenu = jsonObj.getJSONArray("jsonmode");
for (int i = 0; i < jsonArrayMenu.length(); i++) {
JSONObject modeJsonObj = jsonArrayMenu.getJSONObject(i);
return modeJsonObj.getInt("mode");
}
}
return -1;
}
If parsing is successful, we will able to retrieve the mode value as 0 or 1.
However if parsing is unsuccessful, JSONException will be thrown and we will log this exception in AGC using Huawei Crash Service.
Java:
catch (JSONException e) {
Log.e(TAG,"JSONException : " +e.getMessage());
AGConnectCrash.getInstance().recordException(e);
}
Now when app encounters crash, Crash service reports the crash on dashboard in App Gallery connect. To monitor crash, as follows:
1. Sign in to App Gallery connect and select my project.
2. Choose the app.
3. Select Quality > Crash on left panel of the screen.
If you see parsing implementation of JSON, expected mode value should be integer
"mode": 0
But mistakenly, we have added mode value as string in remote config.
Code:
{
"jsonmode": [{
"mode": "0",
"details": "system_settings_mode"
}]
}
Now when we try to run our app, it will throw JSONException, since we are expecting mode value as int from remote config. This exception will be added to AGC dashboard using Huawei crash service.
As a developer, when I go to AGC dashboard to monito my app crash report, I realize my mistake and update the value in AGC remote config as follows:
Code:
{
"jsonmode": [{
"mode": 0,
"details": "system_settings_mode"
}]
}
Now our app will change its theme based on system settings whether if dark mode is enabled or not.
Code snippet of MainActivity.java
Java:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private AGConnectConfig agConnectConfig;
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initializeRemoteConfig();
ConfigValues last = agConnectConfig.loadLastFetched();
agConnectConfig.apply(last);
agConnectConfig.fetch(5).addOnSuccessListener(new OnSuccessListener<ConfigValues>() {
@Override
public void onSuccess(ConfigValues configValues) {
agConnectConfig.apply(configValues);
String value = agConnectConfig.getValueAsString("mode_status");
Log.d(TAG, "remoteconfig value : " + value);
try {
int mode = parseMode(value);
Log.d(TAG, "mode value : " + mode);
if(mode == 0)) {
initilizeDarkModeListner();
}
else if(mode == 1) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
} catch (JSONException e) {
Log.e(TAG,"JSONException : " +e.getMessage());
AGConnectCrash.getInstance().recordException(e);
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, " error: " + e.getMessage());
}
});
}
private void initializeRemoteConfig() {
agConnectConfig = AGConnectConfig.getInstance();
Map<String, Object> map = new HashMap<>();
map.put("mode_status", "NA");
agConnectConfig.applyDefault(map);
}
private void initilizeDarkModeListner() {
Awareness.getCaptureClient(this).getDarkModeStatus()
// Callback listener for execution success.
.addOnSuccessListener(new OnSuccessListener<DarkModeStatusResponse>() {
@Override
public void onSuccess(DarkModeStatusResponse darkModeStatusResponse) {
DarkModeStatus darkModeStatus = darkModeStatusResponse.getDarkModeStatus();
if (darkModeStatus.isDarkModeOn()) {
Log.i(TAG, "dark mode is on");
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
Log.i(TAG, "dark mode is off");
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
}
})
// Callback listener for execution failure.
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
Log.e(TAG, "get darkMode status failed " + e.getMessage());
}
});
}
private int parseMode(String json) throws JSONException {
if(json != null) {
JSONObject jsonObj = new JSONObject(json);
JSONArray jsonArrayMenu = jsonObj.getJSONArray("jsonmode");
for (int i = 0; i < jsonArrayMenu.length(); i++) {
JSONObject modeJsonObj = jsonArrayMenu.getJSONObject(i);
return modeJsonObj.getInt("mode");
}
}
return -1;
}
}
Tips and Tricks
1. Huawei Crash services work on non-Huawei device.
2. AGConnectCrash.getInstance().testIt(mContext) triggers app crash. Make sure to comment or remove it before releasing your app.
3. Crash Service takes around 1 to 3 minutes to post the crash logs on App Gallery connect dashboard/console.
4. Crash SDK collects App and system data.
System data:
AAID, Android ID (obtained when AAID is empty), system type, system version, ROM version, device brand, system language, device model, whether the device is rooted, screen orientation, screen height, screen width, available memory space, available disk space, and network connection status.
App data:
APK name, app version, crashed stack, and thread stack.
5. The Crash SDK collects data locally and reports data to the collection server through HTTPS after encrypting the data.
Conclusion
In this article, we have learnt how Huawei crash service can help developers to monitor crash/exception report on AGC and fix it.
We uploaded wrong JSON data into Remote Configuration and cause our app to go into JSONException. Using Huawei Crash Service, we monitored the exception in AGC dashboard. After finding out issue in JSON data, we added correct data in remote config and fixed our app.
References
Huawei Crash Service
Huawei Remote Configuration
Original Source
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