Allow prefix-based Uri permission grants.

Define new FLAG_GRANT_PREFIX_URI_PERMISSION which indicates that a
Uri permission grant should also apply to any other Uris that have
matching scheme, authority, and path segments.  For example, a prefix
grant for /foo/ would allow /foo/bar/ but not /foo2/.

Allow persistable and prefix grants to be issued directly through
grantUriPermission().  Relaxing persistable is fine, since it still
requires the receiver to actively take the permission.

Since exact- and prefix-match grants for the same Uri can coexist,
we track them separately using a new UriGrant key.  (Consider the
case where an app separately extends READ|PREFIX and WRITE for
the same Uri: we can't let that become READ|WRITE|PREFIX.)

Fix revoke to always take away persisted permissions.  Move prefix
matching logic to Uri and add tests.  Add new flags to "am" tool, and
various internal uses around Intent and Context.  Switch some lagging
users to ArraySet.

Bug: 10607375
Change-Id: Ia8ce2b88421ff9f2fe5a979a27a026fc445d46f1
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index f1ce54a..fe532bf 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1865,17 +1865,26 @@
     }
 
     private String uriModeFlagToString(int uriModeFlags) {
-        switch (uriModeFlags) {
-            case Intent.FLAG_GRANT_READ_URI_PERMISSION |
-                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION:
-                return "read and write";
-            case Intent.FLAG_GRANT_READ_URI_PERMISSION:
-                return "read";
-            case Intent.FLAG_GRANT_WRITE_URI_PERMISSION:
-                return "write";
+        StringBuilder builder = new StringBuilder();
+        if ((uriModeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
+            builder.append("read and ");
         }
-        throw new IllegalArgumentException(
-                "Unknown permission mode flags: " + uriModeFlags);
+        if ((uriModeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+            builder.append("write and ");
+        }
+        if ((uriModeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0) {
+            builder.append("persistable and ");
+        }
+        if ((uriModeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) != 0) {
+            builder.append("prefix and ");
+        }
+
+        if (builder.length() > 5) {
+            builder.setLength(builder.length() - 5);
+            return builder.toString();
+        } else {
+            throw new IllegalArgumentException("Unknown permission mode flags: " + uriModeFlags);
+        }
     }
 
     private void enforceForUri(
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index f3c803d..5b41394 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -1641,7 +1641,7 @@
      *
      * @see #getPersistedUriPermissions()
      */
-    public void takePersistableUriPermission(Uri uri, int modeFlags) {
+    public void takePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) {
         try {
             ActivityManagerNative.getDefault().takePersistableUriPermission(uri, modeFlags);
         } catch (RemoteException e) {
@@ -1656,7 +1656,7 @@
      *
      * @see #getPersistedUriPermissions()
      */
-    public void releasePersistableUriPermission(Uri uri, int modeFlags) {
+    public void releasePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) {
         try {
             ActivityManagerNative.getDefault().releasePersistableUriPermission(uri, modeFlags);
         } catch (RemoteException e) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 906484a..cbb6cf5 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2791,9 +2791,13 @@
      * @param uri The Uri you would like to grant access to.
      * @param modeFlags The desired access modes.  Any combination of
      * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
-     * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+     * Intent.FLAG_GRANT_READ_URI_PERMISSION},
      * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
-     * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+     * Intent.FLAG_GRANT_WRITE_URI_PERMISSION},
+     * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+     * Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION}, or
+     * {@link Intent#FLAG_GRANT_PREFIX_URI_PERMISSION
+     * Intent.FLAG_GRANT_PREFIX_URI_PERMISSION}.
      *
      * @see #revokeUriPermission
      */
@@ -2806,7 +2810,8 @@
      * Uri will match all previously granted Uris that are the same or a
      * sub-path of the given Uri.  That is, revoking "content://foo/target" will
      * revoke both "content://foo/target" and "content://foo/target/sub", but not
-     * "content://foo".
+     * "content://foo".  It will not remove any prefix grants that exist at a
+     * higher level.
      *
      * @param uri The Uri you would like to revoke access to.
      * @param modeFlags The desired access modes.  Any combination of
@@ -2817,7 +2822,7 @@
      *
      * @see #grantUriPermission
      */
-    public abstract void revokeUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags);
+    public abstract void revokeUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
 
     /**
      * Determine whether a particular process and user ID has been granted
@@ -2841,7 +2846,7 @@
      * @see #checkCallingUriPermission
      */
     public abstract int checkUriPermission(Uri uri, int pid, int uid,
-            @Intent.GrantUriMode int modeFlags);
+            @Intent.AccessUriMode int modeFlags);
 
     /**
      * Determine whether the calling process and user ID has been
@@ -2864,7 +2869,7 @@
      *
      * @see #checkUriPermission(Uri, int, int, int)
      */
-    public abstract int checkCallingUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags);
+    public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
 
     /**
      * Determine whether the calling process of an IPC <em>or you</em> has been granted
@@ -2884,7 +2889,7 @@
      * @see #checkCallingUriPermission
      */
     public abstract int checkCallingOrSelfUriPermission(Uri uri,
-            @Intent.GrantUriMode int modeFlags);
+            @Intent.AccessUriMode int modeFlags);
 
     /**
      * Check both a Uri and normal permission.  This allows you to perform
@@ -2910,7 +2915,7 @@
      */
     public abstract int checkUriPermission(@Nullable Uri uri, @Nullable String readPermission,
             @Nullable String writePermission, int pid, int uid,
-            @Intent.GrantUriMode int modeFlags);
+            @Intent.AccessUriMode int modeFlags);
 
     /**
      * If a particular process and user ID has not been granted
@@ -2932,7 +2937,7 @@
      * @see #checkUriPermission(Uri, int, int, int)
      */
     public abstract void enforceUriPermission(
-            Uri uri, int pid, int uid, @Intent.GrantUriMode int modeFlags, String message);
+            Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags, String message);
 
     /**
      * If the calling process and user ID has not been granted
@@ -2954,7 +2959,7 @@
      * @see #checkCallingUriPermission(Uri, int)
      */
     public abstract void enforceCallingUriPermission(
-            Uri uri, @Intent.GrantUriMode int modeFlags, String message);
+            Uri uri, @Intent.AccessUriMode int modeFlags, String message);
 
     /**
      * If the calling process of an IPC <em>or you</em> has not been
@@ -2973,7 +2978,7 @@
      * @see #checkCallingOrSelfUriPermission(Uri, int)
      */
     public abstract void enforceCallingOrSelfUriPermission(
-            Uri uri, @Intent.GrantUriMode int modeFlags, String message);
+            Uri uri, @Intent.AccessUriMode int modeFlags, String message);
 
     /**
      * Enforce both a Uri and normal permission.  This allows you to perform
@@ -2998,7 +3003,7 @@
      */
     public abstract void enforceUriPermission(
             @Nullable Uri uri, @Nullable String readPermission,
-            @Nullable String writePermission, int pid, int uid, @Intent.GrantUriMode int modeFlags,
+            @Nullable String writePermission, int pid, int uid, @Intent.AccessUriMode int modeFlags,
             @Nullable String message);
 
     /** @hide */
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 421956b..67b6737 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -18,6 +18,7 @@
 
 import android.content.pm.ApplicationInfo;
 import android.util.ArraySet;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -864,8 +865,9 @@
         }
 
         // Migrate any clip data and flags from target.
-        int permFlags = target.getFlags()
-                & (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION);
+        int permFlags = target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION
+                | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+                | FLAG_GRANT_PREFIX_URI_PERMISSION);
         if (permFlags != 0) {
             ClipData targetClipData = target.getClipData();
             if (targetClipData == null && target.getData() != null) {
@@ -3425,11 +3427,29 @@
     // Intent flags (see mFlags variable).
 
     /** @hide */
-    @IntDef(flag = true,
-            value = {FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION})
+    @IntDef(flag = true, value = {
+            FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION,
+            FLAG_GRANT_PERSISTABLE_URI_PERMISSION, FLAG_GRANT_PREFIX_URI_PERMISSION })
     @Retention(RetentionPolicy.SOURCE)
     public @interface GrantUriMode {}
 
+    /** @hide */
+    @IntDef(flag = true, value = {
+            FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AccessUriMode {}
+
+    /**
+     * Test if given mode flags specify an access mode, which must be at least
+     * read and/or write.
+     *
+     * @hide
+     */
+    public static boolean isAccessUriMode(int modeFlags) {
+        return (modeFlags & (Intent.FLAG_GRANT_READ_URI_PERMISSION
+                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) != 0;
+    }
+
     /**
      * If set, the recipient of this Intent will be granted permission to
      * perform read operations on the URI in the Intent's data and any URIs
@@ -3491,6 +3511,17 @@
     public static final int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 0x00000040;
 
     /**
+     * When combined with {@link #FLAG_GRANT_READ_URI_PERMISSION} and/or
+     * {@link #FLAG_GRANT_WRITE_URI_PERMISSION}, the URI permission grant
+     * applies to any URI that is a prefix match against the original granted
+     * URI. (Without this flag, the URI must match exactly for access to be
+     * granted.) Another URI is considered a prefix match only when scheme,
+     * authority, and all path segments defined by the prefix are an exact
+     * match.
+     */
+    public static final int FLAG_GRANT_PREFIX_URI_PERMISSION = 0x00000080;
+
+    /**
      * If set, the new activity is not kept in the history stack.  As soon as
      * the user navigates away from it, the activity is finished.  This may also
      * be set with the {@link android.R.styleable#AndroidManifestActivity_noHistory
@@ -3810,9 +3841,9 @@
     /**
      * @hide Flags that can't be changed with PendingIntent.
      */
-    public static final int IMMUTABLE_FLAGS =
-            FLAG_GRANT_READ_URI_PERMISSION
-            | FLAG_GRANT_WRITE_URI_PERMISSION;
+    public static final int IMMUTABLE_FLAGS = FLAG_GRANT_READ_URI_PERMISSION
+            | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+            | FLAG_GRANT_PREFIX_URI_PERMISSION;
 
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
@@ -6350,6 +6381,8 @@
      *
      * @see #FLAG_GRANT_READ_URI_PERMISSION
      * @see #FLAG_GRANT_WRITE_URI_PERMISSION
+     * @see #FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+     * @see #FLAG_GRANT_PREFIX_URI_PERMISSION
      * @see #FLAG_DEBUG_LOG_RESOLUTION
      * @see #FLAG_FROM_BACKGROUND
      * @see #FLAG_ACTIVITY_BROUGHT_TO_FRONT
@@ -7381,9 +7414,10 @@
                     // Since we migrated in child, we need to promote ClipData
                     // and flags to ourselves to grant.
                     setClipData(target.getClipData());
-                    addFlags(target.getFlags()
-                            & (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION
-                                    | FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
+                    addFlags(target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION
+                            | FLAG_GRANT_WRITE_URI_PERMISSION
+                            | FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+                            | FLAG_GRANT_PREFIX_URI_PERMISSION));
                     return true;
                 } else {
                     return false;
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
index a7a8a0a..ce70455 100644
--- a/core/java/android/net/Uri.java
+++ b/core/java/android/net/Uri.java
@@ -21,6 +21,7 @@
 import android.os.Parcelable;
 import android.os.StrictMode;
 import android.util.Log;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
@@ -32,8 +33,10 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.RandomAccess;
 import java.util.Set;
+
 import libcore.net.UriCodec;
 
 /**
@@ -2338,4 +2341,29 @@
             StrictMode.onFileUriExposed(location);
         }
     }
+
+    /**
+     * Test if this is a path prefix match against the given Uri. Verifies that
+     * scheme, authority, and atomic path segments match.
+     *
+     * @hide
+     */
+    public boolean isPathPrefixMatch(Uri prefix) {
+        if (!Objects.equals(getScheme(), prefix.getScheme())) return false;
+        if (!Objects.equals(getAuthority(), prefix.getAuthority())) return false;
+
+        List<String> seg = getPathSegments();
+        List<String> prefixSeg = prefix.getPathSegments();
+
+        final int prefixSize = prefixSeg.size();
+        if (seg.size() < prefixSize) return false;
+
+        for (int i = 0; i < prefixSize; i++) {
+            if (!Objects.equals(seg.get(i), prefixSeg.get(i))) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/core/tests/coretests/src/android/net/UriTest.java b/core/tests/coretests/src/android/net/UriTest.java
index 6fb8946..cd45017 100644
--- a/core/tests/coretests/src/android/net/UriTest.java
+++ b/core/tests/coretests/src/android/net/UriTest.java
@@ -19,12 +19,14 @@
 import android.content.ContentUris;
 import android.os.Parcel;
 import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
 import java.io.File;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import junit.framework.TestCase;
 
 public class UriTest extends TestCase {
 
@@ -752,4 +754,54 @@
         assertEquals("d e", Uri.parse("http://a/b?c=d%20e").getQueryParameter("c"));
         assertEquals("d e", Uri.parse("http://a/b?c=d+e").getQueryParameter("c"));
     }
+
+    public void testPathPrefixMatch() {
+        // Exact match
+        assertTrue(Uri.parse("content://com.example/path").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+        assertTrue(Uri.parse("content://com.example/path").isPathPrefixMatch(
+                Uri.parse("content://com.example/path")));
+        assertTrue(Uri.parse("content://com.example///path///").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+        assertTrue(Uri.parse("content://com.example/path").isPathPrefixMatch(
+                Uri.parse("content://com.example///path///")));
+
+        // Child match
+        assertTrue(Uri.parse("content://com.example/path/to/child").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+        assertTrue(Uri.parse("content://com.example/path/to/child").isPathPrefixMatch(
+                Uri.parse("content://com.example/path")));
+
+        // Extra parameters
+        assertTrue(Uri.parse("content://com.example/path#fragment").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+        assertTrue(Uri.parse("content://com.example/path?q=v").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+        assertTrue(Uri.parse("content://com.example/path/?q=v").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/")));
+
+        // Different path
+        assertFalse(Uri.parse("content://com.example/path").isPathPrefixMatch(
+                Uri.parse("content://com.example/path/deeper/")));
+        assertFalse(Uri.parse("content://com.example/path2").isPathPrefixMatch(
+                Uri.parse("content://com.example/path")));
+
+        // Top-level match
+        assertTrue(Uri.parse("content://com.example/path/").isPathPrefixMatch(
+                Uri.parse("content://com.example/")));
+        assertTrue(Uri.parse("content://com.example/path/").isPathPrefixMatch(
+                Uri.parse("content://com.example")));
+
+        // Different prefixes
+        assertFalse(Uri.parse("content://com.example/path/").isPathPrefixMatch(
+                Uri.parse("file://com.example/path/")));
+        assertFalse(Uri.parse("content://com.example/path/").isPathPrefixMatch(
+                Uri.parse("content://org.example/path/")));
+
+        // Escaping
+        assertTrue(Uri.parse("content://com.example/path path/").isPathPrefixMatch(
+                Uri.parse("content://com.example/path%20path/")));
+        assertFalse(Uri.parse("content://com.example/path/path").isPathPrefixMatch(
+                Uri.parse("content://com.example/path%2Fpath")));
+    }
 }