Support setting the _PROFILES app-op
Add public broadcast
CrossProfileApps#ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED =
"android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED" and
hidden API CrossProfile#setInteractAcrossProfilesAppOp.
This new hidden API should be used rather than setting the app-op
directly. It ensures that the app-op is set for each user in the profile
group and that the new broadcast is sent when an app's ability to
interact across profiles has changed.
Unit tests are added alongside these changes. CTSVerifier changes should
be subsequently added to enforce the behaviour of the new public
broadcast.
Test: atest frameworks/base/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java --verbose
BUG: 136249261
BUG: 147490565
Change-Id: Iad190a4b972da40bae7ffcf215b8962e8225f4af
diff --git a/api/current.txt b/api/current.txt
index b36f8bf..9a364bb 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -11389,6 +11389,7 @@
method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle);
method @NonNull public java.util.List<android.os.UserHandle> getTargetUserProfiles();
method public void startMainActivity(@NonNull android.content.ComponentName, @NonNull android.os.UserHandle);
+ field public static final String ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED = "android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED";
}
public final class FeatureGroupInfo implements android.os.Parcelable {
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index 9d57514..5aa9c9b 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.app.AppOpsManager.Mode;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -42,6 +43,18 @@
* use this class to start its main activity in managed profile.
*/
public class CrossProfileApps {
+
+ /**
+ * Broadcast signalling that the receiving app's ability to interact across profiles has
+ * changed, as defined by the return value of {@link #canInteractAcrossProfiles()}.
+ *
+ * <p>Apps that have set the {@code android:crossProfile} manifest attribute to {@code true}
+ * can receive this broadcast in manifest broadcast receivers. Otherwise, it can only be
+ * received by dynamically-registered broadcast receivers.
+ */
+ public static final String ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED =
+ "android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED";
+
private final Context mContext;
private final ICrossProfileApps mService;
private final UserManager mUserManager;
@@ -254,6 +267,38 @@
return settingsIntent;
}
+ /**
+ * Sets the app-op for {@link android.Manifest.permission#INTERACT_ACROSS_PROFILES} that is
+ * configurable by users in Settings. This configures it for the profile group of the calling
+ * package.
+ *
+ * <p>Before calling, check {@link #canRequestInteractAcrossProfiles()} and do not call if it is
+ * {@code false}. If presenting a user interface, do not allow the user to configure the app-op
+ * in that case.
+ *
+ * <p>The underlying app-op {@link android.app.AppOpsManager#OP_INTERACT_ACROSS_PROFILES} should
+ * never be set directly. This method ensures that the app-op is kept in sync for the app across
+ * each user in the profile group and that those apps are sent a broadcast when their ability to
+ * interact across profiles changes.
+ *
+ * <p>This method should be used whenever an app's ability to interact across profiles changes,
+ * as defined by the return value of {@link #canInteractAcrossProfiles()}. This includes user
+ * consent changes in Settings or during provisioning, plus changes to the admin or OEM consent
+ * whitelists that make the current app-op value invalid.
+ *
+ * @hide
+ */
+ @RequiresPermission(
+ allOf={android.Manifest.permission.MANAGE_APP_OPS_MODES,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
+ public void setInteractAcrossProfilesAppOp(@NonNull String packageName, @Mode int newMode) {
+ try {
+ mService.setInteractAcrossProfilesAppOp(packageName, newMode);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
private void verifyCanAccessUser(UserHandle userHandle) {
if (!getTargetUserProfiles().contains(userHandle)) {
throw new SecurityException("Not allowed to access " + userHandle);
diff --git a/core/java/android/content/pm/ICrossProfileApps.aidl b/core/java/android/content/pm/ICrossProfileApps.aidl
index c5db0cc..694b1a3 100644
--- a/core/java/android/content/pm/ICrossProfileApps.aidl
+++ b/core/java/android/content/pm/ICrossProfileApps.aidl
@@ -32,4 +32,5 @@
List<UserHandle> getTargetUserProfiles(in String callingPackage);
boolean canInteractAcrossProfiles(in String callingPackage);
boolean canRequestInteractAcrossProfiles(in String callingPackage);
+ void setInteractAcrossProfilesAppOp(in String packageName, int newMode);
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
index ed71399..bdc1b07 100644
--- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
+++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -16,6 +16,8 @@
package com.android.server.pm;
import static android.app.AppOpsManager.OP_INTERACT_ACROSS_PROFILES;
+import static android.content.Intent.FLAG_RECEIVER_REGISTERED_ONLY;
+import static android.content.pm.CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
@@ -26,6 +28,7 @@
import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.AppOpsManager.Mode;
import android.app.IApplicationThread;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManagerInternal;
@@ -66,8 +69,6 @@
private Context mContext;
private Injector mInjector;
private AppOpsService mAppOpsService;
- private final DevicePolicyManagerInternal mDpmi;
- private final IPackageManager mIpm;
public CrossProfileAppsServiceImpl(Context context) {
this(context, new InjectorImpl(context));
@@ -77,8 +78,6 @@
CrossProfileAppsServiceImpl(Context context, Injector injector) {
mContext = context;
mInjector = injector;
- mIpm = AppGlobals.getPackageManager();
- mDpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
}
@Override
@@ -144,7 +143,7 @@
// 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(
+ final int permissionFlag = mInjector.checkComponentPermission(
android.Manifest.permission.INTERACT_ACROSS_PROFILES, callingUid,
-1, true);
if (permissionFlag != PackageManager.PERMISSION_GRANTED
@@ -172,23 +171,27 @@
public boolean canRequestInteractAcrossProfiles(String callingPackage) {
Objects.requireNonNull(callingPackage);
verifyCallingPackage(callingPackage);
-
- final List<UserHandle> targetUserProfiles = getTargetUserProfilesUnchecked(
+ return canRequestInteractAcrossProfilesUnchecked(
callingPackage, mInjector.getCallingUserId());
+ }
+
+ private boolean canRequestInteractAcrossProfilesUnchecked(
+ String packageName, @UserIdInt int userId) {
+ List<UserHandle> targetUserProfiles = getTargetUserProfilesUnchecked(packageName, userId);
if (targetUserProfiles.isEmpty()) {
return false;
}
-
if (!hasRequestedAppOpPermission(
- AppOpsManager.opToPermission(OP_INTERACT_ACROSS_PROFILES), callingPackage)) {
+ AppOpsManager.opToPermission(OP_INTERACT_ACROSS_PROFILES), packageName)) {
return false;
}
- return isCrossProfilePackageWhitelisted(callingPackage);
+ return isCrossProfilePackageWhitelisted(packageName);
}
private boolean hasRequestedAppOpPermission(String permission, String packageName) {
try {
- String[] packages = mIpm.getAppOpPermissionPackages(permission);
+ String[] packages =
+ mInjector.getIPackageManager().getAppOpPermissionPackages(permission);
return ArrayUtils.contains(packages, packageName);
} catch (RemoteException exc) {
Slog.e(TAG, "PackageManager dead. Cannot get permission info");
@@ -206,7 +209,6 @@
if (targetUserProfiles.isEmpty()) {
return false;
}
-
final int callingUid = mInjector.getCallingUid();
return isPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
|| isPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS, callingUid)
@@ -219,7 +221,8 @@
private boolean isCrossProfilePackageWhitelisted(String packageName) {
final long ident = mInjector.clearCallingIdentity();
try {
- return mDpmi.getAllCrossProfilePackages().contains(packageName);
+ return mInjector.getDevicePolicyManagerInternal()
+ .getAllCrossProfilePackages().contains(packageName);
} finally {
mInjector.restoreCallingIdentity(ident);
}
@@ -295,6 +298,104 @@
}
}
+ @Override
+ public void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) {
+ final int callingUid = mInjector.getCallingUid();
+ if (!isPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ && !isPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS, callingUid)) {
+ throw new SecurityException(
+ "INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL is required to set the"
+ + " app-op for interacting across profiles.");
+ }
+ if (!isPermissionGranted(Manifest.permission.MANAGE_APP_OPS_MODES, callingUid)) {
+ throw new SecurityException(
+ "MANAGE_APP_OPS_MODES is required to set the app-op for interacting across"
+ + " profiles.");
+ }
+ final int callingUserId = mInjector.getCallingUserId();
+ if (newMode == AppOpsManager.MODE_ALLOWED
+ && !canRequestInteractAcrossProfilesUnchecked(packageName, callingUserId)) {
+ // The user should not be prompted for apps that cannot request to interact across
+ // profiles. However, we return early here if required to avoid race conditions.
+ Slog.e(TAG, "Tried to turn on the appop for interacting across profiles for invalid"
+ + " app " + packageName);
+ return;
+ }
+ final int[] profileIds =
+ mInjector.getUserManager().getProfileIds(callingUserId, /* enabledOnly= */ false);
+ for (int profileId : profileIds) {
+ if (!isPackageInstalled(packageName, profileId)) {
+ continue;
+ }
+ setInteractAcrossProfilesAppOpForUser(packageName, newMode, profileId);
+ }
+ }
+
+ private boolean isPackageInstalled(String packageName, @UserIdInt int userId) {
+ final int callingUid = mInjector.getCallingUid();
+ final long identity = mInjector.clearCallingIdentity();
+ try {
+ final PackageInfo info =
+ mInjector.getPackageManagerInternal()
+ .getPackageInfo(
+ packageName,
+ MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
+ callingUid,
+ userId);
+ return info != null;
+ } finally {
+ mInjector.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void setInteractAcrossProfilesAppOpForUser(
+ String packageName, @Mode int newMode, @UserIdInt int userId) {
+ try {
+ setInteractAcrossProfilesAppOpForUserOrThrow(packageName, newMode, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Missing package " + packageName + " on user ID " + userId, e);
+ }
+ }
+
+ private void setInteractAcrossProfilesAppOpForUserOrThrow(
+ String packageName, @Mode int newMode, @UserIdInt int userId)
+ throws PackageManager.NameNotFoundException {
+ final int uid = mInjector.getPackageManager()
+ .getPackageUidAsUser(packageName, /* flags= */ 0, userId);
+ if (currentModeEquals(newMode, packageName, uid)) {
+ Slog.w(TAG,"Attempt to set mode to existing value of " + newMode + " for "
+ + packageName + " on user ID " + userId);
+ return;
+ }
+ mInjector.getAppOpsManager()
+ .setMode(OP_INTERACT_ACROSS_PROFILES,
+ uid,
+ packageName,
+ newMode);
+ sendCanInteractAcrossProfilesChangedBroadcast(packageName, uid, UserHandle.of(userId));
+ }
+
+ private boolean currentModeEquals(@Mode int otherMode, String packageName, int uid) {
+ final String op =
+ AppOpsManager.permissionToOp(Manifest.permission.INTERACT_ACROSS_PROFILES);
+ return otherMode ==
+ mInjector.getAppOpsManager().unsafeCheckOpNoThrow(op, uid, packageName);
+ }
+
+ private void sendCanInteractAcrossProfilesChangedBroadcast(
+ String packageName, int uid, UserHandle userHandle) {
+ final Intent intent = new Intent(ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED)
+ .setPackage(packageName);
+ if (!appDeclaresCrossProfileAttribute(uid)) {
+ intent.addFlags(FLAG_RECEIVER_REGISTERED_ONLY);
+ }
+ mInjector.sendBroadcastAsUser(intent, userHandle);
+ }
+
+ private boolean appDeclaresCrossProfileAttribute(int uid) {
+ return mInjector.getPackageManagerInternal().getPackage(uid).isCrossProfile();
+ }
+
private boolean isSameProfileGroup(@UserIdInt int callerUserId, @UserIdInt int userId) {
final long ident = mInjector.clearCallingIdentity();
try {
@@ -311,8 +412,8 @@
mInjector.getAppOpsManager().checkPackage(mInjector.getCallingUid(), callingPackage);
}
- private static boolean isPermissionGranted(String permission, int uid) {
- return PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+ private boolean isPermissionGranted(String permission, int uid) {
+ return PackageManager.PERMISSION_GRANTED == mInjector.checkComponentPermission(
permission, uid, /* owningUid= */-1, /* exported= */ true);
}
@@ -376,6 +477,27 @@
public ActivityTaskManagerInternal getActivityTaskManagerInternal() {
return LocalServices.getService(ActivityTaskManagerInternal.class);
}
+
+ @Override
+ public IPackageManager getIPackageManager() {
+ return AppGlobals.getPackageManager();
+ }
+
+ @Override
+ public DevicePolicyManagerInternal getDevicePolicyManagerInternal() {
+ return LocalServices.getService(DevicePolicyManagerInternal.class);
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+ mContext.sendBroadcastAsUser(intent, user);
+ }
+
+ @Override
+ public int checkComponentPermission(
+ String permission, int uid, int owningUid, boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
}
@VisibleForTesting
@@ -401,5 +523,13 @@
ActivityManagerInternal getActivityManagerInternal();
ActivityTaskManagerInternal getActivityTaskManagerInternal();
+
+ IPackageManager getIPackageManager();
+
+ DevicePolicyManagerInternal getDevicePolicyManagerInternal();
+
+ void sendBroadcastAsUser(Intent intent, UserHandle user);
+
+ int checkComponentPermission(String permission, int uid, int owningUid, boolean exported);
}
}
diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp
index 3ce514a..d2f86ee 100644
--- a/services/robotests/Android.bp
+++ b/services/robotests/Android.bp
@@ -43,6 +43,9 @@
"platform-test-annotations",
"testng",
],
+ static_libs: [
+ "androidx.test.ext.truth",
+ ],
instrumentation_for: "FrameworksServicesLib",
}
diff --git a/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java b/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java
new file mode 100644
index 0000000..ce088643
--- /dev/null
+++ b/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.OP_INTERACT_ACROSS_PROFILES;
+import static android.content.Intent.FLAG_RECEIVER_REGISTERED_ONLY;
+import static android.content.pm.CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.Manifest;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.Mode;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.parsing.AndroidPackage;
+import android.content.pm.parsing.PackageImpl;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.LocalServices;
+import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowUserManager;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import com.google.android.collect.Lists;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Unit tests for {@link CrossProfileAppsServiceImpl}. */
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowUserManager.class, ShadowApplicationPackageManager.class})
+public class CrossProfileAppsServiceImplRoboTest {
+ private static final int CALLING_UID = 1111;
+ private static final String CROSS_PROFILE_APP_PACKAGE_NAME =
+ "com.android.server.pm.crossprofileappsserviceimplrobotest.crossprofileapp";
+ private static final int PERSONAL_PROFILE_USER_ID = 0;
+ private static final int PERSONAL_PROFILE_UID = 2222;
+ private static final int WORK_PROFILE_USER_ID = 10;
+ private static final int WORK_PROFILE_UID = 3333;
+ private static final int OTHER_PROFILE_WITHOUT_CROSS_PROFILE_APP_USER_ID = 20;
+
+ private final ContextWrapper mContext = ApplicationProvider.getApplicationContext();
+ private final UserManager mUserManager = mContext.getSystemService(UserManager.class);
+ private final AppOpsManager mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+ private final PackageManager mPackageManager = mContext.getPackageManager();
+ private final TestInjector mInjector = new TestInjector();
+ private final CrossProfileAppsServiceImpl mCrossProfileAppsServiceImpl =
+ new CrossProfileAppsServiceImpl(mContext, mInjector);
+ private final Map<UserHandle, Set<Intent>> mSentUserBroadcasts = new HashMap<>();
+
+ @Mock private PackageManagerInternal mPackageManagerInternal;
+ @Mock private IPackageManager mIPackageManager;
+ @Mock private DevicePolicyManagerInternal mDevicePolicyManagerInternal;
+
+ @Before
+ public void initializeMocks() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mockCrossProfileAppInstalledAndEnabledOnEachProfile();
+ mockCrossProfileAppRequestsInteractAcrossProfiles();
+ mockCrossProfileAppWhitelisted();
+ }
+
+ private void mockCrossProfileAppInstalledAndEnabledOnEachProfile() {
+ // They are enabled by default, so we simply have to ensure that a package info with an
+ // application info is returned.
+ final PackageInfo packageInfo = buildTestPackageInfo();
+ when(mPackageManagerInternal.getPackageInfo(
+ eq(CROSS_PROFILE_APP_PACKAGE_NAME),
+ /* flags= */ anyInt(),
+ /* filterCallingUid= */ anyInt(),
+ eq(PERSONAL_PROFILE_USER_ID)))
+ .thenReturn(packageInfo);
+ when(mPackageManagerInternal.getPackageInfo(
+ eq(CROSS_PROFILE_APP_PACKAGE_NAME),
+ /* flags= */ anyInt(),
+ /* filterCallingUid= */ anyInt(),
+ eq(WORK_PROFILE_USER_ID)))
+ .thenReturn(packageInfo);
+ mockCrossProfileAndroidPackage(PackageImpl.forParsing(CROSS_PROFILE_APP_PACKAGE_NAME));
+ }
+
+ private PackageInfo buildTestPackageInfo() {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.applicationInfo = new ApplicationInfo();
+ return packageInfo;
+ }
+
+ private void mockCrossProfileAppRequestsInteractAcrossProfiles() throws Exception {
+ final String permissionName = Manifest.permission.INTERACT_ACROSS_PROFILES;
+ when(mIPackageManager.getAppOpPermissionPackages(permissionName))
+ .thenReturn(new String[] {CROSS_PROFILE_APP_PACKAGE_NAME});
+ }
+
+ private void mockCrossProfileAppWhitelisted() {
+ when(mDevicePolicyManagerInternal.getAllCrossProfilePackages())
+ .thenReturn(Lists.newArrayList(CROSS_PROFILE_APP_PACKAGE_NAME));
+ }
+
+ @Before
+ public void setUpCrossProfileAppUidsAndPackageNames() {
+ ShadowApplicationPackageManager.setPackageUidAsUser(
+ CROSS_PROFILE_APP_PACKAGE_NAME, PERSONAL_PROFILE_UID, PERSONAL_PROFILE_USER_ID);
+ ShadowApplicationPackageManager.setPackageUidAsUser(
+ CROSS_PROFILE_APP_PACKAGE_NAME, WORK_PROFILE_UID, WORK_PROFILE_USER_ID);
+ }
+
+ @Before
+ public void grantPermissions() {
+ grantPermissions(
+ Manifest.permission.MANAGE_APP_OPS_MODES,
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+ }
+
+ @Before
+ public void setUpProfiles() {
+ final ShadowUserManager shadowUserManager = Shadow.extract(mUserManager);
+ shadowUserManager.addProfileIds(
+ PERSONAL_PROFILE_USER_ID,
+ WORK_PROFILE_USER_ID,
+ OTHER_PROFILE_WITHOUT_CROSS_PROFILE_APP_USER_ID);
+ }
+
+ @Before
+ public void setInteractAcrossProfilesAppOpDefault() {
+ // It seems to be necessary to provide the shadow with the default already specified in
+ // AppOpsManager.
+ final int defaultMode = AppOpsManager.opToDefaultMode(OP_INTERACT_ACROSS_PROFILES);
+ explicitlySetInteractAcrossProfilesAppOp(PERSONAL_PROFILE_UID, defaultMode);
+ explicitlySetInteractAcrossProfilesAppOp(WORK_PROFILE_UID, defaultMode);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_missingInteractAcrossUsersAndFull_throwsSecurityException() {
+ denyPermissions(Manifest.permission.INTERACT_ACROSS_USERS);
+ denyPermissions(Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+ try {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ fail();
+ } catch (SecurityException expected) {}
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_missingManageAppOpsModes_throwsSecurityException() {
+ denyPermissions(Manifest.permission.MANAGE_APP_OPS_MODES);
+ try {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ fail();
+ } catch (SecurityException expected) {}
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_setsAppOp() {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(getCrossProfileAppOp()).isEqualTo(MODE_ALLOWED);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_setsAppOpWithUsersAndWithoutFull() {
+ denyPermissions(Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(getCrossProfileAppOp()).isEqualTo(MODE_ALLOWED);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_setsAppOpWithFullAndWithoutUsers() {
+ denyPermissions(Manifest.permission.INTERACT_ACROSS_USERS);
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(getCrossProfileAppOp()).isEqualTo(MODE_ALLOWED);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_setsAppOpOnOtherProfile() {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(getCrossProfileAppOp(WORK_PROFILE_UID)).isEqualTo(MODE_ALLOWED);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_sendsBroadcast() {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedCanInteractAcrossProfilesChangedBroadcast()).isTrue();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_sendsBroadcastToOtherProfile() {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedCanInteractAcrossProfilesChangedBroadcast(WORK_PROFILE_USER_ID))
+ .isTrue();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_doesNotSendBroadcastToProfileWithoutPackage() {
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedCanInteractAcrossProfilesChangedBroadcast(
+ OTHER_PROFILE_WITHOUT_CROSS_PROFILE_APP_USER_ID))
+ .isFalse();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_toSameAsCurrent_doesNotSendBroadcast() {
+ explicitlySetInteractAcrossProfilesAppOp(MODE_ALLOWED);
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedCanInteractAcrossProfilesChangedBroadcast()).isFalse();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_toAllowed_whenNotAbleToRequest_doesNotSet() {
+ mockCrossProfileAppNotWhitelisted();
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(getCrossProfileAppOp()).isNotEqualTo(MODE_ALLOWED);
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_toAllowed_whenNotAbleToRequest_doesNotSendBroadcast() {
+ mockCrossProfileAppNotWhitelisted();
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedCanInteractAcrossProfilesChangedBroadcast()).isFalse();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_withoutCrossProfileAttribute_manifestReceiversDoNotGetBroadcast() {
+ declareCrossProfileAttributeOnCrossProfileApp(false);
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedManifestCanInteractAcrossProfilesChangedBroadcast()).isFalse();
+ }
+
+ @Test
+ public void setInteractAcrossProfilesAppOp_withCrossProfileAttribute_manifestReceiversGetBroadcast() {
+ declareCrossProfileAttributeOnCrossProfileApp(true);
+ mCrossProfileAppsServiceImpl.setInteractAcrossProfilesAppOp(
+ CROSS_PROFILE_APP_PACKAGE_NAME, MODE_ALLOWED);
+ assertThat(receivedManifestCanInteractAcrossProfilesChangedBroadcast()).isTrue();
+ }
+
+ private void explicitlySetInteractAcrossProfilesAppOp(@Mode int mode) {
+ explicitlySetInteractAcrossProfilesAppOp(PERSONAL_PROFILE_UID, mode);
+ }
+
+ private void explicitlySetInteractAcrossProfilesAppOp(int uid, @Mode int mode) {
+ shadowOf(mAppOpsManager).setMode(
+ OP_INTERACT_ACROSS_PROFILES, uid, CROSS_PROFILE_APP_PACKAGE_NAME, mode);
+ }
+
+ private void grantPermissions(String... permissions) {
+ shadowOf(mContext).grantPermissions(Process.myPid(), CALLING_UID, permissions);
+ }
+
+ private void denyPermissions(String... permissions) {
+ shadowOf(mContext).denyPermissions(Process.myPid(), CALLING_UID, permissions);
+ }
+
+
+ private @Mode int getCrossProfileAppOp() {
+ return getCrossProfileAppOp(PERSONAL_PROFILE_UID);
+ }
+
+ private @Mode int getCrossProfileAppOp(int uid) {
+ return mAppOpsManager.unsafeCheckOpNoThrow(
+ AppOpsManager.permissionToOp(Manifest.permission.INTERACT_ACROSS_PROFILES),
+ uid,
+ CROSS_PROFILE_APP_PACKAGE_NAME);
+ }
+
+ private boolean receivedCanInteractAcrossProfilesChangedBroadcast() {
+ return receivedCanInteractAcrossProfilesChangedBroadcast(PERSONAL_PROFILE_USER_ID);
+ }
+
+ private boolean receivedCanInteractAcrossProfilesChangedBroadcast(@UserIdInt int userId) {
+ final UserHandle userHandle = UserHandle.of(userId);
+ if (!mSentUserBroadcasts.containsKey(userHandle)) {
+ return false;
+ }
+ return mSentUserBroadcasts.get(userHandle)
+ .stream()
+ .anyMatch(this::isBroadcastCanInteractAcrossProfilesChanged);
+ }
+
+ private boolean isBroadcastCanInteractAcrossProfilesChanged(Intent intent) {
+ return intent.getAction().equals(ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED)
+ && CROSS_PROFILE_APP_PACKAGE_NAME.equals(intent.getPackage());
+ }
+
+ private void mockCrossProfileAndroidPackage(AndroidPackage androidPackage) {
+ when(mPackageManagerInternal.getPackage(CROSS_PROFILE_APP_PACKAGE_NAME))
+ .thenReturn(androidPackage);
+ when(mPackageManagerInternal.getPackage(PERSONAL_PROFILE_UID)).thenReturn(androidPackage);
+ when(mPackageManagerInternal.getPackage(WORK_PROFILE_UID)).thenReturn(androidPackage);
+ }
+
+ private void mockCrossProfileAppNotWhitelisted() {
+ when(mDevicePolicyManagerInternal.getAllCrossProfilePackages())
+ .thenReturn(new ArrayList<>());
+ }
+
+ private boolean receivedManifestCanInteractAcrossProfilesChangedBroadcast() {
+ final UserHandle userHandle = UserHandle.of(PERSONAL_PROFILE_USER_ID);
+ if (!mSentUserBroadcasts.containsKey(userHandle)) {
+ return false;
+ }
+ return mSentUserBroadcasts.get(userHandle)
+ .stream()
+ .anyMatch(this::isBroadcastManifestCanInteractAcrossProfilesChanged);
+ }
+
+ private boolean isBroadcastManifestCanInteractAcrossProfilesChanged(Intent intent) {
+ // The manifest check is negative since the FLAG_RECEIVER_REGISTERED_ONLY flag means that
+ // manifest receivers can NOT receive the broadcast.
+ return isBroadcastCanInteractAcrossProfilesChanged(intent)
+ && (intent.getFlags() & FLAG_RECEIVER_REGISTERED_ONLY) == 0;
+ }
+
+ private void declareCrossProfileAttributeOnCrossProfileApp(boolean value) {
+ mockCrossProfileAndroidPackage(
+ PackageImpl.forParsing(CROSS_PROFILE_APP_PACKAGE_NAME).setCrossProfile(value));
+ }
+
+ private class TestInjector implements CrossProfileAppsServiceImpl.Injector {
+
+ @Override
+ public int getCallingUid() {
+ return CALLING_UID;
+ }
+
+ @Override
+ public @UserIdInt int getCallingUserId() {
+ return PERSONAL_PROFILE_USER_ID;
+ }
+
+ @Override
+ public UserHandle getCallingUserHandle() {
+ return UserHandle.of(getCallingUserId());
+ }
+
+ @Override
+ public long clearCallingIdentity() {
+ return 0;
+ }
+
+ @Override
+ public void restoreCallingIdentity(long token) {}
+
+ @Override
+ public UserManager getUserManager() {
+ return mUserManager;
+ }
+
+ @Override
+ public PackageManagerInternal getPackageManagerInternal() {
+ return mPackageManagerInternal;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public AppOpsManager getAppOpsManager() {
+ return mAppOpsManager;
+ }
+
+ @Override
+ public ActivityManagerInternal getActivityManagerInternal() {
+ return LocalServices.getService(ActivityManagerInternal.class);
+ }
+
+ @Override
+ public ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return LocalServices.getService(ActivityTaskManagerInternal.class);
+ }
+
+ @Override
+ public IPackageManager getIPackageManager() {
+ return mIPackageManager;
+ }
+
+ @Override
+ public DevicePolicyManagerInternal getDevicePolicyManagerInternal() {
+ return mDevicePolicyManagerInternal;
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+ // Robolectric's shadows do not currently support sendBroadcastAsUser.
+ final Set<Intent> broadcasts =
+ mSentUserBroadcasts.containsKey(user)
+ ? mSentUserBroadcasts.get(user)
+ : new HashSet<>();
+ broadcasts.add(intent);
+ mSentUserBroadcasts.put(user, broadcasts);
+ mContext.sendBroadcastAsUser(intent, user);
+ }
+
+ @Override
+ public int checkComponentPermission(
+ String permission, int uid, int owningUid, boolean exported) {
+ // ActivityManager#checkComponentPermission calls through to
+ // AppGlobals.getPackageManager()#checkUidPermission, which calls through to
+ // ShadowActivityThread with Robolectric. This method is currently not supported there.
+ return mContext.checkPermission(permission, Process.myPid(), uid);
+ }
+ }
+}
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
index ab121ed..1443eab 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java
@@ -26,6 +26,7 @@
import org.robolectric.annotation.Resetter;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +40,7 @@
private static final Map<String, PackageInfo> sPackageInfos = new ArrayMap<>();
private static final List<PackageInfo> sInstalledPackages = new ArrayList<>();
private static final Map<String, Integer> sPackageUids = new ArrayMap<>();
+ private static final Map<Integer, Map<String, Integer>> sUserPackageUids = new ArrayMap<>();
/**
* Registers the package {@code packageName} to be returned when invoking {@link
@@ -58,6 +60,19 @@
sPackageUids.put(packageName, packageUid);
}
+ /**
+ * Sets the package uid {@code packageUid} for the package {@code packageName} to be returned
+ * when invoking {@link ApplicationPackageManager#getPackageUidAsUser(String, int, int)}.
+ */
+ public static void setPackageUidAsUser(String packageName, int packageUid, int userId) {
+ final Map<String, Integer> userPackageUids =
+ sUserPackageUids.containsKey(userId)
+ ? sUserPackageUids.get(userId)
+ : new HashMap<>();
+ userPackageUids.put(packageName, packageUid);
+ sUserPackageUids.put(userId, userPackageUids);
+ }
+
@Override
protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId)
throws NameNotFoundException {
@@ -75,6 +90,10 @@
@Override
protected int getPackageUidAsUser(String packageName, int flags, int userId)
throws NameNotFoundException {
+ if (sUserPackageUids.containsKey(userId)
+ && sUserPackageUids.get(userId).containsKey(packageName)) {
+ return sUserPackageUids.get(userId).get(packageName);
+ }
if (!sPackageUids.containsKey(packageName)) {
throw new NameNotFoundException(packageName);
}
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
index c6ae1a1..a9e4ee5 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowUserManager.java
@@ -16,18 +16,46 @@
package com.android.server.testing.shadows;
+import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.os.UserManager;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
/** Shadow for {@link UserManager}. */
@Implements(UserManager.class)
public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager {
+ private final Map<Integer, Set<Integer>> profileIds = new HashMap<>();
+
/** @see UserManager#isUserUnlocked() */
@Implementation
public boolean isUserUnlocked(@UserIdInt int userId) {
return false;
}
+
+ /** @see UserManager#getProfileIds(int, boolean) () */
+ @Implementation
+ @NonNull
+ public int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) {
+ // Currently, enabledOnly is ignored.
+ if (!profileIds.containsKey(userId)) {
+ return new int[] {userId};
+ }
+ return profileIds.get(userId).stream().mapToInt(Number::intValue).toArray();
+ }
+
+ /** Add a collection of profile IDs, all within the same profile group. */
+ public void addProfileIds(@UserIdInt int... userIds) {
+ final Set<Integer> profileGroup = new HashSet<>();
+ for (int userId : userIds) {
+ profileGroup.add(userId);
+ profileIds.put(userId, profileGroup);
+ }
+ }
}
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 e18ff61..68f60b4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
@@ -13,14 +13,17 @@
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IApplicationThread;
+import android.app.admin.DevicePolicyManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
@@ -76,6 +79,10 @@
private ActivityManagerInternal mActivityManagerInternal;
@Mock
private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+ @Mock
+ private IPackageManager mIPackageManager;
+ @Mock
+ private DevicePolicyManagerInternal mDevicePolicyManagerInternal;
private TestInjector mTestInjector;
private ActivityInfo mActivityInfo;
@@ -578,5 +585,26 @@
public ActivityTaskManagerInternal getActivityTaskManagerInternal() {
return mActivityTaskManagerInternal;
}
+
+ @Override
+ public IPackageManager getIPackageManager() {
+ return mIPackageManager;
+ }
+
+ @Override
+ public DevicePolicyManagerInternal getDevicePolicyManagerInternal() {
+ return mDevicePolicyManagerInternal;
+ }
+
+ @Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user) {
+ mContext.sendBroadcastAsUser(intent, user);
+ }
+
+ @Override
+ public int checkComponentPermission(
+ String permission, int uid, int owningUid, boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
}
}