The usual way of installing Android apps is through an app store. On my regular phone I've only used the Google Play Store. The Play Store app has one interesting feature - it can update apps without requiring your confirmation.

If your device is only used for one specific purpose (e.g., as a kiosk) within a limited group of people, it might not make sense to publish your app to an app store. Even in this situation, the device will still need maintenance and software updates. Ideally you should be able to perform these updates remotely without even touching the device for confirmation to install your update.

Android already offers this kind of functionality for device owner apps. If you're not sure how to create and enable a device owner app, check out my previous post.

You can find the complete Android Studio project related to this post on github.

Checking for New Version and Downloading Update

To update your app, you first need an updated APK file stored locally on the Android device that you want to update. This APK file should be signed with the same signing key as the original application. Additionally, the version code of the updated APK should not be lower than the version code of the original app (or version code of the last update).

There are many ways how to get the updated APK to your machine. It depends on your specific situation and on which technologies you decided to use. Therefore I won't show any specific code, but I can still suggest some strategies how to accomplish this.

The first thing you can do is to decide how often you want to check for updates and then set up a service in your Android app that performs this task in the background. For this you can check out how to do Job-Scheduling in the official docs.

On the server side you can set up a web service that can at least (1) give information about current APK version on the server, and (2) serve a download of the updated APK. How this is implemented on the server depends on technologies you use, but the service should at least be able to serve the timestamp or versionCode of the updated APK.

Additionally, if your APK file size too large, you might consider to only supply the diff between versions from your sever, and reconstruct the final APK for the update on the device, similarly to how Android Play Store app is doing this.

The update service in your Android app can compare the versionCode or timestamp with the currently installed APK. Getting the version code from AndroidManifest.xml inside of you updated APK file might require some reverse engineering of the binary xml format used on Android, but it should still be fairly trivial. You can use the lastUpdateTime field of the PackageInfo class to compare the timestamps (for more information about getting app installation info checkout my previous post).

Once you've determined that there is a new version on the server, you can then use the DownloadManager class to download the updated APK file.

Updating the App

Starting from Android 6 it became possible to install, uninstall, and update apps silently without any user interaction. You don't need to be root, but your app still needs to be device owner.

Most of the relevant methods for performing the update are located inside of the PackageInstaller and PackageInstaller.Session class.

The install() method shown in the following code takes a context that can provide a PackageManager instance, the package name, and the path to the actual APK file that you want to install. For a successful update, you need to make sure that the apkPath is readable (and your app has to be device owner, of course).

fun install(context: Context, packageName: String, apkPath: String) {

    // PackageManager provides an instance of PackageInstaller
    val packageInstaller = context.packageManager.packageInstaller

    // Prepare params for installing one APK file with MODE_FULL_INSTALL
    // We could use MODE_INHERIT_EXISTING to install multiple split APKs
    val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
    params.setAppPackageName(packageName)

    // Get a PackageInstaller.Session for performing the actual update
    val sessionId = packageInstaller.createSession(params)
    val session = packageInstaller.openSession(sessionId)

    // Copy APK file bytes into OutputStream provided by install Session
    val out = session.openWrite(packageName, 0, -1)
    val fis = File(apkPath).inputStream()
    fis.copyTo(out)
    session.fsync(out)
    out.close()

    // The app gets killed after installation session commit
    session.commit(PendingIntent.getBroadcast(context, sessionId,
            Intent("android.intent.action.MAIN"), 0).intentSender)
}

Restarting App After Update

If the app that you are updating is currently running, it gets killed once you call Session.commit(). To ensure that your app is restarted after update, you need to set up a BroadcastReceiver that listens for MY_PACKAGE_REPLACED intent.

For the BroadcastReceiver create the following class

class UpdateReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {

        // Restart your app here
        val i = Intent(context, MainActivity::class.java)
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        context.startActivity(i)        
    }
}

Additionally, you need to update your AndroidManifest.xml file

    </application>
        ...

        <receiver
            android:name="eu.sisik.devowner.UpdateReceiver" >
            <intent-filter>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
            </intent-filter>
        </receiver>

    </application>

It is important to use an explicit intent that will only be sent to our application. Before I've used PACKAGE_REPLACED, but it stopped working on Android Oreo. The reason was that Android Oreo (API level 26) introduced some implicit broadcast limitations for statically registered receivers. In our case the app will be killed after update, so we need a static receiver here.

Useful Links

If you're not sure how to set up a device owner app, checkout my previous post.

Additionally, if you're doing some Android device maintenance on-the-go, you might checkout my Bugjaeger app, which offers some ADB functionality directly from your phone.

Next Post Previous Post

Add a comment

Comments

@Gorets, so you are calling install(), but the APK with new version is not installed and there is no exception thrown and no error in the logs. Did I understand this correctly? The only things that comes to my mind is that you need to make sure that the APK for updates was not build with 'testOnly' flag in AndroidManifest. First create a release build that has this flag enabled, so that you are able to activate device owner via ADB(required on Android 7 ). Then build all subsequent APKs that you use for actual update without `testOnly`. Hope that helps.
Written on Wed, 20 Mar 2019 19:46:19 by Roman
Yesterday your project (both versions) worked perfect for me, but now it's broken. I'm confused, I tried different devices and emulators, different version of code (with no modification) and there is not logs and actions after install() method was called, could you test it too? There are no good articles on the internet about own mode, so you are my last hope to solve this issue. Thanks in advance
Written on Wed, 20 Mar 2019 18:33:35 by Gorets
@Gorets, I assume you tested it on Android Oreo+. This seems to be related to implicit broadcasts limitations introduced in Oreo (https://developer.android.com/about/versions/oreo/background#broadcasts). I replaced ACTION_PACKAGE_REPLACED with ACTION_MY_PACKAGE_REPLACED. Now it should work. Try to pull new changes from Github repo
Written on Tue, 19 Mar 2019 09:12:50 by Roman
I got an error when the new version was installed and main process killed Did you get that issue? Log below
Written on Tue, 19 Mar 2019 00:30:37 by Gorets
W/BroadcastQueue: Unable to launch app my,app/10142 for broadcast Intent { act=android.intent.action.PACKAGE_REPLACED dat=my,appt flg=0x4000010 (has extras) }: process is not granted
Written on Tue, 19 Mar 2019 00:29:20 by Gorets
Are you getting some error message? What Android version are you using? For Android 7 and higher, you need to set the "testOnly" flag in AndroidManifest.xml to activate device owner with ADB. But the APK file that will be used as update should NOT have testOnly flag set. I never tried this on an emulator, so I'm not sure if that also could not be an issue...
Written on Thu, 26 Jul 2018 16:15:06 by Roman
App is not getting updated. I m trying is emulator. Can you please help me out.
Written on Thu, 26 Jul 2018 15:05:52 by Namita