Privileged apps can now launch their activities across profiles.
Introduced a new INTERACT_ACROSS_PROFILES privileged permission which
allows an application to start a managed profile activity from its personal
profile activity.
Added CrossProfileApps#startAnyActivity(ComponentName, UserHandle) which
requires the INTERACT_ACROSS_PROFILES permission and enables an app from
a personal profile to launch an activity within its managed profile app.
Bug: 118186373
Test: atest com.android.server.pm.CrossProfileAppsServiceImplTest
Test: atest cts/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/CrossProfileAppsHostSideTest.java
Change-Id: I28aa05c7e54f60eb6144275d31eaf8813e2f10ad
diff --git a/api/system-current.txt b/api/system-current.txt
index ca06b07..0502ef1 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -89,6 +89,7 @@
field public static final java.lang.String INSTALL_PACKAGE_UPDATES = "android.permission.INSTALL_PACKAGE_UPDATES";
field public static final java.lang.String INSTALL_SELF_UPDATES = "android.permission.INSTALL_SELF_UPDATES";
field public static final java.lang.String INTENT_FILTER_VERIFICATION_AGENT = "android.permission.INTENT_FILTER_VERIFICATION_AGENT";
+ field public static final java.lang.String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
field public static final java.lang.String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
field public static final java.lang.String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
field public static final java.lang.String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
@@ -1128,6 +1129,10 @@
field public int targetSandboxVersion;
}
+ public class CrossProfileApps {
+ method public void startAnyActivity(android.content.ComponentName, android.os.UserHandle);
+ }
+
public final class InstantAppInfo implements android.os.Parcelable {
ctor public InstantAppInfo(android.content.pm.ApplicationInfo, java.lang.String[], java.lang.String[]);
ctor public InstantAppInfo(java.lang.String, java.lang.CharSequence, java.lang.String[], java.lang.String[]);
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index 1c564f3..740fd7f 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -16,6 +16,8 @@
package android.content.pm;
import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
@@ -66,7 +68,30 @@
mContext.getIApplicationThread(),
mContext.getPackageName(),
component,
- targetUser);
+ targetUser.getIdentifier(),
+ true);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts the specified activity of the caller package in the specified profile if the caller
+ * has {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} permission and
+ * both the caller and target user profiles are in the same user group.
+ *
+ * @param component The ComponentName of the activity to launch. It must be exported.
+ * @param targetUser The UserHandle of the profile, must be one of the users returned by
+ * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
+ * be thrown.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES)
+ public void startAnyActivity(@NonNull ComponentName component, @NonNull UserHandle targetUser) {
+ try {
+ mService.startActivityAsUser(mContext.getIApplicationThread(),
+ mContext.getPackageName(), component, targetUser.getIdentifier(), false);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/core/java/android/content/pm/ICrossProfileApps.aidl b/core/java/android/content/pm/ICrossProfileApps.aidl
index bc2f92a..d2d66cb 100644
--- a/core/java/android/content/pm/ICrossProfileApps.aidl
+++ b/core/java/android/content/pm/ICrossProfileApps.aidl
@@ -28,6 +28,6 @@
*/
interface ICrossProfileApps {
void startActivityAsUser(in IApplicationThread caller, in String callingPackage,
- in ComponentName component, in UserHandle user);
+ in ComponentName component, int userId, boolean launchMainActivity);
List<UserHandle> getTargetUserProfiles(in String callingPackage);
}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3018614..2482b73 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2171,6 +2171,13 @@
<permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
android:protectionLevel="signature|installer" />
+ <!-- @SystemApi Allows an application to start an activity within its managed profile from
+ the personal profile.
+ This permission is not available to third party applications.
+ @hide -->
+ <permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi @hide Allows an application to call APIs that allow it to query and manage
users on the device. This permission is not available to
third party applications. -->
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index 65ccecd..7ae2271 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppOpsManager;
@@ -77,18 +78,20 @@
IApplicationThread caller,
String callingPackage,
ComponentName component,
- UserHandle user) throws RemoteException {
+ @UserIdInt int userId,
+ boolean launchMainActivity) throws RemoteException {
Preconditions.checkNotNull(callingPackage);
Preconditions.checkNotNull(component);
- Preconditions.checkNotNull(user);
verifyCallingPackage(callingPackage);
+ final int callerUserId = mInjector.getCallingUserId();
+ final int callingUid = mInjector.getCallingUid();
+
List<UserHandle> allowedTargetUsers = getTargetUserProfilesUnchecked(
- callingPackage, mInjector.getCallingUserId());
- if (!allowedTargetUsers.contains(user)) {
- throw new SecurityException(
- callingPackage + " cannot access unrelated user " + user.getIdentifier());
+ callingPackage, callerUserId);
+ if (!allowedTargetUsers.contains(UserHandle.of(userId))) {
+ throw new SecurityException(callingPackage + " cannot access unrelated user " + userId);
}
// Verify that caller package is starting activity in its own package.
@@ -98,25 +101,43 @@
+ component.getPackageName());
}
- final int callingUid = mInjector.getCallingUid();
-
- // Verify that target activity does handle the intent with ACTION_MAIN and
- // CATEGORY_LAUNCHER as calling startActivityAsUser ignore them if component is present.
- final Intent launchIntent = new Intent(Intent.ACTION_MAIN);
- launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- // Only package name is set here, as opposed to component name, because intent action and
- // category are ignored if component name is present while we are resolving intent.
- launchIntent.setPackage(component.getPackageName());
- verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, user);
+ // Verify that target activity does handle the intent correctly.
+ final Intent launchIntent = new Intent();
+ if (launchMainActivity) {
+ launchIntent.setAction(Intent.ACTION_MAIN);
+ launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ // Only package name is set here, as opposed to component name, because intent action
+ // and category are ignored if component name is present while we are resolving intent.
+ launchIntent.setPackage(component.getPackageName());
+ } else {
+ // If the main activity is not being launched and the users are different, the caller
+ // must have the required permission and the users must be in the same profile group
+ // in order to launch any of its own activities.
+ if (callerUserId != userId) {
+ final int permissionFlag = ActivityManager.checkComponentPermission(
+ android.Manifest.permission.INTERACT_ACROSS_PROFILES, callingUid,
+ -1, true);
+ if (permissionFlag != PackageManager.PERMISSION_GRANTED
+ || !mInjector.getUserManager().isSameProfileGroup(callerUserId, userId)) {
+ throw new SecurityException("Attempt to launch activity without required "
+ + android.Manifest.permission.INTERACT_ACROSS_PROFILES + " permission"
+ + " or target user is not in the same profile group.");
+ }
+ }
+ launchIntent.setComponent(component);
+ }
+ verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, userId);
launchIntent.setPackage(null);
launchIntent.setComponent(component);
mInjector.getActivityTaskManagerInternal().startActivityAsUser(
caller, callingPackage, launchIntent,
- ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(),
- user.getIdentifier());
+ launchMainActivity
+ ? ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle()
+ : null,
+ userId);
}
private List<UserHandle> getTargetUserProfilesUnchecked(
@@ -163,7 +184,7 @@
* activity is exported.
*/
private void verifyActivityCanHandleIntentAndExported(
- Intent launchIntent, ComponentName component, int callingUid, UserHandle user) {
+ Intent launchIntent, ComponentName component, int callingUid, @UserIdInt int userId) {
final long ident = mInjector.clearCallingIdentity();
try {
final List<ResolveInfo> apps =
@@ -171,7 +192,7 @@
launchIntent,
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
callingUid,
- user.getIdentifier());
+ userId);
final int size = apps.size();
for (int i = 0; i < size; ++i) {
final ActivityInfo activityInfo = apps.get(i).activityInfo;
diff --git a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
index c4c2ad9..839b25f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -212,7 +212,29 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PRIMARY_USER)));
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_currentUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -234,7 +256,31 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notInstalled() {
+ mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -254,7 +300,29 @@
mIApplicationThread,
PACKAGE_TWO,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_fakeCaller() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_TWO,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -276,7 +344,31 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_notExported() {
+ mActivityInfo.exported = false;
+
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -296,7 +388,29 @@
mIApplicationThread,
PACKAGE_ONE,
new ComponentName(PACKAGE_TWO, "test"),
- UserHandle.of(PROFILE_OF_PRIMARY_USER)));
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_profile_anotherPackage() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ new ComponentName(PACKAGE_TWO, "test"),
+ UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -316,7 +430,29 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(SECONDARY_USER)));
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ true));
+
+ verify(mActivityTaskManagerInternal, never())
+ .startActivityAsUser(
+ nullable(IApplicationThread.class),
+ anyString(),
+ any(Intent.class),
+ nullable(Bundle.class),
+ anyInt());
+ }
+
+ @Test
+ public void startAnyActivityAsUser_secondaryUser() {
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mCrossProfileAppsServiceImpl.startActivityAsUser(
+ mIApplicationThread,
+ PACKAGE_ONE,
+ ACTIVITY_COMPONENT,
+ UserHandle.of(SECONDARY_USER).getIdentifier(),
+ false));
verify(mActivityTaskManagerInternal, never())
.startActivityAsUser(
@@ -335,7 +471,8 @@
mIApplicationThread,
PACKAGE_ONE,
ACTIVITY_COMPONENT,
- UserHandle.of(PRIMARY_USER));
+ UserHandle.of(PRIMARY_USER).getIdentifier(),
+ true);
verify(mActivityTaskManagerInternal)
.startActivityAsUser(