Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d3773546 authored by Bram Bonné's avatar Bram Bonné
Browse files

Update storage permission policy to use AppCompat APIs.

This CL introduces ENABLE_SCOPED_STORAGE and REQUIRE_SCOPED_STORAGE as
AppCompat changes that are enabled for apps with targetSDK > P and apps
with targetSDK > Q respectively.

This change removes the getMinimumTargetSDK method, as it
is no longer needed.

Change-Id: I6b05252d42d3b2f1030928ffd03c02523c5a01d2
Fix: 147034231
Bug: 144914977
Bug: 131432978
Test: atest RestrictedPermissionsTest
Test: atest RestrictedStoragePermissionSharedUidTest
parent 6a1e0b72
Loading
Loading
Loading
Loading
+105 −49
Original line number Diff line number Diff line
@@ -27,16 +27,22 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYST
import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;

import static java.lang.Integer.min;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.compat.IPlatformCompat;

/**
 * The behavior of soft restricted permissions is different for each permission. This class collects
@@ -46,6 +52,27 @@ import android.os.UserHandle;
 * {@link com.android.packageinstaller.permission.utils.SoftRestrictedPermissionPolicy}
 */
public abstract class SoftRestrictedPermissionPolicy {
    /**
     * Enables scoped storage, with exceptions for apps that explicitly request legacy access, or
     * apps that hold the {@code android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
     * See https://developer.android.com/training/data-storage#scoped-storage for more information.
     */
    @ChangeId
    // This change is enabled for apps with targetSDK > {@link android.os.Build.VERSION_CODES.P}
    @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.P)
    static final long ENABLE_SCOPED_STORAGE = 144914977L;

    /**
     * Enforces scoped storage for all apps, preventing individual apps from opting out. This change
     * has precedence over {@code ENABLE_SCOPED_STORAGE}.
     */
    @ChangeId
    // This change is enabled for apps with targetSDK > {@link android.os.Build.VERSION_CODES.Q}.
    @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q)
    static final long REQUIRE_SCOPED_STORAGE = 131432978L;

    private static final String LOG_TAG = SoftRestrictedPermissionPolicy.class.getSimpleName();

    private static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT =
            FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
                    | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
@@ -59,41 +86,6 @@ public abstract class SoftRestrictedPermissionPolicy {
                }
            };

    /**
     * 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.
     *
@@ -114,27 +106,31 @@ public abstract class SoftRestrictedPermissionPolicy {
            case READ_EXTERNAL_STORAGE: {
                final boolean isWhiteListed;
                boolean shouldApplyRestriction;
                final int targetSDK;
                final boolean hasRequestedLegacyExternalStorage;
                final boolean hasWriteMediaStorageGrantedForUid;
                final boolean isScopedStorageEnabled;

                if (appInfo != null) {
                    PackageManager pm = context.getPackageManager();
                    int flags = pm.getPermissionFlags(permission, appInfo.packageName, user);
                    isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
                    targetSDK = getMinimumTargetSDK(context, appInfo, user);
                    shouldApplyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0
                            || targetSDK > Build.VERSION_CODES.Q;
                    hasRequestedLegacyExternalStorage = hasUidRequestedLegacyExternalStorage(
                            appInfo.uid, context);
                    hasWriteMediaStorageGrantedForUid = hasWriteMediaStorageGrantedForUid(
                            appInfo.uid, context);
                    final boolean isScopedStorageRequired =
                            isChangeEnabledForUid(context, appInfo, user, REQUIRE_SCOPED_STORAGE);
                    isScopedStorageEnabled =
                            isChangeEnabledForUid(context, appInfo, user, ENABLE_SCOPED_STORAGE)
                            || isScopedStorageRequired;
                    shouldApplyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0
                            || isScopedStorageRequired;
                } else {
                    isWhiteListed = false;
                    shouldApplyRestriction = false;
                    targetSDK = 0;
                    hasRequestedLegacyExternalStorage = false;
                    hasWriteMediaStorageGrantedForUid = false;
                    isScopedStorageEnabled = false;
                }

                // We have a check in PermissionPolicyService.PermissionToOpSynchroniser.setUidMode
@@ -144,7 +140,7 @@ public abstract class SoftRestrictedPermissionPolicy {
                return new SoftRestrictedPermissionPolicy() {
                    @Override
                    public boolean mayGrantPermission() {
                        return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
                        return isWhiteListed || isScopedStorageEnabled;
                    }
                    @Override
                    public int getExtraAppOpCode() {
@@ -152,7 +148,7 @@ public abstract class SoftRestrictedPermissionPolicy {
                    }
                    @Override
                    public boolean mayAllowExtraAppOp() {
                        return !shouldApplyRestriction && targetSDK <= Build.VERSION_CODES.Q
                        return !shouldApplyRestriction
                                && (hasRequestedLegacyExternalStorage
                                        || hasWriteMediaStorageGrantedForUid);
                    }
@@ -164,22 +160,26 @@ public abstract class SoftRestrictedPermissionPolicy {
            }
            case WRITE_EXTERNAL_STORAGE: {
                final boolean isWhiteListed;
                final int targetSDK;
                final boolean isScopedStorageEnabled;

                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);
                    final boolean isScopedStorageRequired =
                            isChangeEnabledForUid(context, appInfo, user, REQUIRE_SCOPED_STORAGE);
                    isScopedStorageEnabled =
                            isChangeEnabledForUid(context, appInfo, user, ENABLE_SCOPED_STORAGE)
                            || isScopedStorageRequired;
                } else {
                    isWhiteListed = false;
                    targetSDK = 0;
                    isScopedStorageEnabled = false;
                }

                return new SoftRestrictedPermissionPolicy() {
                    @Override
                    public boolean mayGrantPermission() {
                        return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
                        return isWhiteListed || isScopedStorageEnabled;
                    }
                };
            }
@@ -188,6 +188,62 @@ public abstract class SoftRestrictedPermissionPolicy {
        }
    }

    /**
     * Checks whether an AppCompat change is enabled for all packages sharing a UID with the
     * provided application.
     *
     * @param context A context to use.
     * @param appInfo The application for which to check whether the compat change is enabled.
     * @param user The user the app belongs to.
     * @param changeId A {@link android.compat.annotation.ChangeId} corresponding to the change.
     *
     * @return true if this change is enabled for all apps sharing the UID of the provided app,
     *         false otherwise.
     */
    private static boolean isChangeEnabledForUid(@NonNull Context context,
            @NonNull ApplicationInfo appInfo, @NonNull UserHandle user, long changeId) {
        PackageManager pm = context.getPackageManager();

        String[] uidPackages = pm.getPackagesForUid(appInfo.uid);
        if (uidPackages != null) {
            for (String uidPackage : uidPackages) {
                ApplicationInfo uidPackageInfo;
                try {
                    uidPackageInfo = pm.getApplicationInfoAsUser(uidPackage, 0, user);
                } catch (PackageManager.NameNotFoundException e) {
                    continue;
                }
                if (!isChangeEnabled(uidPackageInfo, changeId)) {
                    // At least one package sharing this UID does not have this change enabled.
                    return false;
                }
            }
            // All packages sharing this UID returned true for {@link #isChangeEnabled()}.
            return true;
        } else {
            Log.w(LOG_TAG, "Check for change " + changeId + " for uid " + appInfo.uid
                    + " produced no packages. Defaulting to using the information for "
                    + appInfo.packageName + " only.");
            return isChangeEnabled(appInfo, changeId);
        }
    }

    private static boolean isChangeEnabled(@NonNull ApplicationInfo appInfo, long changeId) {
        IBinder binder = ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE);
        IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(binder);

        final long callingId = Binder.clearCallingIdentity();

        try {
            return platformCompat.isChangeEnabled(changeId, appInfo);
        } catch (RemoteException e) {
            Log.e(LOG_TAG, "Check for change " + changeId + " failed. Defaulting to enabled.", e);
            return true;
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }

    private static boolean hasUidRequestedLegacyExternalStorage(int uid, @NonNull Context context) {
        PackageManager packageManager = context.getPackageManager();
        String[] packageNames = packageManager.getPackagesForUid(uid);