Merge "Update storage permission policy to use AppCompat APIs."
diff --git a/services/core/java/com/android/server/policy/SoftRestrictedPermissionPolicy.java b/services/core/java/com/android/server/policy/SoftRestrictedPermissionPolicy.java
index d754380..a83c58d 100644
--- a/services/core/java/com/android/server/policy/SoftRestrictedPermissionPolicy.java
+++ b/services/core/java/com/android/server/policy/SoftRestrictedPermissionPolicy.java
@@ -27,16 +27,22 @@
 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 @@
  * {@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
@@ -60,41 +87,6 @@
             };
 
     /**
-     * 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
@@ -114,27 +106,31 @@
             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 @@
                 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 @@
                     }
                     @Override
                     public boolean mayAllowExtraAppOp() {
-                        return !shouldApplyRestriction && targetSDK <= Build.VERSION_CODES.Q
+                        return !shouldApplyRestriction
                                 && (hasRequestedLegacyExternalStorage
                                         || hasWriteMediaStorageGrantedForUid);
                     }
@@ -164,22 +160,26 @@
             }
             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 @@
         }
     }
 
+    /**
+     * 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);