androidkotlinandroid-notifications

Getting a list of other apps that can send notifications in Kotlin


Back story

I have, thanks to the help of a certain blogpost, some code to retrieve a list of user-installed (i.e. non-system) apps on the user's device:

class NotificationPermissionHelper(private val context: Context) { 

    fun getAllApps(): List<ApplicationInfo> {
        val packageManager = context.packageManager
        val mainIntent = Intent(Intent.ACTION_MAIN, null)
        mainIntent.addCategory(Intent.CATEGORY_LAUNCHER)

        val resolveInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            packageManager.queryIntentActivities(
                mainIntent,
                PackageManager.ResolveInfoFlags.of(0L)
            )
        } else {
            packageManager.queryIntentActivities(mainIntent, 0)
        }

        return resolveInfoList.mapNotNull { resolveInfo ->
            try {
                packageManager.getApplicationInfo(resolveInfo.activityInfo.packageName, 0)
            } catch (e: PackageManager.NameNotFoundException) {
                // Handle cases where the application info can't be found.
                // This could happen if the app was uninstalled.
                null
            }
        }
    }
}

However, my attempt to filter that list against the notification permission, fails miserably....

How do you filter against the notification permission, for each app

Like this, of course:

    fun getAppsWithNotificationPermission(): List<ApplicationInfo> {
        val allApps = getAllApps()
        return allApps.filter { hasNotificationPermission(it.packageName) }
    }

    private fun hasNotificationPermission(packageName: String): Boolean {
        // Check if the app is opted into notifications

            val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
            val mode = getMode(appOpsManager, packageName)
            return mode == AppOpsManager.MODE_ALLOWED

    }

    private fun getMode(appOpsManager: AppOpsManager, packageName: String): Int {
        return onGetOp()(appOpsManager,
            "android:post_notification",
            android.os.Process.myUid(),
            packageName,
            )
    }

    private fun onGetOp(): (AppOpsManager, String, Int, String) -> Int {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return AppOpsManager::unsafeCheckOpNoThrow
        }
        return AppOpsManager::checkOpNoThrow
    }

We have an PackageListDataSource that uses an event bus to start the PackageQueryFetcherActivity which:

What happens when you run your code?

Expected

We get a list of ApplicationInfo containing at least the Phone and Messages apps

Actual

We get an empty list, as for every app....

when I place debug breakpoint on the return statement of hasNotificationPermission,

mode == 1

but

AppOpsManager.MODE_ALLOWED == 0

Speaking of permissions, we don't get the authentication screen for querying all packages, as this method as defined on our BaseFetcherActivity returns true for permission = android.Manifest.permission.QUERY_ALL_PACKAGES:

protected fun hasDataAccessPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            this,
            this.permission,
        ) == PackageManager.PERMISSION_GRANTED
    }

So, it just performs the fetch of all apps (as discussed eariler), and the check for whether the app package has the notification permission seems to always return AppOpsManager.MODE_IGNORED


Solution

  • return onGetOp()(appOpsManager,
                "android:post_notification",
                android.os.Process.myUid(),
                packageName,
                )
    

    According to the documentation, your second parameter needs to be "The uid of the application attempting to perform the operation", just as the third parameter is "The name of the application attempting to perform the operation" (emphasis added). packageName presumably is correct for the third parameter value, but you are passing your own app's uid, not the uid of packageName.