Add 2 compat flags to strictly enable/disable scoped storage

* DEFAULT_SCOPED_STORAGE is the (soft) Scoped Storage policy that we have
by default on (with exceptions to opt out of Scoped Storage).

* FORCE_ENABLED_SCOPED_STORAGE is the (hard) Scoped Storage policy that
forces an app into Scoped Storage (regardless of any way to opt out)
and currently not applicable to any targetsdk.

Different flag combinations:

* Set both the flags to strictly enable scoped storage.

* Unset both the flags to stricly disable scoped storage.

* OP_LEGACY_STORAGE appop precedes any other combination of the flags.
This would be the initial state for all target sdk (as
FORCE_ENABLED_SCOPED_STORAGE is not set).

Bug: 132649864
Bug: 149924527

Test: atest RestrictedPermissionsTest
Test: atest RestrictedStoragePermissionTest
Test: atest RestrictedStoragePermissionSharedUidTest
Test: atest MediaProviderTests:com.android.providers.media.scan.ModernMediaScannerTest
Test: atest MediaProviderTests:com.android.providers.media.MediaProviderTest
Test: atest com.android.providers.media.MediaDocumentsProviderTest

Test: File APIs legacy vs non-legacy app behavior
      * Sample app targeting R and no `preserveLegacyExternalStorage` in Manifest
        is not able to create test.pdf in DCIM (due to scoped storage restrictions)
      * `adb shell am compat disable DEFAULT_SCOPED_STORAGE <pkg_name>`
      or turn the flag off from Settings UI
      Settings-> App Compatibility->(Select app)-> unset DEFAULT_SCOPED_STORAGE
      * Able to create test.pdf in DCIM (hence opt out of scoped storage)
      * Try different combinations of the flag to test other workflows

Test: MediaProvider APIs legacy vs non-legacy app behavior
      * Sample test app targeting R and no `preserveLegacyExternalStorage` in Manifest
      calls query() API for DCIM which does not show test.pdf
      * `adb shell am compat disable DEFAULT_SCOPED_STORAGE <pkg_name>`
      or turn the flag off from Settings UI
      * Sample test app targeting R calls query() API for DCIM as is
      able to read test.pdf file
      * Try different combinations of the flag to test other workflows

Merged-In: Iad2d361ad801ac2d2999d224f9bc06fbf6129619
Change-Id: Iad2d361ad801ac2d2999d224f9bc06fbf6129619
(cherry picked from commit f491007a9a0bef2792d2a2fc18df0fb0758e34b4)
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 1e98752..1632b47 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -36,6 +36,9 @@
 
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.content.ContentProvider;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -65,6 +68,42 @@
         this.featureId = featureId;
     }
 
+    /**
+     * Scoped Storage is on by default. However, it is not strictly enforced and there are multiple
+     * ways to opt out of scoped storage:
+     * <ul>
+     * <li>Target Sdk < Q</li>
+     * <li>Target Sdk = Q and has `requestLegacyExternalStorage` set in AndroidManifest.xml</li>
+     * <li>Target Sdk > Q: Upgrading from an app that was opted out of scoped storage and has
+     * `preserveLegacyExternalStorage` set in AndroidManifest.xml</li>
+     * </ul>
+     * This flag is enabled for all apps by default as Scoped Storage is enabled by default.
+     * Developers can disable this flag to opt out of Scoped Storage and have legacy storage
+     * workflow.
+     *
+     * Note: {@code FORCE_ENABLE_SCOPED_STORAGE} should also be disabled for apps to opt out of
+     * scoped storage.
+     * See https://developer.android.com/training/data-storage#scoped-storage for more information.
+     */
+    @ChangeId
+    private static final long DEFAULT_SCOPED_STORAGE = 149924527L;
+
+    /**
+     * Setting this flag strictly enforces Scoped Storage regardless of:
+     * <ul>
+     * <li>The value of Target Sdk</li>
+     * <li>The value of `requestLegacyExternalStorage` in AndroidManifest.xml</li>
+     * <li>The value of `preserveLegacyExternalStorage` in AndroidManifest.xml</li>
+     * </ul>
+     *
+     * Note: {@code DEFAULT_SCOPED_STORAGE} should also be enabled for apps to be enforced into
+     * scoped storage.
+     * See https://developer.android.com/training/data-storage#scoped-storage for more information.
+     */
+    @ChangeId
+    @Disabled
+    private static final long FORCE_ENABLE_SCOPED_STORAGE = 132649864L;
+
     public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider) {
         String callingPackage = provider.getCallingPackageUnchecked();
         if (callingPackage == null) {
@@ -251,9 +290,33 @@
     }
 
     private boolean isLegacyStorageGranted() {
+        boolean defaultScopedStorage = CompatChanges.isChangeEnabled(
+                DEFAULT_SCOPED_STORAGE, getPackageName(), UserHandle.getUserHandleForUid(uid));
+        boolean forceEnableScopedStorage = CompatChanges.isChangeEnabled(
+                FORCE_ENABLE_SCOPED_STORAGE, getPackageName(), UserHandle.getUserHandleForUid(uid));
+
+        // if Scoped Storage is strictly enforced, the app does *not* have legacy storage access
+        if (isScopedStorageEnforced(defaultScopedStorage, forceEnableScopedStorage)) {
+            return false;
+        }
+        // if Scoped Storage is strictly disabled, the app has legacy storage access
+        if (isScopedStorageDisabled(defaultScopedStorage, forceEnableScopedStorage)) {
+            return true;
+        }
+
         return checkIsLegacyStorageGranted(context, uid, getPackageName());
     }
 
+    private boolean isScopedStorageEnforced(boolean defaultScopedStorage,
+            boolean forceEnableScopedStorage) {
+        return defaultScopedStorage && forceEnableScopedStorage;
+    }
+
+    private boolean isScopedStorageDisabled(boolean defaultScopedStorage,
+            boolean forceEnableScopedStorage) {
+        return !defaultScopedStorage && !forceEnableScopedStorage;
+    }
+
     private boolean isLegacyWriteInternal() {
         return hasPermission(PERMISSION_IS_LEGACY_GRANTED) &&
                 checkPermissionWriteStorage(context, pid, uid, getPackageName());
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index a2cad24..69ff88c 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -73,9 +73,6 @@
 import android.app.PendingIntent;
 import android.app.RecoverableSecurityException;
 import android.app.RemoteAction;
-import android.app.compat.CompatChanges;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledAfter;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipDescription;
@@ -227,15 +224,6 @@
     private static final String PROP_FUSE = "persist.sys.fuse";
 
     /**
-     * App Compatibility flag that indicates whether the app is in scoped storage or not.
-     *
-     * SCOPED_STORAGE is the Change Name and 132649864 the Change ID. Using the buganizer issue ID
-     * as the Change ID for uniqueness as documented here go/compat-framework.
-     */
-    @ChangeId
-    private static final long SCOPED_STORAGE = 132649864L;
-
-    /**
      * These directory names aren't declared in Environment as final variables, and so we need to
      * have the same values in separate final variables in order to have them considered constant
      * expressions.
@@ -5109,21 +5097,9 @@
      * </ul>
      */
     private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) {
-        String packageName =  mCallingIdentity.get().getPackageName();
-        int uid = mCallingIdentity.get().uid;
-        boolean isScopedStorageCompatFlagEnabled;
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            isScopedStorageCompatFlagEnabled = CompatChanges.isChangeEnabled(SCOPED_STORAGE,
-                    packageName, UserHandle.getUserHandleForUid(uid));
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-
         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
                 : isCallingPackageLegacyRead();
-        if (isRequestingLegacyStorage || !isScopedStorageCompatFlagEnabled) {
+        if (isRequestingLegacyStorage) {
             return true;
         }
 
diff --git a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
index 84cc1ef..169c1bc 100644
--- a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
@@ -33,6 +34,8 @@
 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -40,6 +43,20 @@
 
 @RunWith(AndroidJUnit4.class)
 public class MediaDocumentsProviderTest {
+
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().dropShellPermissionIdentity();
+    }
+
     @Test
     public void testSimple() {
         final Context context = InstrumentationRegistry.getTargetContext();
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 7473603..7d7f489 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.Manifest;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -50,6 +51,8 @@
 import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.util.SQLiteQueryBuilder;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,6 +66,19 @@
 public class MediaProviderTest {
     static final String TAG = "MediaProviderTest";
 
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().dropShellPermissionIdentity();
+    }
+
     @Test
     public void testSchema() {
         final Context context = InstrumentationRegistry.getTargetContext();
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index c7adcd1..f1aa936 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -28,6 +28,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
@@ -69,6 +70,9 @@
     @Before
     public void setUp() {
         final Context context = InstrumentationRegistry.getTargetContext();
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
 
         mDir = new File(context.getExternalMediaDirs()[0], "test_" + System.nanoTime());
         mDir.mkdirs();
@@ -83,6 +87,8 @@
     @After
     public void tearDown() {
         FileUtils.deleteContents(mDir);
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().dropShellPermissionIdentity();
     }
 
     @Test