Search code examples
androidkotlinbroadcastreceiveruninstallation

Not getting ACTION_PACKAGE_REMOVED broadcast - Android 10/Settings


We want to know when apps are uninstalled from the Android device. We set up broadcast receivers (not in the manifest) concerning the device apps and process them when received. The problem: when uninstalling an app (using Android Settings -> Apps/Notifications -> AppYouWantToUninstall -> tap the trashcan) the broadcast is not received...however this only happens when the app is installed on an Android 10 device.

Of course there are two other main ways to uninstall apps: 1) tap/hold on the app and bring up the "App Info" menu; and 2) go into the Play Store. Those other two methods deliver a broadcast of ACTION_PACKAGE_REMOVED.

All combinations of OS (Android 7, 8, 9 and 10) and the three methods of uninstalling deliver the ACTION_PACKAGE_REMOVED broadcast except for Settings uninstall method when the app is on an Android 10 device. I'd be skeptical, too, so I'll answer a couple of likely probes: "Yes, if we use the Play Store uninstall for an app on Android 10, we do get a broadcast" and "Yes, if we use the Settings uninstall for an app on Android 7, 8, 9 we do get a broadcast"

The definition in the class:

class AppInstallBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent == null || context == null) return
        when (intent.action) {
            Intent.ACTION_PACKAGE_ADDED ->
                enqueueWork(context, getIntent(context, ACTION_PACKAGE_ADDED, intent.data?.schemeSpecificPart))
            Intent.ACTION_PACKAGE_REPLACED ->
                enqueueWork(context, getIntent(context, ACTION_PACKAGE_REPLACED, intent.data?.schemeSpecificPart))
            Intent.ACTION_PACKAGE_CHANGED ->
                enqueueWork(context, getIntent(context, ACTION_PACKAGE_CHANGED, intent.data?.schemeSpecificPart))
            Intent.ACTION_PACKAGE_REMOVED ->
                enqueueWork(context, getIntent(context, ACTION_PACKAGE_REMOVED, intent.data?.schemeSpecificPart))
            Intent.ACTION_PACKAGE_FULLY_REMOVED ->
                enqueueWork(context, getIntent(context, ACTION_PACKAGE_FULLY_REMOVED, intent.data?.schemeSpecificPart))
                }
    }

...and this is where we process received actions

    private suspend fun processAction(action:String, packageName: String, context: Context) {
        when (action) {
            ACTION_PACKAGE_ADDED -> updateAppInfo(context, packageName, false)
            ACTION_PACKAGE_REPLACED -> updateAppInfo(context, packageName, true)
            ACTION_PACKAGE_REMOVED -> deleteAppInfo(context, packageName)
            ACTION_PACKAGE_FULLY_REMOVED -> deleteFullyAppInfo(context, packageName)
        }
    }

This is where we define (in the application, not the manifest) the receivers:

    private val installBroadcastReceiver = AppInstallBroadcastReceiver()
    private val installReceiverFilter = IntentFilter().apply {
        addAction(Intent.ACTION_PACKAGE_ADDED)
        addAction(Intent.ACTION_PACKAGE_REMOVED)
        addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
        addAction(Intent.ACTION_PACKAGE_CHANGED)
        addAction(Intent.ACTION_PACKAGE_REPLACED)
        addDataScheme("package")
    }

...and where we register, filter

    private fun registerAppChanges() {
        Timber.d("Registering the installation receiver ..")
        registerReceiver(installBroadcastReceiver, installReceiverFilter)
        registerReceiver(unlockReceiver, unlockReceiverFilter)
    }

Solution

  • Accepting the reality of receivers largely not working (any longer) in a way that would have been useful to our situation the solution used was to 1) embed some checking code in onResume to see if there is a difference between the device reality and the app's understanding of reality; 2) Use the one receiver type (ACTION_PACKAGE_FULLY_REMOVED) to at least keep up with those (the main concern in our case).

    To effect the new receiver type, you have to register the receiver in the manifest, like this:

            <receiver
                android:name=".receiver.FullyRemovedBroadcastReceiver">
    <!--            <android:exported="true">  -->
                 <intent-filter>
                     <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>
                     <data android:scheme="package"/>
                  </intent-filter>
             </receiver>
    

    The rest of it (creation of the Broadcast Receiver class):

     class FullyRemovedBroadcastReceiver : BroadcastReceiver() {
         override fun onReceive(context: Context?, intent: Intent?) {
             if (intent == null || context == null) return
             when (intent.action) {
                 ACTION_PACKAGE_FULLY_REMOVED ->
                     enqueueWork(context, getIntent(context, ACTION_PACKAGE_FULLY_REMOVED, intent.data?.schemeSpecificPart))
                 }
         }
    
         private fun getIntent(context: Context?, action: String, packageName: String?) =
                 Intent(context, AppChangeJobIntentService::class.java).apply {
                     this.action = action
                     putExtra(EXTRA_PACKAGE_NAME, packageName)
                     putExtra(EXTRA_DATA_REMOVED, true)
                     putExtra(EXTRA_REPLACING, false)
                 }
    
         private fun enqueueWork(context: Context, intent: Intent) {
             JobIntentService.enqueueWork(
                     context,
                     AppChangeJobIntentService::class.java,
                     APP_CHANGE_JOB_ID,
                     intent)
         }
     }