Merge "Introduce CrossProfileApps API"
This commit is contained in:
committed by
Android (Google) Code Review
commit
3bba35111e
@@ -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 \
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user