/* * Copyright (C) 2019 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.policy; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_LEGACY_STORAGE; import static android.app.AppOpsManager.OP_NONE; import static android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT; import static java.lang.Integer.min; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.UserHandle; /** * The behavior of soft restricted permissions is different for each permission. This class collects * the policies in one place. * * This is the twin of * {@link com.android.packageinstaller.permission.utils.SoftRestrictedPermissionPolicy} */ public abstract class SoftRestrictedPermissionPolicy { private static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT = FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT | FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; private static final SoftRestrictedPermissionPolicy DUMMY_POLICY = new SoftRestrictedPermissionPolicy() { @Override public int resolveAppOp() { return OP_NONE; } @Override public int getDesiredOpMode() { return MODE_DEFAULT; } @Override public boolean shouldSetAppOpIfNotDefault() { return false; } @Override public boolean canBeGranted() { return true; } }; /** * TargetSDK is per package. To make sure two apps int the same shared UID do not fight over * what to set, always compute the combined targetSDK. * * @param context A context * @param appInfo The app that is changed * @param user The user the app belongs to * * @return The minimum targetSDK of all apps sharing the uid of the app */ private static int getMinimumTargetSDK(@NonNull Context context, @NonNull ApplicationInfo appInfo, @NonNull UserHandle user) { PackageManager pm = context.getPackageManager(); int minimumTargetSDK = appInfo.targetSdkVersion; String[] uidPkgs = pm.getPackagesForUid(appInfo.uid); if (uidPkgs != null) { for (String uidPkg : uidPkgs) { if (!uidPkg.equals(appInfo.packageName)) { ApplicationInfo uidPkgInfo; try { uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user); } catch (PackageManager.NameNotFoundException e) { continue; } minimumTargetSDK = min(minimumTargetSDK, uidPkgInfo.targetSdkVersion); } } } return minimumTargetSDK; } /** * Get the policy for a soft restricted permission. * * @param context A context to use * @param appInfo The application the permission belongs to. Can be {@code null}, but then * only {@link #resolveAppOp} will work. * @param user The user the app belongs to. Can be {@code null}, but then only * {@link #resolveAppOp} will work. * @param permission The name of the permission * * @return The policy for this permission */ public static @NonNull SoftRestrictedPermissionPolicy forPermission(@NonNull Context context, @Nullable ApplicationInfo appInfo, @Nullable UserHandle user, @NonNull String permission) { switch (permission) { // Storage uses a special app op to decide the mount state and supports soft restriction // where the restricted state allows the permission but only for accessing the medial // collections. case READ_EXTERNAL_STORAGE: { final int flags; final boolean applyRestriction; final boolean isWhiteListed; final boolean hasRequestedLegacyExternalStorage; final int targetSDK; if (appInfo != null) { PackageManager pm = context.getPackageManager(); flags = pm.getPermissionFlags(permission, appInfo.packageName, user); applyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0; isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0; targetSDK = getMinimumTargetSDK(context, appInfo, user); boolean hasAnyRequestedLegacyExternalStorage = appInfo.hasRequestedLegacyExternalStorage(); // hasRequestedLegacyExternalStorage is per package. To make sure two apps in // the same shared UID do not fight over what to set, always compute the // combined hasRequestedLegacyExternalStorage String[] uidPkgs = pm.getPackagesForUid(appInfo.uid); if (uidPkgs != null) { for (String uidPkg : uidPkgs) { if (!uidPkg.equals(appInfo.packageName)) { ApplicationInfo uidPkgInfo; try { uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user); } catch (PackageManager.NameNotFoundException e) { continue; } hasAnyRequestedLegacyExternalStorage |= uidPkgInfo.hasRequestedLegacyExternalStorage(); } } } hasRequestedLegacyExternalStorage = hasAnyRequestedLegacyExternalStorage; } else { flags = 0; applyRestriction = false; isWhiteListed = false; hasRequestedLegacyExternalStorage = false; targetSDK = 0; } return new SoftRestrictedPermissionPolicy() { @Override public int resolveAppOp() { return OP_LEGACY_STORAGE; } @Override public int getDesiredOpMode() { if (applyRestriction) { return MODE_DEFAULT; } else if (hasRequestedLegacyExternalStorage) { return MODE_ALLOWED; } else { return MODE_IGNORED; } } @Override public boolean shouldSetAppOpIfNotDefault() { // Do not switch from allowed -> ignored as this would mean to retroactively // turn on isolated storage. This will make the app loose all its files. return getDesiredOpMode() != MODE_IGNORED; } @Override public boolean canBeGranted() { if (isWhiteListed || targetSDK >= Build.VERSION_CODES.Q) { return true; } else { return false; } } }; } case WRITE_EXTERNAL_STORAGE: { final boolean isWhiteListed; final int targetSDK; if (appInfo != null) { final int flags = context.getPackageManager().getPermissionFlags(permission, appInfo.packageName, user); isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0; targetSDK = getMinimumTargetSDK(context, appInfo, user); } else { isWhiteListed = false; targetSDK = 0; } return new SoftRestrictedPermissionPolicy() { @Override public int resolveAppOp() { return OP_NONE; } @Override public int getDesiredOpMode() { return MODE_DEFAULT; } @Override public boolean shouldSetAppOpIfNotDefault() { return false; } @Override public boolean canBeGranted() { return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q; } }; } default: return DUMMY_POLICY; } } /** * @return An app op to be changed based on the state of the permission or * {@link AppOpsManager#OP_NONE} if not app-op should be set. */ public abstract int resolveAppOp(); /** * @return The mode the {@link #resolveAppOp() app op} should be in. */ public abstract @AppOpsManager.Mode int getDesiredOpMode(); /** * @return If the {@link #resolveAppOp() app op} should be set even if the app-op is currently * not {@link AppOpsManager#MODE_DEFAULT}. */ public abstract boolean shouldSetAppOpIfNotDefault(); /** * @return If the permission can be granted */ public abstract boolean canBeGranted(); }