diff options
-rw-r--r-- | Android.mk | 1 | ||||
-rw-r--r-- | api/current.txt | 10 | ||||
-rw-r--r-- | api/system-current.txt | 10 | ||||
-rw-r--r-- | api/test-current.txt | 10 | ||||
-rw-r--r-- | core/java/android/app/SystemServiceRegistry.java | 14 | ||||
-rw-r--r-- | core/java/android/content/Context.java | 8 | ||||
-rw-r--r-- | core/java/android/content/pm/crossprofile/CrossProfileApps.java | 90 | ||||
-rw-r--r-- | core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl | 31 | ||||
-rw-r--r-- | services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsService.java | 34 | ||||
-rw-r--r-- | services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java | 264 | ||||
-rw-r--r-- | services/java/com/android/server/SystemServer.java | 5 | ||||
-rw-r--r-- | services/tests/servicestests/Android.mk | 3 | ||||
-rw-r--r-- | services/tests/servicestests/src/com/android/server/pm/crossprofile/CrossProfileAppsServiceImplTest.java | 449 |
13 files changed, 928 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk index 62f750c41a8b..b69adf9e67c2 100644 --- a/Android.mk +++ b/Android.mk @@ -158,6 +158,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 \ diff --git a/api/current.txt b/api/current.txt index 509bb88424a4..243e3f5d48a2 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9100,6 +9100,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"; @@ -11221,6 +11222,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 { diff --git a/api/system-current.txt b/api/system-current.txt index c0c2311e5e3e..3fc189951f0a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9619,6 +9619,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"; @@ -11951,6 +11952,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 { diff --git a/api/test-current.txt b/api/test-current.txt index 909614460b87..17a87a2c7a35 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -9174,6 +9174,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"; @@ -11307,6 +11308,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 { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 50f1f364b9e5..be7193fd1bd3 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -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; @@ -909,6 +911,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)); + } + }); } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c165fb3e925c..accf6245a3b8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4071,6 +4071,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. * diff --git a/core/java/android/content/pm/crossprofile/CrossProfileApps.java b/core/java/android/content/pm/crossprofile/CrossProfileApps.java new file mode 100644 index 000000000000..c441b5f376a0 --- /dev/null +++ b/core/java/android/content/pm/crossprofile/CrossProfileApps.java @@ -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(); + } + } +} diff --git a/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl b/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl new file mode 100644 index 000000000000..dd8d04f6cf0e --- /dev/null +++ b/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl @@ -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); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsService.java b/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsService.java new file mode 100644 index 000000000000..0913269f35e1 --- /dev/null +++ b/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsService.java @@ -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); + } +} diff --git a/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java new file mode 100644 index 000000000000..854b70439d56 --- /dev/null +++ b/services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java @@ -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(); + + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index de5d879a05af..5e12849c6fa4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -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; @@ -1517,6 +1518,10 @@ public final class SystemServer { traceBeginAndSlog("StartLauncherAppsService"); mSystemServiceManager.startService(LauncherAppsService.class); traceEnd(); + + traceBeginAndSlog("StartCrossProfileAppsService"); + mSystemServiceManager.startService(CrossProfileAppsService.class); + traceEnd(); } if (!disableNonCoreServices && !disableMediaProjection) { diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index 8e41a554b9e3..a2ec23496a03 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -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 diff --git a/services/tests/servicestests/src/com/android/server/pm/crossprofile/CrossProfileAppsServiceImplTest.java b/services/tests/servicestests/src/com/android/server/pm/crossprofile/CrossProfileAppsServiceImplTest.java new file mode 100644 index 000000000000..880b77ed81da --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/crossprofile/CrossProfileAppsServiceImplTest.java @@ -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; + } + } +} |