diff --git a/api/current.txt b/api/current.txt index 0d233713d8f6f..0812019670753 100644 --- a/api/current.txt +++ b/api/current.txt @@ -11399,6 +11399,7 @@ package android.content.pm { method @NonNull public CharSequence getProfileSwitchingLabel(@NonNull android.os.UserHandle); method @NonNull public java.util.List 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 9d5751480a80b..5aa9c9bebc2cb 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -19,6 +19,7 @@ import android.annotation.NonNull; 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 @@ import java.util.Set; * 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()}. + * + *

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 @@ public class CrossProfileApps { 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. + * + *

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. + * + *

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. + * + *

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 c5db0ccebf529..694b1a3c4e73a 100644 --- a/core/java/android/content/pm/ICrossProfileApps.aidl +++ b/core/java/android/content/pm/ICrossProfileApps.aidl @@ -32,4 +32,5 @@ interface ICrossProfileApps { List 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 ed7139991937c..bdc1b074920a7 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.ActivityManagerInternal; 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { CrossProfileAppsServiceImpl(Context context, Injector injector) { mContext = context; mInjector = injector; - mIpm = AppGlobals.getPackageManager(); - mDpmi = LocalServices.getService(DevicePolicyManagerInternal.class); } @Override @@ -144,7 +143,7 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { // 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 class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { public boolean canRequestInteractAcrossProfiles(String callingPackage) { Objects.requireNonNull(callingPackage); verifyCallingPackage(callingPackage); - - final List targetUserProfiles = getTargetUserProfilesUnchecked( + return canRequestInteractAcrossProfilesUnchecked( callingPackage, mInjector.getCallingUserId()); + } + + private boolean canRequestInteractAcrossProfilesUnchecked( + String packageName, @UserIdInt int userId) { + List 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { } } + @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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { 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 3ce514a56b60a..d2f86eeabb478 100644 --- a/services/robotests/Android.bp +++ b/services/robotests/Android.bp @@ -43,6 +43,9 @@ android_robolectric_test { "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 0000000000000..ce08864359062 --- /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> 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 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 ab121eddff060..1443eabf07d56 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.Implements; 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 @@ public class ShadowApplicationPackageManager private static final Map sPackageInfos = new ArrayMap<>(); private static final List sInstalledPackages = new ArrayList<>(); private static final Map sPackageUids = new ArrayMap<>(); + private static final Map> sUserPackageUids = new ArrayMap<>(); /** * Registers the package {@code packageName} to be returned when invoking {@link @@ -58,6 +60,19 @@ public class ShadowApplicationPackageManager 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 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 @@ public class ShadowApplicationPackageManager @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 c6ae1a1b6863f..a9e4ee521f903 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> 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 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 e18ff61e32dc5..68f60b4ff67ae 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.verify; 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 @@ public class CrossProfileAppsServiceImplTest { 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 class CrossProfileAppsServiceImplTest { 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); + } } }