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.

If you want to bring your Android skills to the next level, I highly recommend checking out some of the Udemy courses. Disclaimer: I'm participating in the Udemy Affiliate Program and I might get a small commission if you purchase something via the provided link. However, your price won’t be affected and I do believe the courses can help your career/business.

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.

Additionally, there seems to be an issue receiving MY_PACKAGE_REPLACED on some older devices. If you experience similar problem, you might need declare 2 receivers with both - MY_PACKAGE_REPLACED and PACKAGE_REPLACED in your AndroidManifest.xml, and then use android:enabled flag to let the system instantiate the right receiver according to device's API level. Check out this stackoverflow post on how it can be done.

Useful Links

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

An alternative to complete APK updates is to make a web app that runs inside a WebView. I wrote more about this strategy in my other post related to device administration. I've also created an app that allows you to install a web app with your predefined URL that should be loaded. The web app locks to the screen, so it can be used as a kiosk app
https://play.google.com/store/apps/details?id=eu.sisik.kioskconfigurator

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, are you sure that you've removed "testOnly" flag when you were building the updated APK? I just tested it on a real device with Android 7 and it works as expected. There is also a difference between debug and release builds. For release builds you have to have "testOnly" in initial app version, so that you are able to activate device owner via ADB. In debug build, you should be able to work without "testOnly" flag completely. You can write directly to my email address. Might be faster
Written on Thu, 21 Mar 2019 15:19:20 by Roman
Roman, thank you for pointing. It seems I've found the problem, Everything works fine on emulator, but BroadcastReceiver doesn't work after installation new version on my target device (Lenovo Tab, Android Nougat 7.1.1). What it could be? Lenovo shell or Google protection or Nougat feature? Did you know something about there restrictions?
Written on Thu, 21 Mar 2019 11:42:38 by Gorets
@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