Permission not granted - on failing to create cross-profile app pairs
Thomas Künneth
Posted on February 2, 2024
Be nice 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. 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 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. 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.
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. 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
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) 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:
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:
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
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.
val userManager = getSystemService(UserManager::class.java)
userManager.userProfiles.forEach {
println("===> $it")
}
To understand the output, let's look at some code snippets from UserHandle
:
So, user 0 represents the device owner.
User 10 represents a secondary user. I got that one on my testing device after installing Test DPC, 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.
Posted on February 2, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.