summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk1
-rw-r--r--api/current.txt10
-rw-r--r--api/system-current.txt10
-rw-r--r--api/test-current.txt10
-rw-r--r--core/java/android/app/SystemServiceRegistry.java14
-rw-r--r--core/java/android/content/Context.java8
-rw-r--r--core/java/android/content/pm/crossprofile/CrossProfileApps.java90
-rw-r--r--core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl31
-rw-r--r--services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsService.java34
-rw-r--r--services/core/java/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java264
-rw-r--r--services/java/com/android/server/SystemServer.java5
-rw-r--r--services/tests/servicestests/Android.mk3
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/crossprofile/CrossProfileAppsServiceImplTest.java449
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;
+ }
+ }
+}