DEV Community

Cover image for On building a digital assistant for the rest of us (part 3)
Thomas Künneth
Thomas Künneth

Posted on

On building a digital assistant for the rest of us (part 3)

Welcome to the third part of On building a digital assistant for the rest of us. Last time, we refined the camerax usage, and learned how to draw closed freeform shapes on screen. They were used to tell Gemini on which part of the picture to focus. Today we look at what it takes to become a digital assistant on Android.

About roles

A lot of what an Android app can do is based on permissions. But did you know that there is also the concept of roles? To quote the documentation,

A role is a unique name within the system associated with certain privileges.

For example, ROLE_BROWSER identifies an app as, you guessed it, the default browser. To qualify for this role, an app must handle the intent to browse the Internet. Documentation about what requirements are tied to a certain role can be found in the AndroidX Role library. Now, here comes something slightly odd. While we would expect a Jetpack library to allow access to some functionality, at the time of writing, androidx.core.role contains only RoleManagerCompat which only defines constants, accompanied by aforementioned documentation. Requesting roles is done using the RoleManager system service, which is available since API level 29. Now, the role constants are also defined in RoleManager, so for now the only purpose of AndroidX Role library is to provide documentation about what requirements are tied to some role.

To become a digital assistant, viewfAInder must hold ROLE_ASSISTANT. To qualify for the role, we must add an intent filter to the manifest:

<activity
  android:name=".MainActivity"
  ...
  <intent-filter>
    <action android:name="android.intent.action.ASSIST" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>
Enter fullscreen mode Exit fullscreen mode

The list of available roles may change over time, so apps should not make assumptions about the availability of a role but instead query if a role is available. So, a role can be held, not held, or unavailable. Let's define an enum class for this:

enum class RoleStatus {
  NOT_HELD, HELD, UNAVAILABLE
}
Enter fullscreen mode Exit fullscreen mode

And this a convenience function that returns a status based on a Boolean:

fun getRoleStatus(held: Boolean): RoleStatus =
  if (held) RoleStatus.HELD else RoleStatus.NOT_HELD
Enter fullscreen mode Exit fullscreen mode

Now let's focus on how to request a role.

private lateinit var manager: RoleManager

private val roleFlow: MutableStateFlow<RoleStatus> =
  MutableStateFlow(RoleStatus.NOT_HELD)
private val roleLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
  activity -> roleFlow.update {
      getRoleStatus(activity.resultCode == RESULT_OK)
    }
}
Enter fullscreen mode Exit fullscreen mode

The following code snippet shows you how to check if the role is available and, if it is, if it is currently held by the app:

manager = getSystemService(RoleManager::class.java)
manager.run {
  if (isRoleAvailable(RoleManagerCompat.ROLE_ASSISTANT)) {
    roleFlow.update {
      getRoleStatus(isRoleHeld(RoleManagerCompat.ROLE_ASSISTANT))
    }
  } else roleFlow.update { RoleStatus.UNAVAILABLE }
}
Enter fullscreen mode Exit fullscreen mode

If the role is not currently held but is generally available, here's how to request it:

val requestRole = {
  val intent = manager.createRequestRoleIntent(
    RoleManagerCompat.ROLE_ASSISTANT)
    roleLauncher.launch(intent)
}
Enter fullscreen mode Exit fullscreen mode

This could be nicely passed to composables and invoked like this:

@Composable
fun MainScreen(
  ...
  roleStatus: RoleStatus,
  requestRole: () -> Unit
) {
  val scope = rememberCoroutineScope()
  Box(contentAlignment = Alignment.Center) {
    ...
    when (roleStatus) {
      RoleStatus.HELD -> { ... }

      RoleStatus.NOT_HELD -> {
        Column( ... ) {
          Text( ... )
          Button(
            onClick = { scope.launch { requestRole() } },
          ) {
            Text( ... )
          }
        }
      }

      RoleStatus.UNAVAILABLE -> {
        Text( ... )
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

There is one big problem, though. Some roles cannot be acquired using requestRole. While, for example, requesting ROLE_BROWSER will show a nice dialog, ROLE_ASSISTANT won't. The following screenshot shows my sample app RoleDemo. Its source code is available on GitHub.

An app requesting the Browser role

At this point, we might be tempted to ask why this is not mentioned in the documentation, or, why this has been removed. Since those questions will likely remain unanswered anyway, let's focus on making viewfAInder the default digital assistant by using some other method. There must be some, as there is this settings page:

Selecting the default digital assistant app in settings

As you can see, viewfAInder already appears there, because the app meets the preconditions mentioned earlier. While we can't navigate to this settings page directly, we can reach its parent page:

Digital assistant app page in settings

To do so, we only need to slightly update our launcher code:

private val roleLauncher =
  registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
  ) { _ -> roleFlow.update { getRoleStatus(
      manager.isRoleHeld(RoleManagerCompat.ROLE_ASSISTANT))
    }
  }
Enter fullscreen mode Exit fullscreen mode

Once we return from the launched activity, we just check if we now hold ROLE_ASSISTANT and update our flow accordingly. This is the updated requestRole lambda:

val requestRole = {
  val intent = Intent(Settings.ACTION_VOICE_INPUT_SETTINGS)
  roleLauncher.launch(intent)
}
Enter fullscreen mode Exit fullscreen mode

Wrap-up

So, we just fire a predefined action. That wasn't too difficult, fortunately, right? And this concludes the third part of this series. In the next and final part, we will be doing more Gemini magic, making our digital assistant really useful. Want to learn more? Stay tuned.

Top comments (0)