Merge "Support setting the _PROFILES app-op"

This commit is contained in:
TreeHugger Robot
2020-01-17 20:17:37 +00:00
committed by Android (Google) Code Review
9 changed files with 734 additions and 15 deletions

View File

@@ -11399,6 +11399,7 @@ package android.content.pm {
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 {

View File

@@ -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()}.
*
* <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 @@ 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.
*
* <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);

View File

@@ -32,4 +32,5 @@ interface ICrossProfileApps {
List<UserHandle> getTargetUserProfiles(in String callingPackage);
boolean canInteractAcrossProfiles(in String callingPackage);
boolean canRequestInteractAcrossProfiles(in String callingPackage);
void setInteractAcrossProfilesAppOp(in String packageName, int newMode);
}

View File

@@ -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<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 @@ 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);
}
}

View File

@@ -43,6 +43,9 @@ android_robolectric_test {
"platform-test-annotations",
"testng",
],
static_libs: [
"androidx.test.ext.truth",
],
instrumentation_for: "FrameworksServicesLib",
}

View File

@@ -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);
}
}
}

View File

@@ -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<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 @@ 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<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 @@ 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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}