AppGallery Connect Dynamic Ability Feature

AppGallery Team
AppGallery
Published in
7 min readJul 27, 2021

Hello everyone, in this blog I will explain to you what Dynamic Ability feature is and how it is used.

What Is Dynamic Ability?

Dynamic Ability is a service in which HUAWEI AppGallery implements dynamic loading based on the Android App Bundle technology. Apps integrated with the Dynamic Ability can dynamically download features or language packages from HUAWEI AppGallery on user requests. Its benefits are reducing the unnecessary consumption of network traffic and device storage space. Currently, the Dynamic Ability SDK supports mobile phones, smart screens, and speakers with screens.

How It Works?

After the newly created AAB file of an app is uploaded to HUAWEI AppGallery, the platform splits the app into multiple APKs based on three dimensions: language, screen density, and ABI architecture. When a user downloads an app, HUAWEI AppGallery delivers an APK that is suitable for the user device based on the device language, screen density, and ABI architecture. This reduces the consumption of storage space and network traffic on the user device without affecting the app’s features. When a user downloads an app for the first time, only the basic feature module of the app is downloaded, and dynamic features are downloaded only when necessary.

Development Process

First, we create an Anroid Studio project and we configure build.gradle files.

allprojects {

repositories {

maven {url 'http://developer.huawei.com/repo/'}

...

}

}

dependencies {

implementation 'com.huawei.hms:dynamicability:1.0.11.302'

...

}

After that, we need to add Dynamic Feature Module to our project.

We create module name and package name.

We configure module download options.

And we sync the project.

This is the structure of our main project and our dynamic feature.

We override the attachBaseContext method in the project and in an activity of a dynamic feature module, and call FeatureCompat.install to initialize the Dynamic Ability SDK.

override fun attachBaseContext(newBase: Context?) {

super.attachBaseContext(newBase)

// Start the Dynamic Ability SDK.

FeatureCompat.install(newBase)

}

We call FeatureInstallManagerFactory.create method to instantiate FeatureInstallManager in onCreate method of our main app.

override fun onCreate(savedInstanceState: Bundle?) {

mFeatureInstallManager = FeatureInstallManagerFactory.create(this)

...

}

We build a request for dynamic loading, one or more dynamic features can be added. And we call the installFeature method to install features.

fun installFeature(view: View?) {

...

// start install

val request = FeatureInstallRequest.newBuilder()

.addModule("dynamicfeature")

.build()

val task = mFeatureInstallManager!!.installFeature(request)

...

}

We register a listener to monitor the status of the feature installation request. There are three types of listeners: OnFeatureCompleteListener, OnFeatureSuccessListener, and OnFeatureFailureListener.

– OnFeatureCompleteListener: Callback is triggered no matter whether the status is successful or failed. We need to determine whether the request is successful or not. If the request fails, an exception will be thrown when FeatureTask.getResult is called.

fun installFeature(view: View?) {

...

task.addOnListener(object : OnFeatureCompleteListener<Int>() {

override fun onComplete(featureTask: FeatureTask<Int>) {

if (featureTask.isComplete) {

Log.d(TAG, "complete to start install.")

if (featureTask.isSuccessful) {

val result = featureTask.result

sessionId = result

Log.d(TAG, "succeed to start install. session id :$result")

} else {

Log.d(TAG, "fail to start install.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

}

})

...

}

OnFeatureSuccessListener: Callback is triggered only after HUAWEI AppGallery successfully responds to the request. The callback result contains sessionId, which is the unique ID of a dynamic loading task. With sessionId, we can obtain the dynamic loading progress and cancel a task any time.

fun installFeature(view: View?) {

...

task.addOnListener(object : OnFeatureSuccessListener<Int>() {

override fun onSuccess(integer: Int) {

Log.d(TAG, "load feature onSuccess.session id:$integer")

}

})

...

}

OnFeatureFailureListener: Callback is triggered only when HUAWEI AppGallery fails to respond.

fun installFeature(view: View?) {

...

task.addOnListener(object : OnFeatureFailureListener<Int?>() {

override fun onFailure(exception: Exception) {

if (exception is FeatureInstallException) {

val errorCode = exception.errorCode

Log.d(TAG, "load feature onFailure.errorCode:$errorCode")

} else {

exception.printStackTrace()

}

}

})

...

}

To use the Dynamic Ability, a user must agree to the agreement of HUAWEI AppGallery. Before downloading a dynamic feature, our app needs to verify that the user agrees to the agreement.

  • If the user agrees to the agreement, the installation process continues.
  • If the user rejects the agreement, the installation process is terminated.

private val mStateUpdateListener = InstallStateListener {

...

if (it.status() == FeatureInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {

try {

mFeatureInstallManager!!.triggerUserConfirm(it, this, 1)

} catch (e: SendIntentException) {

e.printStackTrace()

}

return@InstallStateListener

}

...

}

Before downloading and installation of a dynamic feature, our app checks whether the user’s device is using a mobile network. According to that, a data consumption reminder is displayed to the user for download consent.

  • If the user consents to the download, the app starts to download the dynamic feature.
  • If the user does not consent to the download, the app terminates the download task.

private val mStateUpdateListener = InstallStateListener {

...

if (it.status() == FeatureInstallSessionStatus.REQUIRES_PERSON_AGREEMENT) {

try {

mFeatureInstallManager!!.triggerUserConfirm(it, this, 1)

} catch (e: SendIntentException) {

e.printStackTrace()

}

return@InstallStateListener

}

...

}

We can monitor the download progress of the dynamic feature.

private val mStateUpdateListener = InstallStateListener {

...

if (it.status() == FeatureInstallSessionStatus.DOWNLOADING) {

val process: Long = it.bytesDownloaded() * 100 / it.totalBytesToDownload()

Log.d(TAG, "downloading percentage: $process")

Toast.makeText(applicationContext, "downloading percentage: $process", Toast.LENGTH_SHORT).show()

return@InstallStateListener

}

...

}

A created listener needs to be registered and deregistered at proper times.

override fun onResume() {

super.onResume()

mFeatureInstallManager?.registerInstallListener(mStateUpdateListener)

}

override fun onPause() {

super.onPause()

mFeatureInstallManager?.unregisterInstallListener(mStateUpdateListener)

}

We can check the installation status.

private val mStateUpdateListener = InstallStateListener {

...

if (it.status() == FeatureInstallSessionStatus.INSTALLED) {

Log.d(TAG, "installed success ,can use new feature")

Toast.makeText(applicationContext, "installed success , can test new feature ", Toast.LENGTH_SHORT).show()

startfeature.isEnabled = true

installfeature.isEnabled = false

return@InstallStateListener

}

if (it.status() == FeatureInstallSessionStatus.UNKNOWN) {

Log.d(TAG, "installed in unknown status")

Toast.makeText(applicationContext, "installed in unknown status ", Toast.LENGTH_SHORT).show()

return@InstallStateListener

}

if (it.status() == FeatureInstallSessionStatus.FAILED) {

Log.d(TAG, "installed failed, errorcode : ${it.errorCode()}")

Toast.makeText(applicationContext, "installed failed, errorcode : ${it.errorCode()}", Toast.LENGTH_SHORT).show()

return@InstallStateListener

}

...

}

If a dynamic feature does not need to be installed instantly, we can choose delayed installation. With this, the feature can be installed when the app is running in the background. After receiving a delay request, HUAWEI AppGallery will delay the installation based on the device running status.

fun delayedInstallFeature(view: View?) {

val features = arrayListOf<String>()

features.add("dynamicfeature")

val task = mFeatureInstallManager!!.delayedInstallFeature(features)

task.addOnListener(object : OnFeatureCompleteListener<Void?>() {

override fun onComplete(featureTask: FeatureTask<Void?>) {

if (featureTask.isComplete) {

Log.d(TAG, "complete to delayed_Install")

if (featureTask.isSuccessful) {

Log.d(TAG, "succeed to delayed_install")

} else {

Log.d(TAG, "fail to delayed_install.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

}

})

}

We can delay the uninstallation of a dynamic feature that is no longer used. The uninstallation is not executed instantly, it is executed when the app is running in the background.

fun delayedUninstallFeature(view: View?) {

val features = arrayListOf<String>()

features.add("dynamicfeature")

val task = mFeatureInstallManager!!.delayedUninstallFeature(features)

task.addOnListener(object : OnFeatureCompleteListener<Void?>() {

override fun onComplete(featureTask: FeatureTask<Void?>) {

if (featureTask.isComplete) {

Log.d(TAG, "complete to delayed_uninstall")

if (featureTask.isSuccessful) {

Log.d(TAG, "succeed to delayed_uninstall")

} else {

Log.d(TAG, "fail to delayed_uninstall.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

}

})

}

Each dynamic loading task has a unique ID, which is specified by sessionId. We can cancel an ongoing task based on sessionId at any time.

fun abortInstallFeature(view: View?) {

Log.d(TAG, "begin abort_install : $sessionId")

val task = mFeatureInstallManager!!.abortInstallFeature(sessionId)

task.addOnListener(object : OnFeatureCompleteListener<Void?>() {

override fun onComplete(featureTask: FeatureTask<Void?>) {

if (featureTask.isComplete) {

Log.d(TAG, "complete to abort_install.")

if (featureTask.isSuccessful) {

Log.d(TAG, "succeed to abort_install.")

} else {

Log.d(TAG, "fail to abort_install.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

}

})

}

We can obtain the execution status of the task.

fun getInstallState(view: View?) {

Log.d(TAG, "begin to get session state for: $sessionId")

val task: FeatureTask<InstallState> = mFeatureInstallManager!!.getInstallState(sessionId)

task.addOnListener(object : OnFeatureCompleteListener<InstallState>() {

override fun onComplete(featureTask: FeatureTask<InstallState>) {

if (featureTask.isComplete) {

Log.d(TAG, "complete to get session state.")

if (featureTask.isSuccessful) {

val state: InstallState = featureTask.result

Log.d(TAG, "succeed to get session state.")

Log.d(TAG, state.toString())

} else {

Log.d(TAG, "failed to get session state.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

}

})

}

We also can obtain the execution status of all tasks in the system.

fun getAllInstallStates(view: View?) {

Log.d(TAG, "begin to get all session states.")

val task = mFeatureInstallManager!!.allInstallStates

task.addOnListener(object : OnFeatureCompleteListener<List<InstallState>>() {

override fun onComplete(featureTask: FeatureTask<List<InstallState>>) {

Log.d(TAG, "complete to get session states.")

if (featureTask.isSuccessful) {

Log.d(TAG, "succeed to get session states.")

val stateList = featureTask.result

for (state in stateList) {

Log.d(TAG, state.toString())

}

} else {

Log.d(TAG, "fail to get session states.")

val exception = featureTask.exception

exception.printStackTrace()

}

}

})

}

During actual usage of an app, the user language may vary. We can dynamically load one or more language packages in our app at a time.

A language package does not need to contain the country code. For example, to load a French package, only fr needs to be entered. The Dynamic Ability SDK automatically loads French resources of multiple countries and regions. To reduce ambiguity, it is advised to use the Locale.forLanguageTag(lang) method to modify the original value of language.

fun loadLanguage(view: View?) {

if (mFeatureInstallManager == null) {

return

}

// start install

val languages = arrayListOf<String>()

languages.add("fr")

val builder = FeatureInstallRequest.newBuilder()

for (lang in languages) {

builder.addLanguage(Locale.forLanguageTag(lang))

}

val request = builder.build()

val task = mFeatureInstallManager!!.installFeature(request)

task.addOnListener(object : OnFeatureSuccessListener<Int>() {

override fun onSuccess(result: Int) {

Log.d(TAG, "onSuccess callback result $result")

}

})

task.addOnListener(object : OnFeatureFailureListener<Int?>() {

override fun onFailure(exception: java.lang.Exception) {

if (exception is FeatureInstallException) {

Log.d(

TAG, "onFailure callback "

+ exception.errorCode

)

} else {

Log.d(TAG, "onFailure callback ", exception)

}

}

})

task.addOnListener(object : OnFeatureCompleteListener<Int?>() {

override fun onComplete(task: FeatureTask<Int?>) {

Log.d(TAG, "onComplete callback")

}

})

}

With this implementation, users can download the basic feature of the app first. And when they need, they can install other necessary features and uninstall unnecessary features.

References

https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-featuredelivery-introduction

--

--

AppGallery Team
AppGallery

Insights, success stories, and monetization tips for app development at https://medium.com/appgallery