Permission not granted - on failing to create cross-profile app pairs
Tags: android, security, development, kotlin
Published: 2024-02-02 16:08:37

[Be nice](https://play.google.com/store/apps/details?id=de.thomaskuenneth.benice) is a simple and friendly app. Its idea is to provide a better user experience for apps that are not really looking good on tablets and foldables because they force the device into portrait mode. In a way, I made it for myself, as I was experiencing said issues with many apps on my Surface Duo. Yes, I am still [using my Surface Duo in 2024](https://dev.to/tkuenneth/what-to-do-with-a-surface-duo-in-2024-4peb). But I figured users of tablets might like *Be nice*, too, because that portrait-lock-in is a pain on large screens as well. It turns out, it hit a nerve of quite a few Pixel Fold owners. I learned that currently the Pixel Fold does not provide app pairs. What *Be nice* does is essentially creating app pairs, so generalising the functionality was not a big deal. That brought my app [center-stage](https://youtu.be/7Z4uP8CktrM?si=Rl0NQYZDtUL7RVHK) on the YouTube channel of Shane Craig. What followed was quite some praise and enthusiasm of users - and a request:

*Could you please enable app pairs across profiles?*

Many of us are using our devices for work and personal stuff. Android supports this through so-called [Work Profiles](https://www.android.com/enterprise/work-profile/). How this machinery works under the hood, is worth a whole series of articles, so I will remain a little vague and general regarding technical terms. To learn more, kindly refer to the [official documentation](https://developer.android.com/work/managed-profiles).

### Launching apps in split-screen mode

Before we turn to my failed attempts of running an app in another profile (and how we learn about profiles anyway), let's look at how *Be nice* launches app pairs. The app is open source, so the complete code is available on [GitHub](https://github.com/tkuenneth/BeNice). I rely solely on public APIs and do not hook into the system. This makes launching an app pair charmingly unorthodox.

- A shortcut launches `BeNiceActivity`
- The intent contains both package name and class name of the two apps (well, activities) to be launched
- First, the first activity is started
- Then we wait a little while
- Then we launch the second activity

```kotlin
class BeNiceActivity : ComponentActivity() {

  override fun onResume() {
    super.onResume()
    val launchAdjacent = shouldLaunchAdjacent(
      prefs = PreferenceManager.getDefaultSharedPreferences(this),
      windowSizeClass = computeWindowSizeClass()
    )
    if (launchAdjacent) {
      Intent(this, AppChooserActivity::class.java).run {
        addFlags(FLAG_ACTIVITY_NEW_TASK)
        startActivity(this)
      }
    }
    Handler(Looper.getMainLooper()).postDelayed({
      launchApp(
        intent = intent,
        launchAdjacent = launchAdjacent
      )
      finish()
    }, if (launchAdjacent) 500L else 0L)
  }

  private fun launchApp(intent: Intent, launchAdjacent: Boolean) {
    lifecycleScope.launch {
      if (ACTION_LAUNCH_APP == intent.action) {
        intent.getStringExtra(PACKAGE_NAME)?.let { packageName ->
          intent.getStringExtra(CLASS_NAME)?.let { className ->
            launchApp(
              packageName = packageName,
              className = className,
              launchAdjacent = launchAdjacent
            )
          }
        }
      }
    }
  }
}

fun Context.launchApp(
  packageName: String,
  className: String,
  launchAdjacent: Boolean
) {
  Intent().run {
    component = ComponentName(
      packageName,
      className
    )
    addFlags(FLAG_ACTIVITY_NEW_TASK)
    if (launchAdjacent) {
      addFlags(
        FLAG_ACTIVITY_LAUNCH_ADJACENT or
            FLAG_ACTIVITY_TASK_ON_HOME
      )
    }
    startActivity(this)
  }
}

fun Context.createBeNiceLaunchIntent(appInfo: AppInfo) =
  Intent(this, BeNiceActivity::class.java).also { intent ->
    intent.action = ACTION_LAUNCH_APP
    intent.addFlags(
      FLAG_ACTIVITY_NEW_TASK or
          FLAG_ACTIVITY_CLEAR_TASK
    )
    intent.putExtra(PACKAGE_NAME, appInfo.packageName)
    intent.putExtra(CLASS_NAME, appInfo.className)
  }
```

That's pretty easy to digest, right? In a nutshell, *Be nice* calls `startActivity()` and configures the intent flags based on whether the second app should be launched adjacent (in split-screen mode) or not. If, for example, a foldable is closed, the user may want to use just one app. That's why the code computes window size classes.

### About profiles

If an app is installed in another *profile*, we need to know that it's there, and how we can launch it. But how do we know about profiles?

`CrossProfileApps` is (quoted from [the docs](https://developer.android.com/reference/android/content/pm/CrossProfileApps)) a class

> for handling cross profile operations. Apps can use this class to interact with its instance in any profile that is in `getTargetUserProfiles()`. For example, app can use this class to start its main activity in managed profile.

Well, in a way this sounds cool, but not entirely what we need. We don't want to start *our* main activity but *another* one, to be more precisely, an activity that belongs to another app. Browsing through the available methods seems to reveal just the one we need:

```kotlin
public void startActivity (Intent intent, 
        UserHandle targetUser, 
        Activity callingActivity)
```

You can guess that I was pretty enthusiastic until I read that *Be nice* must hold either the `INTERACT_ACROSS_PROFILES` or `INTERACT_ACROSS_USERS` permission. Which it (to my knowledge) cannot do as a third party app that is distributed through Google Play, due to the protection levels of these permissions.

So, what to do? `startMainActivity()` doesn't require those restrictive permissions. It can receive a `ComponentName` which should allow us to specify the activity of other apps, if we know both package name and class name. Unfortunately, the documentation tells us that the method starts

> the specified main activity of the caller package in the specified profile.

Well, the caller package would be *Be nice*, but that's not what we want. The source code reveals why it won't work:

![Screenshot of the source code of startMainActivity()](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bgos4tugf7jk4xg6szm.png)

I tried it anyway, and expectedly got this:

```
java.lang.SecurityException: de.thomaskuenneth.benice cannot access unrelated user 10
```

*User 10*, by the way, refers to the Work Profile. How I learned about that is pretty interesting, so let me share this. `CrossProfileApps` offers a method called `getTargetUserProfiles()`, which returns

> a list of user profiles that that the caller can use when calling other APIs in this class

![Showing the result of a call to getTargetUserProfiles()](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekik8uh05ougof84k1dy.png)

As you can see, the list is empty. This implies that *Be nice* can't interact with other profiles. I should have known.

There are two query functions, `canInteractAcrossProfiles()` and `canRequestInteractAcrossProfiles()`. The latter one indicates that we might be able to request an interaction (using `createRequestInteractAcrossProfilesIntent()`). Needless to say that both return `false`. But that doesn't explain how I learned about user 10.

There's another manager in town, called `UserManager`. It offers a function called `getUserProfiles()`, which returns

> a list of UserHandles for profiles associated with the context user, including the user itself.

```kotlin
val userManager = getSystemService(UserManager::class.java)
userManager.userProfiles.forEach {
  println("===> $it")
}
```

![LogCat showing two UserHandles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mqsy4299qdxuj4tz93r6.png)

To understand the output, let's look at some code snippets from `UserHandle`:

![Snippet of the UserHandle source code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o2wclxwmr7uxgc6hwkff.png)

So, user 0 represents the device owner.


![Snippet of the UserHandle source code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcjm8u0jdmh335wkechm.png)

User 10 represents a secondary user. I got that one on my testing device after installing [Test DPC](https://play.google.com/store/apps/details?id=com.afwsamples.testdpc), a unique tool that helps you understanding Work Profiles.

### Conclusion

Please recall that my goal was enhance the app pair feature of *Be nice* by allowing the user to launch apps in other profiles. I did not succeed. From my understanding, ordinary apps cannot acquire the permissions needed to achieve this. If I missed something, please do not hesitate to share your thoughts in the comments.