March 29, 2019

Android Q to the Res-Q!

An intro to RoleManager! And a title with terrible pun for an article that has nothing to do with Resources!

Android Q to the Res-Q!

An intro to RoleManager! And a terrible pun that has nothing to do with Resources!

Android Q Preview was released recently, inviting developers to start testing and supporting new APIs! One of these APIs is RoleManager, and as we can gather from the Q-preview documentation:

Android Q introduces a standard facility, roles, that allows the OS to grant apps elevated access to system functions based on well-understood use cases. Semantically, each role represents a common use case, such as playing music, viewing photos in a gallery, or sending SMS messages. If an app loses its role, this elevated access is also revoked. (Link)

In plain english, a role is basically a type of app that executes a specific function (e.g. web browser). The roles exposed in the Q preview are Browser, Dialer, Emergency, Gallery, Home, Music, and SMS. As a developer, you can now request to hold one of these roles for your app, if it falls into one of those buckets.

Why is this API helpful?

This now lets us proactively query the system to see if our app is the default. If not, it lets us request permission, from the user, to hold that role. We now have more control over checking and responding to these sorts of events. No more silly hacks, no more trying to resolve your package name with the PackageManager, no more searching for options in settings! (🎉🎉🎉)

What’s the catch?

For the RoleManager to resolve a request properly, your Manifest must specify categories, permissions, or additional intent filters for the role to properly be held. The request will be automatically canceled if you fail to do so. For example, if we have an Internet Browser, we must define the CATEGORY_APP_BROWSER inside the IntentFilter for your Launcher activity. If we do not, and we request a role via RoleManager, that request will be denied without ever prompting the user.

How do we use it? Well, It’s quite simple!

Step 0. We add any necessary categories, IntentFilters, or Permissions in your Manifest!

Step 1. Answer this question: Do we have an app that fits into a pre-defined role? If yes, let’s go to Step 2. If no, let’s enjoy some ice cream!

Step 2. Is this Role available on the current system? We can check this via the `RoleManager` class.

val roleManager = getSystemService(RoleManager::class.java)
val isRoleAvailable = roleManager.isRoleAvailable(RoleManager.ROLE_BROWSER)

Step 3. If this Role is available, is our app holding that role currently? We can check this via the RoleManager too!

val roleManager = getSystemService(RoleManager::class.java)
val isRoleHeld = roleManager.isRoleHeld(RoleManager.ROLE_BROWSER)

Step 4. If we are not holding this role, is now an appropriate time to ask the user to request access for this role? If yes, we use RoleManager for its magical powers of Intent creation 🎩🐰🎉.

val roleManager = getSystemService(RoleManager::class.java)
val roleRequestIntent = roleManager.createRequestRoleIntent(
        RoleManager.ROLE_BROWSER)
startActivityForResult(roleRequestIntent, ROLE_REQUEST_CODE)

Step 5. If the user grants permission for us to hold that role, onActivityResult will be called with RESULT_OK. RESULT_CANCELED will return if the user denied our request, or if conditions for holding that role have not been met (e.g. missing category in your manifest).

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == ROLE_REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            // Oh yes :)
        } else {
            // Oh no :(
        }
    }
}

Step 6. Done. From there is just a matter of refreshing our UI to reflect the current state of the system!

Please take a look at a simple example for requesting ROLE_MUSIC in this Github repo!


Secret secrets are no fun, until you tell every one!

Or until the official documentation is updated 😁! What am I talking about? There seems to be some undocumented roles that are available. One in particular is android.app.role.ASSISTANT.

Update 04/08/19: Looks like ROLE_ASSISTANT has been added to the official docs! Thanks to Andrew Kelly for the heads up!

When testing Window on Android Q, I noticed a strange error message:

2019–03–00 00:00:00.000 12345–67890/? E/RoleControllerServiceImpl: Package does not qualify for the role, package: com.dziemia.w.window, role: android.app.role.ASSISTANT

This log statement appears when attempting to select the current version of Window (blue icon) on Android-Q. This led to an adventure through the docs, where surprisingly no Assistant role could be found! So, after some sleuthing🕵️‍♀️, I found that all I needed to do was add an additional IntentFilter with <action/> of ASSIST to my launcher activity in my manifest!

<intent-filter>
    <action android:name="android.intent.action.ASSIST" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

A request for permission through the RoleManager will automatically return RESULT_CANCELED if we fail to include the IntentFilter. If we do everything correctly though, we are greeted with the following:

The great thing about checking roles within RoleManager is that each of them are defined as a String, so my check method is simply:

const val ROLE_ASSISTANT =  "android.app.role.ASSISTANT"

@TargetApi(Build.VERSION_CODES.Q)
private fun isCurrentAssistAppQ(): Boolean {
    return with(getSystemService(RoleManager::class.java)){
        isRoleAvailable(ROLE_ASSISTANT) && isRoleHeld(ROLE_ASSISTANT)
    }
}

Which makes me feel much more secure over what I had to do previously:

private fun isCurrentAssistAppBase(): Boolean = Settings.Secure
        .getString(contentResolver, "assistant")?.let {
            ComponentName.unflattenFromString(it)?.packageName == packageName
        } ?: false

fun Context.openAssist() {
    val openIntent = Intent(Intent.ACTION_MAIN)
    openIntent.component = ComponentName("com.android.settings", "com.android.settings.Settings\$ManageAssistActivity")
    openIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    startActivity(openIntent)
}

The main benefit being that I no longer need send user to a different app, potentially having them drop off. I also needed to assume the system includes ManageAssistActivity , which Chrome laptops do not have. Lastly, I don’t need to use some obscure API and pass in a magic variable (“assistant”) to validate the user’s selection within the system settings screen.

🍋🍋 Easy peasy lemon squeezy! 🍋🍋

Hope you enjoyed this article! Spam me on Twitter @wdziemia, and as always, happy coding!