diff --git a/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java new file mode 100644 index 0000000000000..c81fbb443dce3 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java @@ -0,0 +1,781 @@ +package com.android.server.pm; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; +import android.app.AppOpsManager; +import android.app.IApplicationThread; +import android.app.admin.DevicePolicyManagerInternal; +import android.content.AttributionSourceState; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.PermissionChecker; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileAppsInternal; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.PermissionInfo; +import android.content.pm.ResolveInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.UserHandle; +import android.os.UserManager; +import android.permission.PermissionCheckerManager; +import android.permission.PermissionManager; +import android.platform.test.annotations.Presubmit; +import android.util.SparseArray; + +import com.android.internal.util.FunctionalUtils.ThrowingRunnable; +import com.android.internal.util.FunctionalUtils.ThrowingSupplier; +import com.android.server.LocalServices; +import com.android.server.pm.permission.PermissionManagerService; +import com.android.server.wm.ActivityTaskManagerInternal; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Build/Install/Run: + * atest FrameworksServicesTests:com.android.server.pm.CrossProfileAppsServiceImplTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class CrossProfileAppsServiceImplTest { + private static final String PACKAGE_ONE = "com.one"; + private static final String FEATURE_ID = "feature.one"; + private static final int PACKAGE_ONE_UID = 1111; + private static final ComponentName ACTIVITY_COMPONENT = + new ComponentName("com.one", "test"); + + private static final String PACKAGE_TWO = "com.two"; + private static final int PACKAGE_TWO_UID = 2222; + + private static final int PRIMARY_USER = 0; + private static final int PROFILE_OF_PRIMARY_USER = 10; + private static final int SECONDARY_USER = 11; + + @Mock + private Context mContext; + @Mock + private UserManager mUserManager; + @Mock + private PackageManager mPackageManager; + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private AppOpsManager mAppOpsManager; + @Mock + private ActivityManagerInternal mActivityManagerInternal; + @Mock + private ActivityTaskManagerInternal mActivityTaskManagerInternal; + @Mock + private IPackageManager mIPackageManager; + @Mock + private DevicePolicyManagerInternal mDevicePolicyManagerInternal; + + private TestInjector mTestInjector; + private ActivityInfo mActivityInfo; + private CrossProfileAppsServiceImpl mCrossProfileAppsServiceImpl; + private IApplicationThread mIApplicationThread; + + private SparseArray mUserEnabled = new SparseArray<>(); + + @Before + public void initCrossProfileAppsServiceImpl() { + mTestInjector = new TestInjector(); + LocalServices.removeServiceForTest(CrossProfileAppsInternal.class); + mCrossProfileAppsServiceImpl = new CrossProfileAppsServiceImpl(mContext, mTestInjector); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + } + + @Before + public void setupEnabledProfiles() { + mUserEnabled.put(PRIMARY_USER, true); + mUserEnabled.put(PROFILE_OF_PRIMARY_USER, true); + mUserEnabled.put(SECONDARY_USER, true); + + when(mUserManager.getEnabledProfileIds(anyInt())).thenAnswer( + invocation -> { + List users = new ArrayList<>(); + final int targetUser = invocation.getArgument(0); + users.add(targetUser); + + int profileUserId = -1; + if (targetUser == PRIMARY_USER) { + profileUserId = PROFILE_OF_PRIMARY_USER; + } else if (targetUser == PROFILE_OF_PRIMARY_USER) { + profileUserId = PRIMARY_USER; + } + + if (profileUserId != -1 && mUserEnabled.get(profileUserId)) { + users.add(profileUserId); + } + return users.stream().mapToInt(i -> i).toArray(); + }); + } + + @Before + public void setupCaller() { + mTestInjector.setCallingUid(PACKAGE_ONE_UID); + mTestInjector.setCallingUserId(PRIMARY_USER); + } + + @Before + public void setupPackage() throws Exception { + // PACKAGE_ONE are installed in all users. + mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, true); + mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, true); + mockAppsInstalled(PACKAGE_ONE, SECONDARY_USER, true); + + // Packages are resolved to their corresponding UID. + doAnswer(invocation -> { + final int uid = invocation.getArgument(0); + final String packageName = invocation.getArgument(1); + if (uid == PACKAGE_ONE_UID && PACKAGE_ONE.equals(packageName)) { + return null; + } else if (uid ==PACKAGE_TWO_UID && PACKAGE_TWO.equals(packageName)) { + return null; + } + throw new SecurityException("Not matching"); + }).when(mAppOpsManager).checkPackage(anyInt(), anyString()); + + // The intent is resolved to the ACTIVITY_COMPONENT. + mockActivityLaunchIntentResolvedTo(ACTIVITY_COMPONENT); + } + + @Test + public void getTargetUserProfiles_fromPrimaryUser_installed() throws Exception { + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).containsExactly(UserHandle.of(PROFILE_OF_PRIMARY_USER)); + } + + @Test + public void getTargetUserProfiles_fromPrimaryUser_notInstalled() throws Exception { + mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false); + + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).isEmpty(); + } + + @Test + public void getTargetUserProfiles_fromPrimaryUser_userNotEnabled() throws Exception { + mUserEnabled.put(PROFILE_OF_PRIMARY_USER, false); + + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).isEmpty(); + } + + @Test + public void getTargetUserProfiles_fromSecondaryUser() throws Exception { + mTestInjector.setCallingUserId(SECONDARY_USER); + + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).isEmpty(); + } + + @Test + public void getTargetUserProfiles_fromProfile_installed() throws Exception { + mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER); + + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).containsExactly(UserHandle.of(PRIMARY_USER)); + } + + @Test + public void getTargetUserProfiles_fromProfile_notInstalled() throws Exception { + mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER); + mockAppsInstalled(PACKAGE_ONE, PRIMARY_USER, false); + + List targetProfiles = + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE); + assertThat(targetProfiles).isEmpty(); + } + + @Test(expected = SecurityException.class) + public void getTargetUserProfiles_fakeCaller() throws Exception { + mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_TWO); + } + + @Test + public void startActivityAsUser_currentUser() throws Exception { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_currentUser() { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PRIMARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_profile_notInstalled() throws Exception { + mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false); + + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_profile_notInstalled() { + mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false); + + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_profile_fakeCaller() throws Exception { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_TWO, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_profile_fakeCaller() { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_TWO, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_profile_notExported() throws Exception { + mActivityInfo.exported = false; + + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_profile_notExported() { + try { + when(mPackageManager.getPermissionInfo(anyString(), anyInt())) + .thenReturn(new PermissionInfo()); + } catch (PackageManager.NameNotFoundException ignored) { + } + mActivityInfo.exported = false; + + + // There's a bug in static mocking if the APK is large - so here is the next best thing... + doReturn(Context.PERMISSION_CHECKER_SERVICE).when(mContext) + .getSystemServiceName(PermissionCheckerManager.class); + PermissionCheckerManager permissionCheckerManager = mock(PermissionCheckerManager.class); + doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(permissionCheckerManager) + .checkPermission(eq(Manifest.permission.INTERACT_ACROSS_PROFILES), any( + AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(), + anyBoolean(), anyInt()); + doReturn(permissionCheckerManager).when(mContext).getSystemService( + Context.PERMISSION_CHECKER_SERVICE); + + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_profile_anotherPackage() throws Exception { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + new ComponentName(PACKAGE_TWO, "test"), + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_profile_anotherPackage() { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + new ComponentName(PACKAGE_TWO, "test"), + UserHandle.of(PROFILE_OF_PRIMARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_secondaryUser() throws Exception { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(SECONDARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startAnyActivityAsUser_secondaryUser() { + assertThrows( + SecurityException.class, + () -> + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(SECONDARY_USER).getIdentifier(), + false, + /* targetTask */ null, + /* options */ null)); + + verify(mActivityTaskManagerInternal, never()) + .startActivityAsUser( + nullable(IApplicationThread.class), + anyString(), + nullable(String.class), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + anyInt()); + } + + @Test + public void startActivityAsUser_fromProfile_success() throws Exception { + mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER); + + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PRIMARY_USER).getIdentifier(), + true, + /* targetTask */ null, + /* options */ null); + + verify(mActivityTaskManagerInternal) + .startActivityAsUser( + nullable(IApplicationThread.class), + eq(PACKAGE_ONE), + eq(FEATURE_ID), + any(Intent.class), + nullable(IBinder.class), + anyInt(), + nullable(Bundle.class), + eq(PRIMARY_USER)); + } + + @Test + public void startActivityAsUser_sameTask_fromProfile_success() throws Exception { + mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER); + + Bundle options = ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(); + Binder targetTask = new Binder(); + mCrossProfileAppsServiceImpl.startActivityAsUser( + mIApplicationThread, + PACKAGE_ONE, + FEATURE_ID, + ACTIVITY_COMPONENT, + UserHandle.of(PRIMARY_USER).getIdentifier(), + true, + targetTask, + options); + verify(mActivityTaskManagerInternal) + .startActivityAsUser( + nullable(IApplicationThread.class), + eq(PACKAGE_ONE), + eq(FEATURE_ID), + any(Intent.class), + eq(targetTask), + anyInt(), + eq(options), + eq(PRIMARY_USER)); + } + + private void mockAppsInstalled(String packageName, int user, boolean installed) { + when(mPackageManagerInternal.getPackageInfo( + eq(packageName), + anyLong(), + anyInt(), + eq(user))) + .thenReturn(installed ? createInstalledPackageInfo() : null); + } + + private PackageInfo createInstalledPackageInfo() { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.enabled = true; + return packageInfo; + } + + private void mockActivityLaunchIntentResolvedTo(ComponentName componentName) { + ResolveInfo resolveInfo = new ResolveInfo(); + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.packageName = componentName.getPackageName(); + activityInfo.name = componentName.getClassName(); + activityInfo.exported = true; + resolveInfo.activityInfo = activityInfo; + mActivityInfo = activityInfo; + + when(mPackageManagerInternal.queryIntentActivities( + any(Intent.class), nullable(String.class), anyLong(), anyInt(), anyInt())) + .thenReturn(Collections.singletonList(resolveInfo)); + } + + private class TestInjector implements CrossProfileAppsServiceImpl.Injector { + private int mCallingUid; + private int mCallingUserId; + private int mCallingPid; + + public void setCallingUid(int uid) { + mCallingUid = uid; + } + + public void setCallingPid(int pid) { + mCallingPid = pid; + } + + public void setCallingUserId(int userId) { + mCallingUserId = userId; + } + + @Override + public int getCallingUid() { + return mCallingUid; + } + + @Override + public int getCallingPid() { + return mCallingPid; + } + + @Override + public int getCallingUserId() { + return mCallingUserId; + } + + @Override + public UserHandle getCallingUserHandle() { + return UserHandle.of(mCallingUserId); + } + + @Override + public long clearCallingIdentity() { + return 0; + } + + @Override + public void restoreCallingIdentity(long token) { + } + + @Override + public void withCleanCallingIdentity(ThrowingRunnable action) { + action.run(); + } + + @Override + public T withCleanCallingIdentity(ThrowingSupplier action) { + return action.get(); + } + + @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 mActivityManagerInternal; + } + + @Override + 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); + } + + @Override + public void killUid(int uid) { + PermissionManagerService.killUid( + UserHandle.getAppId(uid), + UserHandle.getUserId(uid), + PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED); + } + } +} \ No newline at end of file