Merge "Introduce CrossProfileApps API"

This commit is contained in:
TreeHugger Robot
2017-11-13 06:43:44 +00:00
committed by Android (Google) Code Review
13 changed files with 928 additions and 1 deletions

View File

@@ -159,6 +159,7 @@ LOCAL_SRC_FILES += \
core/java/android/content/ISyncServiceAdapter.aidl \
core/java/android/content/ISyncStatusObserver.aidl \
core/java/android/content/om/IOverlayManager.aidl \
core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl \
core/java/android/content/pm/IDexModuleRegisterCallback.aidl \
core/java/android/content/pm/ILauncherApps.aidl \
core/java/android/content/pm/IOnAppsChangedListener.aidl \

View File

@@ -9106,6 +9106,7 @@ package android.content {
field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2
field public static final int CONTEXT_INCLUDE_CODE = 1; // 0x1
field public static final int CONTEXT_RESTRICTED = 4; // 0x4
field public static final java.lang.String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
field public static final java.lang.String DEVICE_POLICY_SERVICE = "device_policy";
field public static final java.lang.String DISPLAY_SERVICE = "display";
field public static final java.lang.String DOWNLOAD_SERVICE = "download";
@@ -11227,6 +11228,15 @@ package android.content.pm {
}
package android.content.pm.crossprofile {
public class CrossProfileApps {
method public java.util.List<android.os.UserHandle> getTargetUserProfiles();
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
}
}
package android.content.res {
public class AssetFileDescriptor implements java.io.Closeable android.os.Parcelable {

View File

@@ -9627,6 +9627,7 @@ package android.content {
field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2
field public static final int CONTEXT_INCLUDE_CODE = 1; // 0x1
field public static final int CONTEXT_RESTRICTED = 4; // 0x4
field public static final java.lang.String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
field public static final java.lang.String DEVICE_POLICY_SERVICE = "device_policy";
field public static final java.lang.String DISPLAY_SERVICE = "display";
field public static final java.lang.String DOWNLOAD_SERVICE = "download";
@@ -11960,6 +11961,15 @@ package android.content.pm {
}
package android.content.pm.crossprofile {
public class CrossProfileApps {
method public java.util.List<android.os.UserHandle> getTargetUserProfiles();
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
}
}
package android.content.pm.permission {
public final class RuntimePermissionPresentationInfo implements android.os.Parcelable {

View File

@@ -9183,6 +9183,7 @@ package android.content {
field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2
field public static final int CONTEXT_INCLUDE_CODE = 1; // 0x1
field public static final int CONTEXT_RESTRICTED = 4; // 0x4
field public static final java.lang.String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
field public static final java.lang.String DEVICE_POLICY_SERVICE = "device_policy";
field public static final java.lang.String DISPLAY_SERVICE = "display";
field public static final java.lang.String DOWNLOAD_SERVICE = "download";
@@ -11316,6 +11317,15 @@ package android.content.pm {
}
package android.content.pm.crossprofile {
public class CrossProfileApps {
method public java.util.List<android.os.UserHandle> getTargetUserProfiles();
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
}
}
package android.content.res {
public class AssetFileDescriptor implements java.io.Closeable android.os.Parcelable {

View File

@@ -41,6 +41,8 @@ import android.content.pm.IShortcutService;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
import android.content.pm.crossprofile.CrossProfileApps;
import android.content.pm.crossprofile.ICrossProfileApps;
import android.content.res.Resources;
import android.hardware.ConsumerIrManager;
import android.hardware.ISerialManager;
@@ -922,6 +924,18 @@ final class SystemServiceRegistry {
public RulesManager createService(ContextImpl ctx) {
return new RulesManager(ctx.getOuterContext());
}});
registerService(Context.CROSS_PROFILE_APPS_SERVICE, CrossProfileApps.class,
new CachedServiceFetcher<CrossProfileApps>() {
@Override
public CrossProfileApps createService(ContextImpl ctx)
throws ServiceNotFoundException {
IBinder b = ServiceManager.getServiceOrThrow(
Context.CROSS_PROFILE_APPS_SERVICE);
return new CrossProfileApps(ctx.getOuterContext(),
ICrossProfileApps.Stub.asInterface(b));
}
});
}
/**

View File

@@ -4072,6 +4072,14 @@ public abstract class Context {
*/
public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone";
/**
* Use with {@link #getSystemService} to retrieve a
* {@link android.content.pm.crossprofile.CrossProfileApps} for cross profile operations.
*
* @see #getSystemService
*/
public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
/**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2017 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 android.content.pm.crossprofile;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import java.util.List;
/**
* Class for handling cross profile operations. Apps can use this class to interact with its
* instance in any profile that is in {@link #getTargetUserProfiles()}. For example, app can
* use this class to start its main activity in managed profile.
*/
public class CrossProfileApps {
private final Context mContext;
private final ICrossProfileApps mService;
/** @hide */
public CrossProfileApps(Context context, ICrossProfileApps service) {
mContext = context;
mService = service;
}
/**
* Starts the specified main activity of the caller package in the specified profile.
*
* @param component The ComponentName of the activity to launch, it must be exported and has
* action {@link android.content.Intent#ACTION_MAIN}, category
* {@link android.content.Intent#CATEGORY_LAUNCHER}. Otherwise, SecurityException will
* be thrown.
* @param user The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
* @param sourceBounds The Rect containing the source bounds of the clicked icon, see
* {@link android.content.Intent#setSourceBounds(Rect)}.
* @param startActivityOptions Options to pass to startActivity
*/
public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user,
@Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) {
try {
mService.startActivityAsUser(mContext.getPackageName(),
component, sourceBounds, startActivityOptions, user);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Return a list of user profiles that that the caller can use when calling other APIs in this
* class.
* <p>
* A user profile would be considered as a valid target user profile, provided that:
* <ul>
* <li>It gets caller app installed</li>
* <li>It is not equal to the calling user</li>
* <li>It is in the same profile group of calling user profile</li>
* <li>It is enabled</li>
* </ul>
*
* @see UserManager#getUserProfiles()
*/
public @NonNull List<UserHandle> getTargetUserProfiles() {
try {
return mService.getTargetUserProfiles(mContext.getPackageName());
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2017 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 android.content.pm.crossprofile;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
/**
* @hide
*/
interface ICrossProfileApps {
void startActivityAsUser(in String callingPackage, in ComponentName component, in Rect sourceBounds, in Bundle startActivityOptions, in UserHandle user);
List<UserHandle> getTargetUserProfiles(in String callingPackage);
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 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.crossprofile;
import android.content.Context;
import com.android.server.SystemService;
public class CrossProfileAppsService extends SystemService {
private CrossProfileAppsServiceImpl mServiceImpl;
public CrossProfileAppsService(Context context) {
super(context);
mServiceImpl = new CrossProfileAppsServiceImpl(context);
}
@Override
public void onStart() {
publishBinderService(Context.CROSS_PROFILE_APPS_SERVICE, mServiceImpl);
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2017 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.crossprofile;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.crossprofile.ICrossProfileApps;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.List;
public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub {
private static final String TAG = "CrossProfileAppsService";
private Context mContext;
private Injector mInjector;
public CrossProfileAppsServiceImpl(Context context) {
this(context, new InjectorImpl(context));
}
@VisibleForTesting
CrossProfileAppsServiceImpl(Context context, Injector injector) {
mContext = context;
mInjector = injector;
}
@Override
public List<UserHandle> getTargetUserProfiles(String callingPackage) {
Preconditions.checkNotNull(callingPackage);
verifyCallingPackage(callingPackage);
return getTargetUserProfilesUnchecked(
callingPackage, mInjector.getCallingUserId());
}
@Override
public void startActivityAsUser(
String callingPackage,
ComponentName component,
Rect sourceBounds,
Bundle startActivityOptions,
UserHandle user) throws RemoteException {
Preconditions.checkNotNull(callingPackage);
Preconditions.checkNotNull(component);
Preconditions.checkNotNull(user);
verifyCallingPackage(callingPackage);
List<UserHandle> allowedTargetUsers = getTargetUserProfilesUnchecked(
callingPackage, mInjector.getCallingUserId());
if (!allowedTargetUsers.contains(user)) {
throw new SecurityException(
callingPackage + " cannot access unrelated user " + user.getIdentifier());
}
// Verify that caller package is starting activity in its own package.
if (!callingPackage.equals(component.getPackageName())) {
throw new SecurityException(
callingPackage + " attempts to start an activity in other package - "
+ component.getPackageName());
}
final int callingUid = mInjector.getCallingUid();
// Verify that target activity does handle the intent with ACTION_MAIN and
// CATEGORY_LAUNCHER as calling startActivityAsUser ignore them if component is present.
final Intent launchIntent = new Intent(Intent.ACTION_MAIN);
launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
launchIntent.setSourceBounds(sourceBounds);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
// Only package name is set here, as opposed to component name, because intent action and
// category are ignored if component name is present while we are resolving intent.
launchIntent.setPackage(component.getPackageName());
verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, user);
final long ident = mInjector.clearCallingIdentity();
try {
launchIntent.setComponent(component);
mContext.startActivityAsUser(launchIntent, startActivityOptions, user);
} finally {
mInjector.restoreCallingIdentity(ident);
}
}
private List<UserHandle> getTargetUserProfilesUnchecked(
String callingPackage, @UserIdInt int callingUserId) {
final long ident = mInjector.clearCallingIdentity();
try {
final int[] enabledProfileIds =
mInjector.getUserManager().getEnabledProfileIds(callingUserId);
List<UserHandle> targetProfiles = new ArrayList<>();
for (final int userId : enabledProfileIds) {
if (userId == callingUserId) {
continue;
}
if (!isPackageEnabled(callingPackage, userId)) {
continue;
}
targetProfiles.add(UserHandle.of(userId));
}
return targetProfiles;
} finally {
mInjector.restoreCallingIdentity(ident);
}
}
private boolean isPackageEnabled(String packageName, @UserIdInt int userId) {
final int callingUid = mInjector.getCallingUid();
final long ident = mInjector.clearCallingIdentity();
try {
final PackageInfo info = mInjector.getPackageManagerInternal()
.getPackageInfo(
packageName,
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
callingUid,
userId);
return info != null && info.applicationInfo.enabled;
} finally {
mInjector.restoreCallingIdentity(ident);
}
}
/**
* Verify that the specified intent does resolved to the specified component and the resolved
* activity is exported.
*/
private void verifyActivityCanHandleIntentAndExported(
Intent launchIntent, ComponentName component, int callingUid, UserHandle user) {
final long ident = mInjector.clearCallingIdentity();
try {
final List<ResolveInfo> apps =
mInjector.getPackageManagerInternal().queryIntentActivities(
launchIntent,
MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
callingUid,
user.getIdentifier());
final int size = apps.size();
for (int i = 0; i < size; ++i) {
final ActivityInfo activityInfo = apps.get(i).activityInfo;
if (TextUtils.equals(activityInfo.packageName, component.getPackageName())
&& TextUtils.equals(activityInfo.name, component.getClassName())
&& activityInfo.exported) {
return;
}
}
throw new SecurityException("Attempt to launch activity without "
+ " category Intent.CATEGORY_LAUNCHER or activity is not exported" + component);
} finally {
mInjector.restoreCallingIdentity(ident);
}
}
/**
* Verify that the given calling package is belong to the calling UID.
*/
private void verifyCallingPackage(String callingPackage) {
mInjector.getAppOpsManager().checkPackage(mInjector.getCallingUid(), callingPackage);
}
private static class InjectorImpl implements Injector {
private Context mContext;
public InjectorImpl(Context context) {
mContext = context;
}
public int getCallingUid() {
return Binder.getCallingUid();
}
public int getCallingUserId() {
return UserHandle.getCallingUserId();
}
public UserHandle getCallingUserHandle() {
return Binder.getCallingUserHandle();
}
public long clearCallingIdentity() {
return Binder.clearCallingIdentity();
}
public void restoreCallingIdentity(long token) {
Binder.restoreCallingIdentity(token);
}
public UserManager getUserManager() {
return mContext.getSystemService(UserManager.class);
}
public PackageManagerInternal getPackageManagerInternal() {
return LocalServices.getService(PackageManagerInternal.class);
}
public PackageManager getPackageManager() {
return mContext.getPackageManager();
}
public AppOpsManager getAppOpsManager() {
return mContext.getSystemService(AppOpsManager.class);
}
}
@VisibleForTesting
public interface Injector {
int getCallingUid();
int getCallingUserId();
UserHandle getCallingUserHandle();
long clearCallingIdentity();
void restoreCallingIdentity(long token);
UserManager getUserManager();
PackageManagerInternal getPackageManagerInternal();
PackageManager getPackageManager();
AppOpsManager getAppOpsManager();
}
}

View File

@@ -95,6 +95,7 @@ import com.android.server.pm.OtaDexoptService;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.ShortcutService;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.crossprofile.CrossProfileAppsService;
import com.android.server.policy.PhoneWindowManager;
import com.android.server.power.PowerManagerService;
import com.android.server.power.ShutdownThread;
@@ -1489,6 +1490,10 @@ public final class SystemServer {
traceBeginAndSlog("StartLauncherAppsService");
mSystemServiceManager.startService(LauncherAppsService.class);
traceEnd();
traceBeginAndSlog("StartCrossProfileAppsService");
mSystemServiceManager.startService(CrossProfileAppsService.class);
traceEnd();
}
if (!disableMediaProjection) {

View File

@@ -25,7 +25,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
mockito-target-minus-junit4 \
platform-test-annotations \
ShortcutManagerTestUtils \
truth-prebuilt
truth-prebuilt \
testng
LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/aidl

View File

@@ -0,0 +1,449 @@
package com.android.server.pm.crossprofile;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
import android.app.AppOpsManager;
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.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.util.SparseArray;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Build/Install/Run:
* bit FrameworksServicesTests:com.android.server.pm.crossprofile.CrossProfileAppsServiceImplTest
*/
@Presubmit
@RunWith(MockitoJUnitRunner.class)
public class CrossProfileAppsServiceImplTest {
private static final String PACKAGE_ONE = "com.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;
private TestInjector mTestInjector;
private ActivityInfo mActivityInfo;
private CrossProfileAppsServiceImpl mCrossProfileAppsServiceImpl;
private SparseArray<Boolean> mUserEnabled = new SparseArray<>();
@Before
public void initCrossProfileAppsServiceImpl() {
mTestInjector = new TestInjector();
mCrossProfileAppsServiceImpl = new CrossProfileAppsServiceImpl(mContext, mTestInjector);
}
@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<Integer> 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<UserHandle> 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<UserHandle> targetProfiles =
mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
assertThat(targetProfiles).isEmpty();
}
@Test
public void getTargetUserProfiles_fromPrimaryUser_userNotEnabled() throws Exception {
mUserEnabled.put(PROFILE_OF_PRIMARY_USER, false);
List<UserHandle> targetProfiles =
mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
assertThat(targetProfiles).isEmpty();
}
@Test
public void getTargetUserProfiles_fromSecondaryUser() throws Exception {
mTestInjector.setCallingUserId(SECONDARY_USER);
List<UserHandle> targetProfiles =
mCrossProfileAppsServiceImpl.getTargetUserProfiles(PACKAGE_ONE);
assertThat(targetProfiles).isEmpty();
}
@Test
public void getTargetUserProfiles_fromProfile_installed() throws Exception {
mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
List<UserHandle> 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<UserHandle> 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(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(PRIMARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_profile_successWithOption() throws Exception {
Bundle options = Bundle.forPair("test_key", "test_value");
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
options,
UserHandle.of(PROFILE_OF_PRIMARY_USER));
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
verify(mContext)
.startActivityAsUser(
intentCaptor.capture(),
bundleCaptor.capture(),
eq(UserHandle.of(PROFILE_OF_PRIMARY_USER)));
Intent intent = intentCaptor.getValue();
assertEquals(ACTIVITY_COMPONENT, intent.getComponent());
Bundle bundle = bundleCaptor.getValue();
assertEquals("test_value", bundle.getString("test_key"));
}
@Test
public void startActivityAsUser_profile_notInstalled() throws Exception {
mockAppsInstalled(PACKAGE_ONE, PROFILE_OF_PRIMARY_USER, false);
assertThrows(
SecurityException.class,
() ->
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(PROFILE_OF_PRIMARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_profile_fakeCaller() throws Exception {
assertThrows(
SecurityException.class,
() ->
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_TWO,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(PROFILE_OF_PRIMARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_profile_notExported() throws Exception {
mActivityInfo.exported = false;
assertThrows(
SecurityException.class,
() ->
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(PROFILE_OF_PRIMARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_profile_anotherPackage() throws Exception {
assertThrows(
SecurityException.class,
() ->
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
new ComponentName(PACKAGE_TWO, "test"),
null,
null,
UserHandle.of(PROFILE_OF_PRIMARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_secondaryUser() throws Exception {
assertThrows(
SecurityException.class,
() ->
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(SECONDARY_USER)));
verify(mContext, never())
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
any(UserHandle.class));
}
@Test
public void startActivityAsUser_fromProfile_success() throws Exception {
mTestInjector.setCallingUserId(PROFILE_OF_PRIMARY_USER);
mCrossProfileAppsServiceImpl.startActivityAsUser(
PACKAGE_ONE,
ACTIVITY_COMPONENT,
null,
null,
UserHandle.of(PRIMARY_USER));
verify(mContext)
.startActivityAsUser(
any(Intent.class),
nullable(Bundle.class),
eq(UserHandle.of(PRIMARY_USER)));
}
private void mockAppsInstalled(String packageName, int user, boolean installed) {
when(mPackageManagerInternal.getPackageInfo(
eq(packageName),
anyInt(),
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), anyInt(), anyInt(), anyInt()))
.thenReturn(Collections.singletonList(resolveInfo));
}
private class TestInjector implements CrossProfileAppsServiceImpl.Injector {
private int mCallingUid;
private int mCallingUserId;
public void setCallingUid(int uid) {
mCallingUid = uid;
}
public void setCallingUserId(int userId) {
mCallingUserId = userId;
}
@Override
public int getCallingUid() {
return mCallingUid;
}
@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 UserManager getUserManager() {
return mUserManager;
}
@Override
public PackageManagerInternal getPackageManagerInternal() {
return mPackageManagerInternal;
}
@Override
public PackageManager getPackageManager() {
return mPackageManager;
}
@Override
public AppOpsManager getAppOpsManager() {
return mAppOpsManager;
}
}
}