| /* |
| * 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; |
| import static android.Manifest.permission.CONFIGURE_INTERACT_ACROSS_PROFILES; |
| import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; |
| import static android.Manifest.permission.INTERACT_ACROSS_USERS; |
| import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; |
| import static android.Manifest.permission.MANAGE_APP_OPS_MODES; |
| import static android.app.AppOpsManager.OP_INTERACT_ACROSS_PROFILES; |
| import static android.content.Intent.FLAG_RECEIVER_REGISTERED_ONLY; |
| import static android.content.pm.CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED; |
| 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.ActivityManager; |
| import android.app.ActivityManagerInternal; |
| import android.app.ActivityOptions; |
| import android.app.AppGlobals; |
| import android.app.AppOpsManager; |
| import android.app.AppOpsManager.Mode; |
| import android.app.IApplicationThread; |
| import android.app.admin.DevicePolicyEventLogger; |
| import android.app.admin.DevicePolicyManagerInternal; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.PermissionChecker; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.CrossProfileAppsInternal; |
| import android.content.pm.ICrossProfileApps; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.ResolveInfo; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.permission.PermissionManager; |
| import android.stats.devicepolicy.DevicePolicyEnums; |
| import android.text.TextUtils; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.FunctionalUtils.ThrowingRunnable; |
| import com.android.internal.util.FunctionalUtils.ThrowingSupplier; |
| import com.android.server.LocalServices; |
| import com.android.server.pm.permission.PermissionManagerService; |
| import com.android.server.wm.ActivityTaskManagerInternal; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { |
| private static final String TAG = "CrossProfileAppsService"; |
| |
| private final LocalService mLocalService = new LocalService(); |
| |
| 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) { |
| Objects.requireNonNull(callingPackage); |
| |
| verifyCallingPackage(callingPackage); |
| |
| DevicePolicyEventLogger |
| .createEvent(DevicePolicyEnums.CROSS_PROFILE_APPS_GET_TARGET_USER_PROFILES) |
| .setStrings(new String[] {callingPackage}) |
| .write(); |
| |
| return getTargetUserProfilesUnchecked( |
| callingPackage, mInjector.getCallingUserId()); |
| } |
| |
| @Override |
| public void startActivityAsUser( |
| IApplicationThread caller, |
| String callingPackage, |
| String callingFeatureId, |
| ComponentName component, |
| @UserIdInt int userId, |
| boolean launchMainActivity) throws RemoteException { |
| Objects.requireNonNull(callingPackage); |
| Objects.requireNonNull(component); |
| |
| verifyCallingPackage(callingPackage); |
| |
| DevicePolicyEventLogger |
| .createEvent(DevicePolicyEnums.CROSS_PROFILE_APPS_START_ACTIVITY_AS_USER) |
| .setStrings(new String[] {callingPackage}) |
| .write(); |
| |
| final int callerUserId = mInjector.getCallingUserId(); |
| final int callingUid = mInjector.getCallingUid(); |
| final int callingPid = mInjector.getCallingPid(); |
| |
| List<UserHandle> allowedTargetUsers = getTargetUserProfilesUnchecked( |
| callingPackage, callerUserId); |
| if (!allowedTargetUsers.contains(UserHandle.of(userId))) { |
| throw new SecurityException(callingPackage + " cannot access unrelated user " + userId); |
| } |
| |
| // 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()); |
| } |
| |
| // Verify that target activity does handle the intent correctly. |
| final Intent launchIntent = new Intent(); |
| if (launchMainActivity) { |
| launchIntent.setAction(Intent.ACTION_MAIN); |
| launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); |
| 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()); |
| } else { |
| // If the main activity is not being launched and the users are different, the caller |
| // must have the required permission and the users must be in the same profile group |
| // in order to launch any of its own activities. |
| if (callerUserId != userId) { |
| final int permissionFlag = PermissionChecker.checkPermissionForPreflight( |
| mContext, |
| INTERACT_ACROSS_PROFILES, |
| callingPid, |
| callingUid, |
| callingPackage); |
| if (permissionFlag != PermissionChecker.PERMISSION_GRANTED |
| || !isSameProfileGroup(callerUserId, userId)) { |
| throw new SecurityException("Attempt to launch activity without required " |
| + INTERACT_ACROSS_PROFILES |
| + " permission or target user is not in the same profile group."); |
| } |
| } |
| launchIntent.setComponent(component); |
| } |
| verifyActivityCanHandleIntentAndExported(launchIntent, component, callingUid, userId); |
| |
| launchIntent.setPackage(null); |
| launchIntent.setComponent(component); |
| mInjector.getActivityTaskManagerInternal().startActivityAsUser( |
| caller, callingPackage, callingFeatureId, launchIntent, |
| /* resultTo= */ null, |
| Intent.FLAG_ACTIVITY_NEW_TASK, |
| launchMainActivity |
| ? ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle() |
| : null, |
| userId); |
| } |
| |
| @Override |
| public void startActivityAsUserByIntent( |
| IApplicationThread caller, |
| String callingPackage, |
| String callingFeatureId, |
| Intent intent, |
| @UserIdInt int userId, |
| IBinder callingActivity, |
| Bundle options) throws RemoteException { |
| Objects.requireNonNull(callingPackage); |
| Objects.requireNonNull(intent); |
| Objects.requireNonNull(intent.getComponent(), "The intent must have a Component set"); |
| |
| verifyCallingPackage(callingPackage); |
| |
| final int callerUserId = mInjector.getCallingUserId(); |
| final int callingUid = mInjector.getCallingUid(); |
| |
| List<UserHandle> allowedTargetUsers = getTargetUserProfilesUnchecked( |
| callingPackage, callerUserId); |
| if (callerUserId != userId && !allowedTargetUsers.contains(UserHandle.of(userId))) { |
| throw new SecurityException(callingPackage + " cannot access unrelated user " + userId); |
| } |
| |
| Intent launchIntent = new Intent(intent); |
| launchIntent.setPackage(callingPackage); |
| |
| if (!callingPackage.equals(launchIntent.getComponent().getPackageName())) { |
| throw new SecurityException( |
| callingPackage + " attempts to start an activity in other package - " |
| + launchIntent.getComponent().getPackageName()); |
| } |
| |
| if (callerUserId != userId) { |
| if (!hasCallerGotInteractAcrossProfilesPermission(callingPackage)) { |
| throw new SecurityException("Attempt to launch activity without required " |
| + INTERACT_ACROSS_PROFILES |
| + " permission or target user is not in the same profile group."); |
| } |
| } |
| |
| verifyActivityCanHandleIntent(launchIntent, callingUid, userId); |
| |
| mInjector.getActivityTaskManagerInternal() |
| .startActivityAsUser( |
| caller, |
| callingPackage, |
| callingFeatureId, |
| launchIntent, |
| callingActivity, |
| /* startFlags= */ 0, |
| options, |
| userId); |
| logStartActivityByIntent(callingPackage); |
| } |
| |
| private void logStartActivityByIntent(String packageName) { |
| DevicePolicyEventLogger |
| .createEvent(DevicePolicyEnums.START_ACTIVITY_BY_INTENT) |
| .setStrings(packageName) |
| .setBoolean(isCallingUserAManagedProfile()) |
| .write(); |
| } |
| |
| @Override |
| public boolean canRequestInteractAcrossProfiles(String callingPackage) { |
| Objects.requireNonNull(callingPackage); |
| verifyCallingPackage(callingPackage); |
| return canRequestInteractAcrossProfilesUnchecked(callingPackage); |
| } |
| |
| private boolean canRequestInteractAcrossProfilesUnchecked(String packageName) { |
| final int[] enabledProfileIds = |
| mInjector.getUserManager().getEnabledProfileIds(mInjector.getCallingUserId()); |
| if (enabledProfileIds.length < 2) { |
| return false; |
| } |
| if (isProfileOwner(packageName, enabledProfileIds)) { |
| return false; |
| } |
| return hasRequestedAppOpPermission( |
| AppOpsManager.opToPermission(OP_INTERACT_ACROSS_PROFILES), packageName); |
| } |
| |
| private boolean hasRequestedAppOpPermission(String permission, String packageName) { |
| try { |
| String[] packages = |
| mInjector.getIPackageManager().getAppOpPermissionPackages(permission); |
| return ArrayUtils.contains(packages, packageName); |
| } catch (RemoteException exc) { |
| Slog.e(TAG, "PackageManager dead. Cannot get permission info"); |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean canInteractAcrossProfiles(String callingPackage) { |
| Objects.requireNonNull(callingPackage); |
| verifyCallingPackage(callingPackage); |
| |
| final List<UserHandle> targetUserProfiles = getTargetUserProfilesUnchecked( |
| callingPackage, mInjector.getCallingUserId()); |
| if (targetUserProfiles.isEmpty()) { |
| return false; |
| } |
| return hasCallerGotInteractAcrossProfilesPermission(callingPackage); |
| } |
| |
| private boolean hasCallerGotInteractAcrossProfilesPermission(String callingPackage) { |
| return hasInteractAcrossProfilesPermission( |
| callingPackage, mInjector.getCallingUid(), mInjector.getCallingPid()); |
| } |
| |
| private boolean isCrossProfilePackageAllowlisted(String packageName) { |
| return mInjector.withCleanCallingIdentity(() -> |
| mInjector.getDevicePolicyManagerInternal() |
| .getAllCrossProfilePackages().contains(packageName)); |
| } |
| |
| private boolean isCrossProfilePackageAllowlistedByDefault(String packageName) { |
| return mInjector.withCleanCallingIdentity(() -> |
| mInjector.getDevicePolicyManagerInternal() |
| .getDefaultCrossProfilePackages().contains(packageName)); |
| } |
| |
| private List<UserHandle> getTargetUserProfilesUnchecked( |
| String packageName, @UserIdInt int userId) { |
| return mInjector.withCleanCallingIdentity(() -> { |
| final int[] enabledProfileIds = |
| mInjector.getUserManager().getEnabledProfileIds(userId); |
| |
| List<UserHandle> targetProfiles = new ArrayList<>(); |
| for (final int profileId : enabledProfileIds) { |
| if (profileId == userId) { |
| continue; |
| } |
| if (!isPackageEnabled(packageName, profileId)) { |
| continue; |
| } |
| targetProfiles.add(UserHandle.of(profileId)); |
| } |
| return targetProfiles; |
| }); |
| } |
| |
| private boolean isPackageEnabled(String packageName, @UserIdInt int userId) { |
| final int callingUid = mInjector.getCallingUid(); |
| return mInjector.withCleanCallingIdentity(() -> { |
| final PackageInfo info = mInjector.getPackageManagerInternal() |
| .getPackageInfo( |
| packageName, |
| MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, |
| callingUid, |
| userId); |
| return info != null && info.applicationInfo.enabled; |
| }); |
| } |
| |
| private void verifyActivityCanHandleIntent( |
| Intent launchIntent, int callingUid, @UserIdInt int userId) { |
| mInjector.withCleanCallingIdentity(() -> { |
| final List<ResolveInfo> activities = |
| mInjector.getPackageManagerInternal().queryIntentActivities( |
| launchIntent, |
| launchIntent.resolveTypeIfNeeded(mContext.getContentResolver()), |
| MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, |
| callingUid, |
| userId); |
| |
| if (!activities.isEmpty()) { |
| return; |
| } |
| throw new SecurityException("Activity cannot handle intent"); |
| }); |
| } |
| |
| /** |
| * 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, @UserIdInt int userId) { |
| mInjector.withCleanCallingIdentity(() -> { |
| final List<ResolveInfo> apps = |
| mInjector.getPackageManagerInternal().queryIntentActivities( |
| launchIntent, |
| launchIntent.resolveTypeIfNeeded(mContext.getContentResolver()), |
| MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, |
| callingUid, |
| userId); |
| 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); |
| }); |
| } |
| |
| /** |
| * See {@link android.content.pm.CrossProfileApps#setInteractAcrossProfilesAppOp(String, int)}. |
| * |
| * <p>Use {@link #setInteractAcrossProfilesAppOpUnchecked(String, int, int)} to avoid permission |
| * checks. |
| */ |
| @Override |
| public void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { |
| setInteractAcrossProfilesAppOp(packageName, newMode, mInjector.getCallingUserId()); |
| } |
| |
| private void setInteractAcrossProfilesAppOp( |
| String packageName, @Mode int newMode, @UserIdInt int userId) { |
| final int callingUid = mInjector.getCallingUid(); |
| if (!isPermissionGranted(INTERACT_ACROSS_USERS_FULL, callingUid) |
| && !isPermissionGranted(INTERACT_ACROSS_USERS, callingUid)) { |
| throw new SecurityException( |
| "INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL is required to set the" |
| + " app-op for interacting across profiles."); |
| } |
| if (!isPermissionGranted(MANAGE_APP_OPS_MODES, callingUid) |
| && !isPermissionGranted(CONFIGURE_INTERACT_ACROSS_PROFILES, callingUid)) { |
| throw new SecurityException( |
| "MANAGE_APP_OPS_MODES or CONFIGURE_INTERACT_ACROSS_PROFILES is required to set" |
| + " the app-op for interacting across profiles."); |
| } |
| setInteractAcrossProfilesAppOpUnchecked(packageName, newMode, userId); |
| } |
| |
| private void setInteractAcrossProfilesAppOpUnchecked( |
| String packageName, @Mode int newMode, @UserIdInt int userId) { |
| if (newMode == AppOpsManager.MODE_ALLOWED |
| && !canConfigureInteractAcrossProfiles(packageName, userId)) { |
| // The user should not be prompted for apps that cannot request to interact across |
| // profiles. However, we return early here if required to avoid race conditions. |
| Slog.e(TAG, "Tried to turn on the appop for interacting across profiles for invalid" |
| + " app " + packageName); |
| return; |
| } |
| final int[] profileIds = |
| mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false); |
| for (int profileId : profileIds) { |
| if (!isPackageInstalled(packageName, profileId)) { |
| continue; |
| } |
| // Only log once per profile group by checking against the user ID. |
| setInteractAcrossProfilesAppOpForProfile( |
| packageName, newMode, profileId, /* logMetrics= */ profileId == userId); |
| } |
| } |
| |
| /** |
| * Returns whether the given package name is installed in the given user ID. The calling UID is |
| * used as the filter calling UID, as described at {@link PackageManagerInternal#getPackageInfo( |
| * String, int, int, int)}. |
| */ |
| private boolean isPackageInstalled(String packageName, @UserIdInt int userId) { |
| return mInjector.withCleanCallingIdentity(() -> { |
| final int flags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; |
| final PackageInfo info = |
| mInjector.getPackageManagerInternal() |
| .getPackageInfo(packageName, flags, mInjector.getCallingUid(), userId); |
| return info != null; |
| }); |
| } |
| |
| private void setInteractAcrossProfilesAppOpForProfile( |
| String packageName, @Mode int newMode, @UserIdInt int profileId, boolean logMetrics) { |
| try { |
| setInteractAcrossProfilesAppOpForProfileOrThrow( |
| packageName, newMode, profileId, logMetrics); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, "Missing package " + packageName + " on profile user ID " + profileId, e); |
| } |
| } |
| |
| private void setInteractAcrossProfilesAppOpForProfileOrThrow( |
| String packageName, @Mode int newMode, @UserIdInt int profileId, boolean logMetrics) |
| throws PackageManager.NameNotFoundException { |
| final int uid = mInjector.getPackageManager() |
| .getPackageUidAsUser(packageName, /* flags= */ 0, profileId); |
| if (currentModeEquals(newMode, packageName, uid)) { |
| Slog.i(TAG, "Attempt to set mode to existing value of " + newMode + " for " |
| + packageName + " on profile user ID " + profileId); |
| return; |
| } |
| |
| final boolean hadPermission = hasInteractAcrossProfilesPermission( |
| packageName, uid, PermissionChecker.PID_UNKNOWN); |
| |
| if (isPermissionGranted(CONFIGURE_INTERACT_ACROSS_PROFILES, mInjector.getCallingUid())) { |
| // Clear calling identity since the CONFIGURE_INTERACT_ACROSS_PROFILES permission allows |
| // this particular app-op to be modified without the broader app-op permissions. |
| mInjector.withCleanCallingIdentity(() -> |
| mInjector.getAppOpsManager() |
| .setMode(OP_INTERACT_ACROSS_PROFILES, uid, packageName, newMode)); |
| } else { |
| mInjector.getAppOpsManager() |
| .setMode(OP_INTERACT_ACROSS_PROFILES, uid, packageName, newMode); |
| } |
| // Kill the UID before sending the broadcast to ensure that apps can be informed when |
| // their app-op has been revoked. |
| maybeKillUid(packageName, uid, hadPermission); |
| sendCanInteractAcrossProfilesChangedBroadcast(packageName, uid, UserHandle.of(profileId)); |
| maybeLogSetInteractAcrossProfilesAppOp(packageName, newMode, logMetrics, uid); |
| } |
| |
| /** |
| * Kills the process represented by the given UID if it has lost the permission to |
| * interact across profiles. |
| */ |
| private void maybeKillUid(String packageName, int uid, boolean hadPermission) { |
| if (!hadPermission) { |
| return; |
| } |
| if (hasInteractAcrossProfilesPermission(packageName, uid, PermissionChecker.PID_UNKNOWN)) { |
| return; |
| } |
| mInjector.killUid(uid); |
| } |
| |
| private void maybeLogSetInteractAcrossProfilesAppOp( |
| String packageName, @Mode int newMode, boolean logMetrics, int uid) { |
| if (!logMetrics) { |
| return; |
| } |
| DevicePolicyEventLogger |
| .createEvent(DevicePolicyEnums.SET_INTERACT_ACROSS_PROFILES_APP_OP) |
| .setStrings(packageName) |
| .setInt(newMode) |
| .setBoolean(appDeclaresCrossProfileAttribute(uid)) |
| .write(); |
| } |
| |
| /** |
| * Returns whether the given app-op mode is equivalent to the currently-set app-op of the given |
| * package name and UID. Clears identity to avoid permission checks, so ensure the caller does |
| * any necessary permission checks. |
| */ |
| private boolean currentModeEquals(@Mode int otherMode, String packageName, int uid) { |
| final String op = AppOpsManager.permissionToOp(INTERACT_ACROSS_PROFILES); |
| return mInjector.withCleanCallingIdentity(() -> otherMode |
| == mInjector.getAppOpsManager().unsafeCheckOpNoThrow(op, uid, packageName)); |
| } |
| |
| private void sendCanInteractAcrossProfilesChangedBroadcast( |
| String packageName, int uid, UserHandle userHandle) { |
| final Intent intent = |
| new Intent(ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED).setPackage(packageName); |
| if (appDeclaresCrossProfileAttribute(uid)) { |
| intent.addFlags( |
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND | Intent.FLAG_RECEIVER_FOREGROUND); |
| } else { |
| intent.addFlags(FLAG_RECEIVER_REGISTERED_ONLY); |
| } |
| for (ResolveInfo receiver : findBroadcastReceiversForUser(intent, userHandle)) { |
| intent.setComponent(receiver.getComponentInfo().getComponentName()); |
| mInjector.sendBroadcastAsUser(intent, userHandle); |
| } |
| } |
| |
| private List<ResolveInfo> findBroadcastReceiversForUser(Intent intent, UserHandle userHandle) { |
| return mInjector.getPackageManager() |
| .queryBroadcastReceiversAsUser(intent, /* flags= */ 0, userHandle); |
| } |
| |
| private boolean appDeclaresCrossProfileAttribute(int uid) { |
| return mInjector.getPackageManagerInternal().getPackage(uid).isCrossProfile(); |
| } |
| |
| @Override |
| public boolean canConfigureInteractAcrossProfiles(String packageName) { |
| return canConfigureInteractAcrossProfiles(packageName, mInjector.getCallingUserId()); |
| } |
| |
| private boolean canConfigureInteractAcrossProfiles(String packageName, @UserIdInt int userId) { |
| if (!canUserAttemptToConfigureInteractAcrossProfiles(packageName, userId)) { |
| return false; |
| } |
| if (!hasOtherProfileWithPackageInstalled(packageName, userId)) { |
| return false; |
| } |
| if (!hasRequestedAppOpPermission( |
| AppOpsManager.opToPermission(OP_INTERACT_ACROSS_PROFILES), packageName)) { |
| return false; |
| } |
| return isCrossProfilePackageAllowlisted(packageName); |
| } |
| |
| @Override |
| public boolean canUserAttemptToConfigureInteractAcrossProfiles(String packageName) { |
| return canUserAttemptToConfigureInteractAcrossProfiles( |
| packageName, mInjector.getCallingUserId()); |
| } |
| |
| private boolean canUserAttemptToConfigureInteractAcrossProfiles( |
| String packageName, @UserIdInt int userId) { |
| final int[] profileIds = |
| mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false); |
| if (profileIds.length < 2) { |
| return false; |
| } |
| if (isProfileOwner(packageName, profileIds)) { |
| return false; |
| } |
| if (!hasRequestedAppOpPermission( |
| AppOpsManager.opToPermission(OP_INTERACT_ACROSS_PROFILES), packageName)) { |
| return false; |
| } |
| return !isPlatformSignedAppWithNonUserConfigurablePermission(packageName, profileIds); |
| } |
| |
| private boolean isPlatformSignedAppWithNonUserConfigurablePermission( |
| String packageName, int[] profileIds) { |
| return !isCrossProfilePackageAllowlistedByDefault(packageName) |
| && isPlatformSignedAppWithAutomaticProfilesPermission(packageName, profileIds); |
| } |
| |
| /** |
| * Only platform-signed apps can be granted INTERACT_ACROSS_PROFILES automatically without user |
| * consent. |
| * |
| * Returns true if the app is automatically granted the permission in at least one profile. |
| */ |
| private boolean isPlatformSignedAppWithAutomaticProfilesPermission( |
| String packageName, int[] profileIds) { |
| for (int userId : profileIds) { |
| final int uid = mInjector.getPackageManagerInternal().getPackageUidInternal( |
| packageName, /* flags= */ 0, userId); |
| if (uid == -1) { |
| continue; |
| } |
| if (isPermissionGranted(INTERACT_ACROSS_PROFILES, uid)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean hasOtherProfileWithPackageInstalled(String packageName, @UserIdInt int userId) { |
| return mInjector.withCleanCallingIdentity(() -> { |
| final int[] profileIds = |
| mInjector.getUserManager().getProfileIds(userId, /* enabledOnly= */ false); |
| for (int profileId : profileIds) { |
| if (profileId != userId && isPackageInstalled(packageName, profileId)) { |
| return true; |
| } |
| } |
| return false; |
| }); |
| } |
| |
| @Override |
| public void resetInteractAcrossProfilesAppOps(List<String> packageNames) { |
| packageNames.forEach(this::resetInteractAcrossProfilesAppOp); |
| } |
| |
| private void resetInteractAcrossProfilesAppOp(String packageName) { |
| if (canConfigureInteractAcrossProfiles(packageName)) { |
| Slog.w(TAG, "Not resetting app-op for package " + packageName |
| + " since it is still configurable by users."); |
| return; |
| } |
| final String op = |
| AppOpsManager.permissionToOp(INTERACT_ACROSS_PROFILES); |
| setInteractAcrossProfilesAppOp(packageName, AppOpsManager.opToDefaultMode(op)); |
| } |
| |
| @Override |
| public void clearInteractAcrossProfilesAppOps() { |
| final int defaultMode = |
| AppOpsManager.opToDefaultMode( |
| AppOpsManager.permissionToOp(INTERACT_ACROSS_PROFILES)); |
| findAllPackageNames() |
| .forEach(packageName -> setInteractAcrossProfilesAppOp(packageName, defaultMode)); |
| } |
| |
| private List<String> findAllPackageNames() { |
| return mInjector.getPackageManagerInternal() |
| .getInstalledApplications( |
| /* flags= */ 0, mInjector.getCallingUserId(), mInjector.getCallingUid()) |
| .stream() |
| .map(applicationInfo -> applicationInfo.packageName) |
| .collect(Collectors.toList()); |
| } |
| |
| CrossProfileAppsInternal getLocalService() { |
| return mLocalService; |
| } |
| |
| private boolean isSameProfileGroup(@UserIdInt int callerUserId, @UserIdInt int userId) { |
| return mInjector.withCleanCallingIdentity(() -> |
| mInjector.getUserManager().isSameProfileGroup(callerUserId, userId)); |
| } |
| |
| /** |
| * Verify that the given calling package is belong to the calling UID. |
| */ |
| private void verifyCallingPackage(String callingPackage) { |
| mInjector.getAppOpsManager().checkPackage(mInjector.getCallingUid(), callingPackage); |
| } |
| |
| private boolean isPermissionGranted(String permission, int uid) { |
| return PackageManager.PERMISSION_GRANTED == mInjector.checkComponentPermission( |
| permission, uid, /* owningUid= */-1, /* exported= */ true); |
| } |
| |
| private boolean isCallingUserAManagedProfile() { |
| return isManagedProfile(mInjector.getCallingUserId()); |
| } |
| |
| private boolean isManagedProfile(@UserIdInt int userId) { |
| return mInjector.withCleanCallingIdentity(() |
| -> mContext.getSystemService(UserManager.class).isManagedProfile(userId)); |
| } |
| |
| private boolean hasInteractAcrossProfilesPermission(String packageName, int uid, int pid) { |
| if (isPermissionGranted(INTERACT_ACROSS_USERS_FULL, uid) |
| || isPermissionGranted(INTERACT_ACROSS_USERS, uid)) { |
| return true; |
| } |
| return PermissionChecker.PERMISSION_GRANTED |
| == PermissionChecker.checkPermissionForPreflight( |
| mContext, INTERACT_ACROSS_PROFILES, pid, uid, packageName); |
| } |
| |
| private boolean isProfileOwner(String packageName, int[] userIds) { |
| for (int userId : userIds) { |
| if (isProfileOwner(packageName, userId)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isProfileOwner(String packageName, final @UserIdInt int userId) { |
| final ComponentName profileOwner = |
| mInjector.withCleanCallingIdentity(() -> |
| mInjector.getDevicePolicyManagerInternal().getProfileOwnerAsUser(userId)); |
| if (profileOwner == null) { |
| return false; |
| } |
| return profileOwner.getPackageName().equals(packageName); |
| } |
| |
| private static class InjectorImpl implements Injector { |
| private Context mContext; |
| |
| public InjectorImpl(Context context) { |
| mContext = context; |
| } |
| |
| @Override |
| public int getCallingUid() { |
| return Binder.getCallingUid(); |
| } |
| |
| @Override |
| public int getCallingPid() { |
| return Binder.getCallingPid(); |
| } |
| |
| @Override |
| public int getCallingUserId() { |
| return UserHandle.getCallingUserId(); |
| } |
| |
| @Override |
| public UserHandle getCallingUserHandle() { |
| return Binder.getCallingUserHandle(); |
| } |
| |
| @Override |
| public long clearCallingIdentity() { |
| return Binder.clearCallingIdentity(); |
| } |
| |
| @Override |
| public void restoreCallingIdentity(long token) { |
| Binder.restoreCallingIdentity(token); |
| } |
| |
| @Override |
| public void withCleanCallingIdentity(ThrowingRunnable action) { |
| Binder.withCleanCallingIdentity(action); |
| } |
| |
| @Override |
| public final <T> T withCleanCallingIdentity(ThrowingSupplier<T> action) { |
| return Binder.withCleanCallingIdentity(action); |
| } |
| |
| @Override |
| public UserManager getUserManager() { |
| return mContext.getSystemService(UserManager.class); |
| } |
| |
| @Override |
| public PackageManagerInternal getPackageManagerInternal() { |
| return LocalServices.getService(PackageManagerInternal.class); |
| } |
| |
| @Override |
| public PackageManager getPackageManager() { |
| return mContext.getPackageManager(); |
| } |
| |
| @Override |
| public AppOpsManager getAppOpsManager() { |
| return mContext.getSystemService(AppOpsManager.class); |
| } |
| |
| @Override |
| public ActivityManagerInternal getActivityManagerInternal() { |
| return LocalServices.getService(ActivityManagerInternal.class); |
| } |
| |
| @Override |
| public ActivityTaskManagerInternal getActivityTaskManagerInternal() { |
| return LocalServices.getService(ActivityTaskManagerInternal.class); |
| } |
| |
| @Override |
| public IPackageManager getIPackageManager() { |
| return AppGlobals.getPackageManager(); |
| } |
| |
| @Override |
| public DevicePolicyManagerInternal getDevicePolicyManagerInternal() { |
| return LocalServices.getService(DevicePolicyManagerInternal.class); |
| } |
| |
| @Override |
| public void sendBroadcastAsUser(Intent intent, UserHandle user) { |
| mContext.sendBroadcastAsUser(intent, user); |
| } |
| |
| @Override |
| public int checkComponentPermission( |
| String permission, int uid, int owningUid, boolean exported) { |
| return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported); |
| } |
| |
| @Override |
| public void killUid(int uid) { |
| PermissionManagerService.killUid( |
| UserHandle.getAppId(uid), |
| UserHandle.getUserId(uid), |
| PermissionManager.KILL_APP_REASON_PERMISSIONS_REVOKED); |
| } |
| } |
| |
| @VisibleForTesting |
| public interface Injector { |
| int getCallingUid(); |
| |
| int getCallingPid(); |
| |
| int getCallingUserId(); |
| |
| UserHandle getCallingUserHandle(); |
| |
| long clearCallingIdentity(); |
| |
| void restoreCallingIdentity(long token); |
| |
| void withCleanCallingIdentity(ThrowingRunnable action); |
| |
| <T> T withCleanCallingIdentity(ThrowingSupplier<T> action); |
| |
| UserManager getUserManager(); |
| |
| PackageManagerInternal getPackageManagerInternal(); |
| |
| PackageManager getPackageManager(); |
| |
| AppOpsManager getAppOpsManager(); |
| |
| ActivityManagerInternal getActivityManagerInternal(); |
| |
| ActivityTaskManagerInternal getActivityTaskManagerInternal(); |
| |
| IPackageManager getIPackageManager(); |
| |
| DevicePolicyManagerInternal getDevicePolicyManagerInternal(); |
| |
| void sendBroadcastAsUser(Intent intent, UserHandle user); |
| |
| int checkComponentPermission(String permission, int uid, int owningUid, boolean exported); |
| |
| void killUid(int uid); |
| } |
| |
| class LocalService extends CrossProfileAppsInternal { |
| |
| @Override |
| public boolean verifyPackageHasInteractAcrossProfilePermission( |
| String packageName, @UserIdInt int userId) |
| throws PackageManager.NameNotFoundException { |
| final int uid = Objects.requireNonNull( |
| mInjector.getPackageManager().getApplicationInfoAsUser( |
| Objects.requireNonNull(packageName), /* flags= */ 0, userId)).uid; |
| return verifyUidHasInteractAcrossProfilePermission(packageName, uid); |
| } |
| |
| @Override |
| public boolean verifyUidHasInteractAcrossProfilePermission(String packageName, int uid) { |
| Objects.requireNonNull(packageName); |
| return hasInteractAcrossProfilesPermission( |
| packageName, uid, PermissionChecker.PID_UNKNOWN); |
| } |
| |
| @Override |
| public List<UserHandle> getTargetUserProfiles(String packageName, int userId) { |
| return getTargetUserProfilesUnchecked(packageName, userId); |
| } |
| |
| @Override |
| public void setInteractAcrossProfilesAppOp( |
| String packageName, int newMode, @UserIdInt int userId) { |
| CrossProfileAppsServiceImpl.this.setInteractAcrossProfilesAppOpUnchecked( |
| packageName, newMode, userId); |
| } |
| } |
| } |