Photo by rawpixel on Unsplash

Implement an in-app update function

Recently Google announced they’re going to offer an in-app update API available for apps published via Google Play. However, apps that are not distributed via the Play Store won’t have access to this API. I recently implemented an in-app update function and, as I didn’t find any complete examples out there, wanted to share my experience.

This is what we’re going to achieve.

Finding out if an update is available

Before we implement the actual updater we first need to know if an update is available. I used Firebase Remote Config to define a property latestVersionCode which would always point to the version code of the latest version. As it is an incremental integer it’s perfectly suited for this kind of comparison. At app start I compare the property against the value from BuildConfig.VERSION_CODE and, if it’s higher, I assume an update is available. I encapsulated the Remote Config code within a RxSingle to better fit my code:

remoteConfiguration.fetchLatestVersionCode()
.onErrorReturnItem(BuildConfig.VERSION_CODE)
.subscribe(versionCode -> {
if (versionCode > BuildConfig.VERSION_CODE) downloadUpdate()
})

In reality I actually show a dialog here informing the user about the update and let him choose if he wants to download it, but for now we directly download it.

Downloading the new APK

We’re using OkHttp to do the download and, as we want to show a progress bar to the user, a special ResponseBody to get informed about the progress. Luckily, there’s an official sample available how to do that here. I migrated that to Kotlin:

Now we simply need to add this ResponseBody as an network interceptor when building theOkHttp client:

val progressListener = object : ProgressListener { ... }
val okHttpClient = OkHttpClient.Builder().addNetworkInterceptor {
val
originalResponse = it.proceed(it.request())
val responseBody = originalResponse.body() ?: return@addNetworkInterceptor originalResponse

return@addNetworkInterceptor originalResponse.newBuilder()
.body(DownloadProgressBody(responseBody, progressListener))
.build()
}.build()

And that’s it. The progressListener will now get informed about the progress. My update() function looks more or less like this:

override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = ((bytesRead.toFloat() / contentLength) * 100).roundToInt()
runOnUiThread { progressBar.progress = progress }

if
(done) installApk()
}

It updates my ProgressBar and if the download is finished it starts the installation.


Now, to trigger the actual download, we make a call via OkHttp to apkUrl which is the URL of your APK and save it in a file val apkFile = File(filesDir, "update.apk") using our internal storage.

val request = Request.Builder().url(apkUrl).build()
val response = okHttpClient.newCall(request).execute()

if (!response.isSuccessful) throw Error("Request failed")

response.body()?.source()?.use { bufferedSource ->
val
bufferedSink = Okio.buffer(Okio.sink(apkFile))
bufferedSink.writeAll(bufferedSource)
bufferedSink.close()
}

Note: You can only download to internal storage on Android N and above. See end of the article for information for lower versions.


Installing the downloaded APK

Just like before this section only covers Android N and above.

First we need to add a FileProvider to our Manifest, so we can grant the package installer access to our internal storage later.

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"
/>
</provider>

with xml/file_provider_paths being

<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="files"
path="."
/>
</paths>

So we use /files from the app’s internal storage. We also need to add a permission to the Manifest, so we’re allowed to install new apps:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

Then we can simply start an intent to execute the system’s default app installer:

fun installApk() {
val
uri = FileProvider.getUriForFile(
applicationContext,
"$packageName.provider",
apkFile
)
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
activity.startActivity(intent)
}

And via the FileProvider we grant the package installer the permission to read apkFile from our app’s internal storage. The rest is handled by the system now.

You may want to provide your users a better experience and delete the apkFile afterwards.


The problem with versions lower Nougat

Below Android 7 the package installer worked different, making it impossible to install the APK from internal storage, see here for reference. The workaround would be to download the APK to the external storage. Then you also need to handle runtime permissions for reading & writing to external storage.

Additionally, there are security issues if you download the APK to external storage, see this Google issue, which require you to implement additional security mechanisms, like a SHA comparison check to validate it’s really your APK you’re going to install and not a modified one.

In my case I decided to not offer users on Android 5 and 6 an in-app update functions because of this complexity, but I at least wanted to document the trouble.