Auto Updating an Android App outside the Play Store

In my previous post, I explained how I was writing a private Android app that is only intended for personal use (basically a hobby project). Because of this, I do not intend to release it in the Google Play Store..

But that brings up an interesting question:

How am I going to update this app over time?

One of the benefits of hosting your app in the Google Play store is that you get the ability to auto-update your apps (amongst other things like in-app billing etc). But, as the official documentation explains, there are many ways you can distribute your Android app including:

  • Distributing through an app marketplace
  • Distributing your apps by email
  • Distributing through a website
  • This is an extra bullet point!

This blog post explains the steps I went through to get my Android app to auto-update via a website. In a nutshell, I wanted the app to be able to:

  • Check a website to see if an updated ..apk is available
  • If so, give the user the option to download the updated .apk
  • Start the install process

To break that down a little bit more, I needed to:

  1. Extract the app version from the embedded resources in the Android App
  2. Create a new WebAPI Endpoint that returns latest version of app and MD5 hash of the .apk
  3. Add the .apk file to IIS and setup Mimetype for successful download
  4. Create about page in Android app with a "check for updates" button
  5. Call the version API to see if the server has an updated version of the app available
  6. Call the web URL that points to the .apk file and download it using OkHttp
  7. Once downloaded, compare the hash of the file with the expected hash given by the API
  8. If sucessful, kick off the install process.
  9. Et Voila

Let's dig into the main areas of it..

App Version Number

The first thing we need to do on the Android side is for the App to have the ability to check its own version number.

The official Android page on versioning talks about the use of the versionCode number which can be used to track the app version. This integer is stored in the build.gradle file

and looks like this:


defaultConfig {
versionCode 2
versionName "1.1"
}

From code, we can access the integer using the following code snippet:


try {
PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0);
String version = pInfo.versionName;
}
catch (PackageManager.NameNotFoundException e)
{
e.printStackTrace();
}

Simples!

ASP.NET WebAPI Endpoint

Now that the Android app can retrieve its own version number, we needs to be able to call a WebAPI endpoint that returns the latest app version from the server to compare against. Something like:


[HttpGet]
public int GetCurrentAppVersion()
{
var version = Convert.ToInt32(ConfigurationManager.AppSettings["AndroidAppVersion"]);
return version;
}

This simple function is just reading the app version from the web.config file:


<appSettings>
<add key="AndroidAppVersion" value="3" />
</appSettings>

..however it might be better suited for this field to be held in a database table somewhere so it can be easily updated. As it stands, I need to alter the web.config file every time I update the app!

Add the .APK file to IIS

The next job is to add the latest copy of the android .apk file to IIS. For this you simply copy the .apk file into the wwwroot directory.

Once you have copied the .apk to the root of the website directory, we need to allow IIS to download the .apk. For this you then need to add the following MIME Type to the web.config file:


<staticContent>
<mimeMap fileExtension=".apk" mimeType="application/vnd.android.package-archive">
</staticContent>

Another way to do this would be to manually add the MIME Type using IIS Manager but I think using the web.config is the better option because it makes the solution more easily deployable.

Once you have done this, we can test the endpoint to make sure we can download the .apk file using a browser. The rationale behind this test is that if we can download the .apk file manually, our Android app will also be able to download it.

Downloading the .APK file using OkHttp

The next step is to download this .apk file from the Android app using the OkHttp library. Using Okhttp makes downloading a binary file mega easy. The following code is asynchronous and is very efficient . Once the file has finished downloading the onResponse function is called which saves the .apk file to the cache directory.


private fun downloadAPK() {

toast("Please Wait.... Downloading App");

val client = OkHttpClient()

var apkUrl = "http://192.168.0.18:801/oceanairdrop.apk";

val request = Request.Builder()
.url(apkUrl)
.build()

client.newCall(request).enqueue(object : Callback {

override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}

@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {

val appPath = getExternalCacheDir().getAbsolutePath()
val file = File(appPath)
file.mkdirs()

val downloadedFile = File(file, "appUpdate.apk")

val sink = Okio.buffer(Okio.sink(downloadedFile))
sink.writeAll(response.body()?.source())
sink.close()

m_fileLocation = downloadedFile.toString()

this@ContactInfo.runOnUiThread(java.lang.Runnable {

// UI Code
toast("Successfully Downloaded File");

try {
if ( m_fileLocation != "")
{
val mimeType = "application/vnd.android.package-archive"
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(File(m_fileLocation)), mimeType)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}

// close activity
finish()
}
catch (ex: Exception)
{
Utils.LogEvent("AppUpdate", Utils.LogType.Error, ex.message.toString())
}
})
}
})
}

Once the .apk file has finished downloading, we need to launch it which will install it for the user. The code that runs on the UI thread is responsible for installing the .apk:


val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(File(m_fileLocation)), "application/vnd.android.package-archive")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)

FileUriExposed exception

Now, there is one little problem with the above intent code which kicks off the install process. - The setDataAndType no longer works if you are targeting API 24 and above (Which is Android 7.0 - Nougat). If you are targeting API 24 and greater you will get a FileUriExposed exception. The new approach for this, is to use the new FileProvider class to launch the new app.

However, to cheat and side-step this issue you can call the following function on application start-up which will allow the proceeding code to work. Please see this reference post here for more information.


private fun enableLaunchAPK() {
if (Build.VERSION.SDK_INT >= 24) {
try {
val m = StrictMode::class.java.getMethod("disableDeathOnFileUriExposure")
m.invoke(null)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

Wrapping Up

That's it.... All works. I now have an application that can auto-update. Now I just need to find some time to actually... you know... Update the app!


Comments

Popular posts from this blog

SeriLog & Application Diagnostic Logging

OxyPlot Charting Control

Android, Self-Signed Certificates and OkHttp