Not getting ACTION_PACKAGE_REMOVED broadcast - Android 10/Settings

Viewed 1606

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)
    }
2 Answers

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)
     }
 }


To run a Broadcast receiver you have to know how it works. Previously when you register a broadcast receiver, it gets fired when any registered action happens even app is not in the background. Then android team make some changes to optimize battery. If your app target 26 or above, you have to declare it dynamically instead of manifest. Here is the text

Note: If your app targets API level 26 or higher, you cannot use the manifest to declare a receiver for implicit broadcasts (broadcasts that do not target your app specifically), except for a few implicit broadcasts that are exempted from that restriction. In most cases, you can use scheduled jobs instead.

So what's the option? You have to register broadcast dynamically using context or applicationContext. So app receives broadcast event as your context is alive. Here is the text from Google:

Context-registered receivers receive broadcasts as long as their registering context is valid. For an example, if you register within an Activity context, you receive broadcasts as long as the activity is not destroyed. If you register with the Application context, you receive broadcasts as long as the app is running.

So how do you listen Broadcast event even your app is not running?

As Broadcast receiver unreliable, I cannot imagine any feasible solution but you can think of running a background service all the time. This will be overkill as well. Check this answer Making BroadcastReceiver work in the background in API 26 or above Also this one from commonsware Android O and the Implicit Broadcast Ban

Another solution I can think of, If API level is lower than 26 it should work as it is but for api 26 and above, in your app onResume you will check each time available apps and your current apps list. Then remove the extra app.

Related