am 21de56a9: Add directory selection to DocumentsProvider.

* commit '21de56a94668e0fda1b8bb4ee4f99a09b40d28fd':
  Add directory selection to DocumentsProvider.
diff --git a/api/current.txt b/api/current.txt
index 8080835..1e42f9a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6927,6 +6927,7 @@
     field public static final java.lang.String ACTION_PASTE = "android.intent.action.PASTE";
     field public static final java.lang.String ACTION_PICK = "android.intent.action.PICK";
     field public static final java.lang.String ACTION_PICK_ACTIVITY = "android.intent.action.PICK_ACTIVITY";
+    field public static final java.lang.String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
     field public static final java.lang.String ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED";
     field public static final java.lang.String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED";
     field public static final java.lang.String ACTION_POWER_USAGE_SUMMARY = "android.intent.action.POWER_USAGE_SUMMARY";
@@ -22480,16 +22481,21 @@
 
   public final class DocumentsContract {
     method public static android.net.Uri buildChildDocumentsUri(java.lang.String, java.lang.String);
+    method public static android.net.Uri buildChildDocumentsViaUri(android.net.Uri, java.lang.String);
     method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
+    method public static android.net.Uri buildDocumentViaUri(android.net.Uri, java.lang.String);
     method public static android.net.Uri buildRecentDocumentsUri(java.lang.String, java.lang.String);
     method public static android.net.Uri buildRootUri(java.lang.String, java.lang.String);
     method public static android.net.Uri buildRootsUri(java.lang.String);
     method public static android.net.Uri buildSearchDocumentsUri(java.lang.String, java.lang.String, java.lang.String);
+    method public static android.net.Uri buildViaUri(java.lang.String, java.lang.String);
+    method public static android.net.Uri createDocument(android.content.ContentResolver, android.net.Uri, java.lang.String, java.lang.String);
     method public static boolean deleteDocument(android.content.ContentResolver, android.net.Uri);
     method public static java.lang.String getDocumentId(android.net.Uri);
     method public static android.graphics.Bitmap getDocumentThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point, android.os.CancellationSignal);
     method public static java.lang.String getRootId(android.net.Uri);
     method public static java.lang.String getSearchDocumentsQuery(android.net.Uri);
+    method public static java.lang.String getViaDocumentId(android.net.Uri);
     method public static boolean isDocumentUri(android.content.Context, android.net.Uri);
     field public static final java.lang.String EXTRA_ERROR = "error";
     field public static final java.lang.String EXTRA_INFO = "info";
@@ -22526,6 +22532,7 @@
     field public static final java.lang.String COLUMN_TITLE = "title";
     field public static final int FLAG_LOCAL_ONLY = 2; // 0x2
     field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
+    field public static final int FLAG_SUPPORTS_DIR_SELECTION = 16; // 0x10
     field public static final int FLAG_SUPPORTS_RECENTS = 4; // 0x4
     field public static final int FLAG_SUPPORTS_SEARCH = 8; // 0x8
   }
@@ -22538,6 +22545,9 @@
     method public java.lang.String getDocumentType(java.lang.String) throws java.io.FileNotFoundException;
     method public final java.lang.String getType(android.net.Uri);
     method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+    method public boolean isChildDocument(java.lang.String, java.lang.String);
+    method public final android.content.res.AssetFileDescriptor openAssetFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
+    method public final android.content.res.AssetFileDescriptor openAssetFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
     method public abstract android.os.ParcelFileDescriptor openDocument(java.lang.String, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
     method public android.content.res.AssetFileDescriptor openDocumentThumbnail(java.lang.String, android.graphics.Point, android.os.CancellationSignal) throws java.io.FileNotFoundException;
     method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
@@ -22550,6 +22560,7 @@
     method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException;
     method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException;
     method public android.database.Cursor querySearchDocuments(java.lang.String, java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException;
+    method public final void revokeDocumentPermission(java.lang.String);
     method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
   }
 
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 67b6737..c0f04af 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2700,9 +2700,11 @@
      * take the persistable permissions using
      * {@link ContentResolver#takePersistableUriPermission(Uri, int)}.
      * <p>
-     * Callers can restrict document selection to a specific kind of data, such
-     * as photos, by setting one or more MIME types in
-     * {@link #EXTRA_MIME_TYPES}.
+     * Callers must indicate the acceptable document MIME types through
+     * {@link #setType(String)}. For example, to select photos, use
+     * {@code image/*}. If multiple disjoint MIME types are acceptable, define
+     * them in {@link #EXTRA_MIME_TYPES} and {@link #setType(String)} to
+     * {@literal *}/*.
      * <p>
      * If the caller can handle multiple returned items (the user performing
      * multiple selection), then you can specify {@link #EXTRA_ALLOW_MULTIPLE}
@@ -2712,9 +2714,10 @@
      * returned URIs can be opened with
      * {@link ContentResolver#openFileDescriptor(Uri, String)}.
      * <p>
-     * Output: The URI of the item that was picked. This must be a
-     * {@code content://} URI so that any receiver can access it. If multiple
-     * documents were selected, they are returned in {@link #getClipData()}.
+     * Output: The URI of the item that was picked, returned in
+     * {@link #getData()}. This must be a {@code content://} URI so that any
+     * receiver can access it. If multiple documents were selected, they are
+     * returned in {@link #getClipData()}.
      *
      * @see DocumentsContract
      * @see #ACTION_CREATE_DOCUMENT
@@ -2756,6 +2759,24 @@
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
 
+    /**
+     * Activity Action: Allow the user to pick a directory. When invoked, the
+     * system will display the various {@link DocumentsProvider} instances
+     * installed on the device, letting the user navigate through them. Apps can
+     * fully manage documents within the returned directory.
+     * <p>
+     * To gain access to descendant (child, grandchild, etc) documents, use
+     * {@link DocumentsContract#buildDocumentViaUri(Uri, String)} and
+     * {@link DocumentsContract#buildChildDocumentsViaUri(Uri, String)} using
+     * the returned directory URI.
+     * <p>
+     * Output: The URI representing the selected directory.
+     *
+     * @see DocumentsContract
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
+
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // Standard intent categories (see addCategory()).
@@ -3334,6 +3355,7 @@
      * @see #ACTION_GET_CONTENT
      * @see #ACTION_OPEN_DOCUMENT
      * @see #ACTION_CREATE_DOCUMENT
+     * @see #ACTION_PICK_DIRECTORY
      */
     public static final String EXTRA_LOCAL_ONLY =
             "android.intent.extra.LOCAL_ONLY";
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index dc18dee..1089f27 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -370,8 +370,8 @@
      * attacks.
      */
     public static boolean contains(File dir, File file) {
-        String dirPath = dir.getPath();
-        String filePath = file.getPath();
+        String dirPath = dir.getAbsolutePath();
+        String filePath = file.getAbsolutePath();
 
         if (dirPath.equals(filePath)) {
             return true;
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index f0520b5..9a768e0 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -57,6 +57,10 @@
  * <p>
  * To create a document provider, extend {@link DocumentsProvider}, which
  * provides a foundational implementation of this contract.
+ * <p>
+ * All client apps must hold a valid URI permission grant to access documents,
+ * typically issued when a user makes a selection through
+ * {@link Intent#ACTION_OPEN_DOCUMENT} or {@link Intent#ACTION_CREATE_DOCUMENT}.
  *
  * @see DocumentsProvider
  */
@@ -69,6 +73,8 @@
     // content://com.example/root/sdcard/search/?query=pony
     // content://com.example/document/12/
     // content://com.example/document/12/children/
+    // content://com.example/via/12/document/24/
+    // content://com.example/via/12/document/24/children/
 
     private DocumentsContract() {
     }
@@ -425,6 +431,14 @@
         public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;
 
         /**
+         * Flag indicating that this root supports directory selection.
+         *
+         * @see #COLUMN_FLAGS
+         * @see DocumentsProvider#isChildDocument(String, String)
+         */
+        public static final int FLAG_SUPPORTS_DIR_SELECTION = 1 << 4;
+
+        /**
          * Flag indicating that this root is currently empty. This may be used
          * to hide the root when opening documents, but the root will still be
          * shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is
@@ -484,12 +498,15 @@
 
     /** {@hide} */
     public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+    /** {@hide} */
+    public static final String EXTRA_URI = "uri";
 
     private static final String PATH_ROOT = "root";
     private static final String PATH_RECENT = "recent";
     private static final String PATH_DOCUMENT = "document";
     private static final String PATH_CHILDREN = "children";
     private static final String PATH_SEARCH = "search";
+    private static final String PATH_VIA = "via";
 
     private static final String PARAM_QUERY = "query";
     private static final String PARAM_MANAGE = "manage";
@@ -532,6 +549,17 @@
     }
 
     /**
+     * Build URI representing access to descendant documents of the given
+     * {@link Document#COLUMN_DOCUMENT_ID}.
+     *
+     * @see #getViaDocumentId(Uri)
+     */
+    public static Uri buildViaUri(String authority, String documentId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+                .appendPath(PATH_VIA).appendPath(documentId).build();
+    }
+
+    /**
      * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
      * document provider. When queried, a provider will return a single row with
      * columns defined by {@link Document}.
@@ -545,6 +573,41 @@
     }
 
     /**
+     * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
+     * document provider. Instead of directly accessing the target document,
+     * gain access via another document. The target document must be a
+     * descendant (child, grandchild, etc) of the via document.
+     * <p>
+     * This is typically used to access documents under a user-selected
+     * directory, since it doesn't require the user to separately confirm each
+     * new document access.
+     *
+     * @param viaUri a related document (directory) that the caller is
+     *            leveraging to gain access to the target document. The target
+     *            document must be a descendant of this directory.
+     * @param documentId the target document, which the caller may not have
+     *            direct access to.
+     * @see Intent#ACTION_PICK_DIRECTORY
+     * @see DocumentsProvider#isChildDocument(String, String)
+     * @see #buildDocumentUri(String, String)
+     */
+    public static Uri buildDocumentViaUri(Uri viaUri, String documentId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+                .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+                .appendPath(documentId).build();
+    }
+
+    /** {@hide} */
+    public static Uri buildDocumentMaybeViaUri(Uri baseUri, String documentId) {
+        if (isViaUri(baseUri)) {
+            return buildDocumentViaUri(baseUri, documentId);
+        } else {
+            return buildDocumentUri(baseUri.getAuthority(), documentId);
+        }
+    }
+
+    /**
      * Build URI representing the children of the given directory in a document
      * provider. When queried, a provider will return zero or more rows with
      * columns defined by {@link Document}.
@@ -562,6 +625,32 @@
     }
 
     /**
+     * Build URI representing the children of the given directory in a document
+     * provider. Instead of directly accessing the target document, gain access
+     * via another document. The target document must be a descendant (child,
+     * grandchild, etc) of the via document.
+     * <p>
+     * This is typically used to access documents under a user-selected
+     * directory, since it doesn't require the user to separately confirm each
+     * new document access.
+     *
+     * @param viaUri a related document (directory) that the caller is
+     *            leveraging to gain access to the target document. The target
+     *            document must be a descendant of this directory.
+     * @param parentDocumentId the target document, which the caller may not
+     *            have direct access to.
+     * @see Intent#ACTION_PICK_DIRECTORY
+     * @see DocumentsProvider#isChildDocument(String, String)
+     * @see #buildChildDocumentsUri(String, String)
+     */
+    public static Uri buildChildDocumentsViaUri(Uri viaUri, String parentDocumentId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+                .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+                .appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();
+    }
+
+    /**
      * Build URI representing a search for matching documents under a specific
      * root in a document provider. When queried, a provider will return zero or
      * more rows with columns defined by {@link Document}.
@@ -580,21 +669,31 @@
     /**
      * Test if the given URI represents a {@link Document} backed by a
      * {@link DocumentsProvider}.
+     *
+     * @see #buildDocumentUri(String, String)
+     * @see #buildDocumentViaUri(Uri, String)
      */
     public static boolean isDocumentUri(Context context, Uri uri) {
         final List<String> paths = uri.getPathSegments();
-        if (paths.size() < 2) {
-            return false;
+        if (paths.size() >= 2
+                && (PATH_DOCUMENT.equals(paths.get(0)) || PATH_VIA.equals(paths.get(0)))) {
+            return isDocumentsProvider(context, uri.getAuthority());
         }
-        if (!PATH_DOCUMENT.equals(paths.get(0))) {
-            return false;
-        }
+        return false;
+    }
 
+    /** {@hide} */
+    public static boolean isViaUri(Uri uri) {
+        final List<String> paths = uri.getPathSegments();
+        return (paths.size() >= 2 && PATH_VIA.equals(paths.get(0)));
+    }
+
+    private static boolean isDocumentsProvider(Context context, String authority) {
         final Intent intent = new Intent(PROVIDER_INTERFACE);
         final List<ResolveInfo> infos = context.getPackageManager()
                 .queryIntentContentProviders(intent, 0);
         for (ResolveInfo info : infos) {
-            if (uri.getAuthority().equals(info.providerInfo.authority)) {
+            if (authority.equals(info.providerInfo.authority)) {
                 return true;
             }
         }
@@ -606,27 +705,40 @@
      */
     public static String getRootId(Uri rootUri) {
         final List<String> paths = rootUri.getPathSegments();
-        if (paths.size() < 2) {
-            throw new IllegalArgumentException("Not a root: " + rootUri);
+        if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) {
+            return paths.get(1);
         }
-        if (!PATH_ROOT.equals(paths.get(0))) {
-            throw new IllegalArgumentException("Not a root: " + rootUri);
-        }
-        return paths.get(1);
+        throw new IllegalArgumentException("Invalid URI: " + rootUri);
     }
 
     /**
      * Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+     *
+     * @see #isDocumentUri(Context, Uri)
      */
     public static String getDocumentId(Uri documentUri) {
         final List<String> paths = documentUri.getPathSegments();
-        if (paths.size() < 2) {
-            throw new IllegalArgumentException("Not a document: " + documentUri);
+        if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {
+            return paths.get(1);
         }
-        if (!PATH_DOCUMENT.equals(paths.get(0))) {
-            throw new IllegalArgumentException("Not a document: " + documentUri);
+        if (paths.size() >= 4 && PATH_VIA.equals(paths.get(0))
+                && PATH_DOCUMENT.equals(paths.get(2))) {
+            return paths.get(3);
         }
-        return paths.get(1);
+        throw new IllegalArgumentException("Invalid URI: " + documentUri);
+    }
+
+    /**
+     * Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+     *
+     * @see #isViaUri(Uri)
+     */
+    public static String getViaDocumentId(Uri documentUri) {
+        final List<String> paths = documentUri.getPathSegments();
+        if (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))) {
+            return paths.get(1);
+        }
+        throw new IllegalArgumentException("Invalid URI: " + documentUri);
     }
 
     /**
@@ -758,7 +870,6 @@
      * @param mimeType MIME type of new document
      * @param displayName name of new document
      * @return newly created document, or {@code null} if failed
-     * @hide
      */
     public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri,
             String mimeType, String displayName) {
@@ -778,13 +889,12 @@
     public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri,
             String mimeType, String displayName) throws RemoteException {
         final Bundle in = new Bundle();
-        in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri));
+        in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);
         in.putString(Document.COLUMN_MIME_TYPE, mimeType);
         in.putString(Document.COLUMN_DISPLAY_NAME, displayName);
 
         final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
-        return buildDocumentUri(
-                parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
+        return out.getParcelable(DocumentsContract.EXTRA_URI);
     }
 
     /**
@@ -811,7 +921,7 @@
     public static void deleteDocument(ContentProviderClient client, Uri documentUri)
             throws RemoteException {
         final Bundle in = new Bundle();
-        in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri));
+        in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
 
         client.call(METHOD_DELETE_DOCUMENT, null, in);
     }
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 49816f8..1a7a00f2 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -46,6 +46,7 @@
 import libcore.io.IoUtils;
 
 import java.io.FileNotFoundException;
+import java.util.Objects;
 
 /**
  * Base class for a document provider. A document provider offers read and write
@@ -125,6 +126,8 @@
     private static final int MATCH_SEARCH = 4;
     private static final int MATCH_DOCUMENT = 5;
     private static final int MATCH_CHILDREN = 6;
+    private static final int MATCH_DOCUMENT_VIA = 7;
+    private static final int MATCH_CHILDREN_VIA = 8;
 
     private String mAuthority;
 
@@ -144,6 +147,8 @@
         mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
         mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
         mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
+        mMatcher.addURI(mAuthority, "via/*/document/*", MATCH_DOCUMENT_VIA);
+        mMatcher.addURI(mAuthority, "via/*/document/*/children", MATCH_CHILDREN_VIA);
 
         // Sanity check our setup
         if (!info.exported) {
@@ -161,6 +166,35 @@
     }
 
     /**
+     * Test if a document is descendant (child, grandchild, etc) from the given
+     * parent. Providers must override this to support directory selection. You
+     * should avoid making network requests to keep this request fast.
+     *
+     * @param parentDocumentId parent to verify against.
+     * @param documentId child to verify.
+     * @return if given document is a descendant of the given parent.
+     * @see DocumentsContract.Root#FLAG_SUPPORTS_DIR_SELECTION
+     */
+    public boolean isChildDocument(String parentDocumentId, String documentId) {
+        return false;
+    }
+
+    /** {@hide} */
+    private void enforceVia(Uri documentUri) {
+        if (DocumentsContract.isViaUri(documentUri)) {
+            final String parent = DocumentsContract.getViaDocumentId(documentUri);
+            final String child = DocumentsContract.getDocumentId(documentUri);
+            if (Objects.equals(parent, child)) {
+                return;
+            }
+            if (!isChildDocument(parent, child)) {
+                throw new SecurityException(
+                        "Document " + child + " is not a descendant of " + parent);
+            }
+        }
+    }
+
+    /**
      * Create a new document and return its newly generated
      * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new
      * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must
@@ -182,9 +216,10 @@
 
     /**
      * Delete the requested document. Upon returning, any URI permission grants
-     * for the requested document will be revoked. If additional documents were
-     * deleted as a side effect of this call, such as documents inside a
-     * directory, the implementor is responsible for revoking those permissions.
+     * for the given document will be revoked. If additional documents were
+     * deleted as a side effect of this call (such as documents inside a
+     * directory) the implementor is responsible for revoking those permissions
+     * using {@link #revokeDocumentPermission(String)}.
      *
      * @param documentId the document to delete.
      */
@@ -420,8 +455,12 @@
                     return querySearchDocuments(
                             getRootId(uri), getSearchDocumentsQuery(uri), projection);
                 case MATCH_DOCUMENT:
+                case MATCH_DOCUMENT_VIA:
+                    enforceVia(uri);
                     return queryDocument(getDocumentId(uri), projection);
                 case MATCH_CHILDREN:
+                case MATCH_CHILDREN_VIA:
+                    enforceVia(uri);
                     if (DocumentsContract.isManageMode(uri)) {
                         return queryChildDocumentsForManage(
                                 getDocumentId(uri), projection, sortOrder);
@@ -449,6 +488,8 @@
                 case MATCH_ROOT:
                     return DocumentsContract.Root.MIME_TYPE_ITEM;
                 case MATCH_DOCUMENT:
+                case MATCH_DOCUMENT_VIA:
+                    enforceVia(uri);
                     return getDocumentType(getDocumentId(uri));
                 default:
                     return null;
@@ -460,6 +501,49 @@
     }
 
     /**
+     * Implementation is provided by the parent class. Can be overridden to
+     * provide additional functionality, but subclasses <em>must</em> always
+     * call the superclass. If the superclass returns {@code null}, the subclass
+     * may implement custom behavior.
+     * <p>
+     * This is typically used to resolve a "via" URI into a concrete document
+     * reference, issuing a narrower single-document URI permission grant along
+     * the way.
+     *
+     * @see DocumentsContract#buildDocumentViaUri(Uri, String)
+     */
+    @Override
+    public Uri canonicalize(Uri uri) {
+        final Context context = getContext();
+        switch (mMatcher.match(uri)) {
+            case MATCH_DOCUMENT_VIA:
+                enforceVia(uri);
+
+                final Uri narrowUri = DocumentsContract.buildDocumentUri(uri.getAuthority(),
+                        DocumentsContract.getDocumentId(uri));
+
+                // Caller may only have prefix grant, so extend them a grant to
+                // the narrow Uri. Caller already holds read grant to get here,
+                // so check for any other modes we should extend.
+                int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+                if (context.checkCallingOrSelfUriPermission(uri,
+                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+                }
+                if (context.checkCallingOrSelfUriPermission(uri,
+                        Intent.FLAG_GRANT_READ_URI_PERMISSION
+                        | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
+                }
+                context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags);
+                return narrowUri;
+        }
+        return null;
+    }
+
+    /**
      * Implementation is provided by the parent class. Throws by default, and
      * cannot be overriden.
      *
@@ -496,54 +580,47 @@
      * provide additional functionality, but subclasses <em>must</em> always
      * call the superclass. If the superclass returns {@code null}, the subclass
      * may implement custom behavior.
-     *
-     * @see #openDocument(String, String, CancellationSignal)
-     * @see #deleteDocument(String)
      */
     @Override
     public Bundle call(String method, String arg, Bundle extras) {
-        final Context context = getContext();
-
         if (!method.startsWith("android:")) {
-            // Let non-platform methods pass through
+            // Ignore non-platform methods
             return super.call(method, arg, extras);
         }
 
-        final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
-        final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
+        final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+        final String authority = documentUri.getAuthority();
+        final String documentId = DocumentsContract.getDocumentId(documentUri);
 
-        // Require that caller can manage requested document
-        final boolean callerHasManage =
-                context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
-                == PackageManager.PERMISSION_GRANTED;
-        enforceWritePermissionInner(documentUri);
+        if (!mAuthority.equals(authority)) {
+            throw new SecurityException(
+                    "Requested authority " + authority + " doesn't match provider " + mAuthority);
+        }
+        enforceVia(documentUri);
 
         final Bundle out = new Bundle();
         try {
             if (METHOD_CREATE_DOCUMENT.equals(method)) {
+                enforceWritePermissionInner(documentUri);
+
                 final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
                 final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
 
                 final String newDocumentId = createDocument(documentId, mimeType, displayName);
-                out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
 
-                // Extend permission grant towards caller if needed
-                if (!callerHasManage) {
-                    final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
-                            mAuthority, newDocumentId);
-                    context.grantUriPermission(getCallingPackage(), newDocumentUri,
-                            Intent.FLAG_GRANT_READ_URI_PERMISSION
-                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
-                }
+                // No need to issue new grants here, since caller either has
+                // manage permission or a prefix grant. We might generate a
+                // "via" style URI if that's how they called us.
+                final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri(documentUri,
+                        newDocumentId);
+                out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
 
             } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
+                enforceWritePermissionInner(documentUri);
                 deleteDocument(documentId);
 
                 // Document no longer exists, clean up any grants
-                context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
-                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-                        | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+                revokeDocumentPermission(documentId);
 
             } else {
                 throw new UnsupportedOperationException("Method not supported " + method);
@@ -555,12 +632,25 @@
     }
 
     /**
+     * Revoke any active permission grants for the given
+     * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document
+     * becomes invalid. Follows the same semantics as
+     * {@link Context#revokeUriPermission(Uri, int)}.
+     */
+    public final void revokeDocumentPermission(String documentId) {
+        final Context context = getContext();
+        context.revokeUriPermission(DocumentsContract.buildDocumentUri(mAuthority, documentId), ~0);
+        context.revokeUriPermission(DocumentsContract.buildViaUri(mAuthority, documentId), ~0);
+    }
+
+    /**
      * Implementation is provided by the parent class. Cannot be overriden.
      *
      * @see #openDocument(String, String, CancellationSignal)
      */
     @Override
     public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        enforceVia(uri);
         return openDocument(getDocumentId(uri), mode, null);
     }
 
@@ -572,17 +662,47 @@
     @Override
     public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
             throws FileNotFoundException {
+        enforceVia(uri);
         return openDocument(getDocumentId(uri), mode, signal);
     }
 
     /**
      * Implementation is provided by the parent class. Cannot be overriden.
      *
+     * @see #openDocument(String, String, CancellationSignal)
+     */
+    @Override
+    @SuppressWarnings("resource")
+    public final AssetFileDescriptor openAssetFile(Uri uri, String mode)
+            throws FileNotFoundException {
+        enforceVia(uri);
+        final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null);
+        return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
+    }
+
+    /**
+     * Implementation is provided by the parent class. Cannot be overriden.
+     *
+     * @see #openDocument(String, String, CancellationSignal)
+     */
+    @Override
+    @SuppressWarnings("resource")
+    public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        enforceVia(uri);
+        final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal);
+        return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
+    }
+
+    /**
+     * Implementation is provided by the parent class. Cannot be overriden.
+     *
      * @see #openDocumentThumbnail(String, Point, CancellationSignal)
      */
     @Override
     public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
             throws FileNotFoundException {
+        enforceVia(uri);
         if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
             final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
             return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
@@ -600,6 +720,7 @@
     public final AssetFileDescriptor openTypedAssetFile(
             Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
             throws FileNotFoundException {
+        enforceVia(uri);
         if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
             final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
             return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 6b77a7c..159ee66 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -9,18 +9,17 @@
         android:label="@string/app_label"
         android:supportsRtl="true">
 
-        <!-- TODO: allow rotation when state saving is in better shape -->
         <activity
             android:name=".DocumentsActivity"
             android:theme="@style/Theme"
             android:icon="@drawable/ic_doc_text">
-            <intent-filter android:priority="100">
+            <intent-filter>
                 <action android:name="android.intent.action.OPEN_DOCUMENT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.OPENABLE" />
                 <data android:mimeType="*/*" />
             </intent-filter>
-            <intent-filter android:priority="100">
+            <intent-filter>
                 <action android:name="android.intent.action.CREATE_DOCUMENT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.OPENABLE" />
@@ -33,6 +32,10 @@
                 <data android:mimeType="*/*" />
             </intent-filter>
             <intent-filter>
+                <action android:name="android.intent.action.PICK_DIRECTORY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
                 <action android:name="android.provider.action.MANAGE_ROOT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.document/root" />
@@ -57,14 +60,5 @@
                 <data android:scheme="package" />
             </intent-filter>
         </receiver>
-
-        <!-- TODO: remove when we have real clients -->
-        <activity android:name=".TestActivity" android:enabled="false">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
     </application>
 </manifest>
diff --git a/packages/DocumentsUI/res/layout/fragment_pick.xml b/packages/DocumentsUI/res/layout/fragment_pick.xml
new file mode 100644
index 0000000..4a2fd03
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/fragment_pick.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <!-- Le sigh, this really should be an asset -->
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="#ccc" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:baselineAligned="false"
+        android:gravity="center_vertical"
+        android:background="#ddd"
+        android:minHeight="?android:attr/listPreferredItemHeightSmall">
+
+        <Button
+            android:id="@android:id/button1"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="?android:attr/selectableItemBackground"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textAllCaps="false"
+            android:padding="8dp" />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 92c30ba..c1a9d72 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -44,6 +44,8 @@
     <string name="menu_share">Share</string>
     <!-- Menu item title that deletes the selected documents [CHAR LIMIT=24] -->
     <string name="menu_delete">Delete</string>
+    <!-- Menu item title that selects the current directory [CHAR LIMIT=48] -->
+    <string name="menu_select">Select \"<xliff:g id="directory" example="My Directory">^1</xliff:g>\"</string>
 
     <!-- Action mode title summarizing the number of documents selected [CHAR LIMIT=32] -->
     <string name="mode_selected_count"><xliff:g id="count" example="3">%1$d</xliff:g> selected</string>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 4212e96..9f76991 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -24,6 +24,7 @@
 import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
 import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_PICK_DIRECTORY;
 import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
 import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
 
@@ -202,6 +203,8 @@
             final String mimeType = getIntent().getType();
             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
             SaveFragment.show(getFragmentManager(), mimeType, title);
+        } else if (mState.action == ACTION_PICK_DIRECTORY) {
+            PickFragment.show(getFragmentManager());
         }
 
         if (mState.action == ACTION_GET_CONTENT) {
@@ -209,7 +212,8 @@
             moreApps.setComponent(null);
             moreApps.setPackage(null);
             RootsFragment.show(getFragmentManager(), moreApps);
-        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE) {
+        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE
+                || mState.action == ACTION_PICK_DIRECTORY) {
             RootsFragment.show(getFragmentManager(), null);
         }
 
@@ -236,6 +240,8 @@
             mState.action = ACTION_CREATE;
         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
             mState.action = ACTION_GET_CONTENT;
+        } else if (Intent.ACTION_PICK_DIRECTORY.equals(action)) {
+            mState.action = ACTION_PICK_DIRECTORY;
         } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
             mState.action = ACTION_MANAGE;
         }
@@ -434,7 +440,8 @@
             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
             actionBar.setIcon(new ColorDrawable());
 
-            if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
+            if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT
+                    || mState.action == ACTION_PICK_DIRECTORY) {
                 actionBar.setTitle(R.string.title_open);
             } else if (mState.action == ACTION_CREATE) {
                 actionBar.setTitle(R.string.title_save);
@@ -576,7 +583,7 @@
         sortSize.setVisible(mState.showSize);
 
         final boolean searchVisible;
-        if (mState.action == ACTION_CREATE) {
+        if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
             createDir.setVisible(cwd != null && cwd.isCreateSupported());
             searchVisible = false;
 
@@ -586,7 +593,9 @@
                 list.setVisible(false);
             }
 
-            SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
+            if (mState.action == ACTION_CREATE) {
+                SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
+            }
         } else {
             createDir.setVisible(false);
 
@@ -819,7 +828,7 @@
 
         if (cwd == null) {
             // No directory means recents
-            if (mState.action == ACTION_CREATE) {
+            if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
                 RecentsCreateFragment.show(fm);
             } else {
                 DirectoryFragment.showRecentsOpen(fm, anim);
@@ -848,6 +857,15 @@
             }
         }
 
+        if (mState.action == ACTION_PICK_DIRECTORY) {
+            final PickFragment pick = PickFragment.get(fm);
+            if (pick != null) {
+                final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
+                        : cwd.displayName;
+                pick.setPickTarget(cwd, displayName);
+            }
+        }
+
         final RootsFragment roots = RootsFragment.get(fm);
         if (roots != null) {
             roots.onCurrentRootChanged();
@@ -1002,12 +1020,18 @@
         new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
     }
 
+    public void onPickRequested(DocumentInfo pickTarget) {
+        final Uri viaUri = DocumentsContract.buildViaUri(pickTarget.authority,
+                pickTarget.documentId);
+        new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
+    }
+
     private void saveStackBlocking() {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
-        if (mState.action == ACTION_CREATE) {
+        if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
             // Remember stack for last create
             values.clear();
             values.put(RecentColumns.KEY, mState.stack.buildKey());
@@ -1040,6 +1064,11 @@
 
         if (mState.action == ACTION_GET_CONTENT) {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        } else if (mState.action == ACTION_PICK_DIRECTORY) {
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
         } else {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -1121,6 +1150,25 @@
         }
     }
 
+    private class PickFinishTask extends AsyncTask<Void, Void, Void> {
+        private final Uri mUri;
+
+        public PickFinishTask(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            saveStackBlocking();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            onFinished(mUri);
+        }
+    }
+
     public static class State implements android.os.Parcelable {
         public int action;
         public String[] acceptMimes;
@@ -1154,7 +1202,8 @@
         public static final int ACTION_OPEN = 1;
         public static final int ACTION_CREATE = 2;
         public static final int ACTION_GET_CONTENT = 3;
-        public static final int ACTION_MANAGE = 4;
+        public static final int ACTION_PICK_DIRECTORY = 4;
+        public static final int ACTION_MANAGE = 5;
 
         public static final int MODE_UNKNOWN = 0;
         public static final int MODE_LIST = 1;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
new file mode 100644
index 0000000..a9e488a1
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 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.documentsui;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.documentsui.model.DocumentInfo;
+
+import java.util.Locale;
+
+/**
+ * Display pick confirmation bar, usually for selecting a directory.
+ */
+public class PickFragment extends Fragment {
+    public static final String TAG = "PickFragment";
+
+    private DocumentInfo mPickTarget;
+
+    private View mContainer;
+    private Button mPick;
+
+    public static void show(FragmentManager fm) {
+        final PickFragment fragment = new PickFragment();
+
+        final FragmentTransaction ft = fm.beginTransaction();
+        ft.replace(R.id.container_save, fragment, TAG);
+        ft.commitAllowingStateLoss();
+    }
+
+    public static PickFragment get(FragmentManager fm) {
+        return (PickFragment) fm.findFragmentByTag(TAG);
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        mContainer = inflater.inflate(R.layout.fragment_pick, container, false);
+
+        mPick = (Button) mContainer.findViewById(android.R.id.button1);
+        mPick.setOnClickListener(mPickListener);
+
+        setPickTarget(null, null);
+
+        return mContainer;
+    }
+
+    private View.OnClickListener mPickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final DocumentsActivity activity = DocumentsActivity.get(PickFragment.this);
+            activity.onPickRequested(mPickTarget);
+        }
+    };
+
+    public void setPickTarget(DocumentInfo pickTarget, CharSequence displayName) {
+        mPickTarget = pickTarget;
+
+        if (mPickTarget != null) {
+            mContainer.setVisibility(View.VISIBLE);
+            final Locale locale = getResources().getConfiguration().locale;
+            final String raw = getString(R.string.menu_select).toUpperCase(locale);
+            mPick.setText(TextUtils.expandTemplate(raw, displayName));
+        } else {
+            mContainer.setVisibility(View.GONE);
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index f1dca1d..933dbe0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -104,7 +104,8 @@
         mRecentsRoot.authority = null;
         mRecentsRoot.rootId = null;
         mRecentsRoot.icon = R.drawable.ic_root_recent;
-        mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
+        mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE
+                | Root.FLAG_SUPPORTS_DIR_SELECTION;
         mRecentsRoot.title = mContext.getString(R.string.root_recent);
         mRecentsRoot.availableBytes = -1;
 
@@ -349,12 +350,15 @@
         final List<RootInfo> matching = Lists.newArrayList();
         for (RootInfo root : roots) {
             final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
+            final boolean supportsDir = (root.flags & Root.FLAG_SUPPORTS_DIR_SELECTION) != 0;
             final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
             final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
             final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
 
             // Exclude read-only devices when creating
             if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
+            // Exclude roots that don't support directory picking
+            if (state.action == State.ACTION_PICK_DIRECTORY && !supportsDir) continue;
             // Exclude advanced devices when not requested
             if (!state.showAdvanced && advanced) continue;
             // Exclude non-local devices when local only
diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
deleted file mode 100644
index 1a47308..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * Copyright (C) 2013 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.documentsui;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.DocumentsContract;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import libcore.io.IoUtils;
-import libcore.io.Streams;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class TestActivity extends Activity {
-    private static final String TAG = "TestActivity";
-
-    private static final int CODE_READ = 42;
-    private static final int CODE_WRITE = 43;
-
-    private TextView mResult;
-
-    @Override
-    public void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        final Context context = this;
-
-        final LinearLayout view = new LinearLayout(context);
-        view.setOrientation(LinearLayout.VERTICAL);
-
-        mResult = new TextView(context);
-        view.addView(mResult);
-
-        final CheckBox multiple = new CheckBox(context);
-        multiple.setText("ALLOW_MULTIPLE");
-        view.addView(multiple);
-        final CheckBox localOnly = new CheckBox(context);
-        localOnly.setText("LOCAL_ONLY");
-        view.addView(localOnly);
-
-        Button button;
-        button = new Button(context);
-        button.setText("OPEN_DOC */*");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("*/*");
-                if (multiple.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-                }
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_READ);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("OPEN_DOC image/*");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("image/*");
-                if (multiple.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-                }
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_READ);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("OPEN_DOC audio/ogg");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("audio/ogg");
-                if (multiple.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-                }
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_READ);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("OPEN_DOC text/plain, application/msword");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("*/*");
-                intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
-                        "text/plain", "application/msword" });
-                if (multiple.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-                }
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_READ);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("CREATE_DOC text/plain");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("text/plain");
-                intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt");
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_WRITE);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("CREATE_DOC image/png");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("image/png");
-                intent.putExtra(Intent.EXTRA_TITLE, "mypicture.png");
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(intent, CODE_WRITE);
-            }
-        });
-        view.addView(button);
-
-        button = new Button(context);
-        button.setText("GET_CONTENT */*");
-        button.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-                intent.addCategory(Intent.CATEGORY_OPENABLE);
-                intent.setType("*/*");
-                if (multiple.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-                }
-                if (localOnly.isChecked()) {
-                    intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-                }
-                startActivityForResult(Intent.createChooser(intent, "Kittens!"), CODE_READ);
-            }
-        });
-        view.addView(button);
-
-        final ScrollView scroll = new ScrollView(context);
-        scroll.addView(view);
-
-        setContentView(scroll);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        mResult.setText(null);
-        String result = "resultCode=" + resultCode + ", data=" + String.valueOf(data);
-
-        if (requestCode == CODE_READ) {
-            final Uri uri = data != null ? data.getData() : null;
-            if (uri != null) {
-                if (DocumentsContract.isDocumentUri(this, uri)) {
-                    result += "; DOC_ID";
-                }
-                try {
-                    getContentResolver().takePersistableUriPermission(
-                            uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
-                } catch (SecurityException e) {
-                    result += "; FAILED TO TAKE";
-                    Log.e(TAG, "Failed to take", e);
-                }
-                InputStream is = null;
-                try {
-                    is = getContentResolver().openInputStream(uri);
-                    final int length = Streams.readFullyNoClose(is).length;
-                    result += "; read length=" + length;
-                } catch (Exception e) {
-                    result += "; ERROR";
-                    Log.e(TAG, "Failed to read " + uri, e);
-                } finally {
-                    IoUtils.closeQuietly(is);
-                }
-            } else {
-                result += "no uri?";
-            }
-        } else if (requestCode == CODE_WRITE) {
-            final Uri uri = data != null ? data.getData() : null;
-            if (uri != null) {
-                if (DocumentsContract.isDocumentUri(this, uri)) {
-                    result += "; DOC_ID";
-                }
-                try {
-                    getContentResolver().takePersistableUriPermission(
-                            uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-                } catch (SecurityException e) {
-                    result += "; FAILED TO TAKE";
-                    Log.e(TAG, "Failed to take", e);
-                }
-                OutputStream os = null;
-                try {
-                    os = getContentResolver().openOutputStream(uri);
-                    os.write("THE COMPLETE WORKS OF SHAKESPEARE".getBytes());
-                } catch (Exception e) {
-                    result += "; ERROR";
-                    Log.e(TAG, "Failed to write " + uri, e);
-                } finally {
-                    IoUtils.closeQuietly(os);
-                }
-            } else {
-                result += "no uri?";
-            }
-        }
-
-        Log.d(TAG, result);
-        mResult.setText(result);
-    }
-}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 559e052..16fc3e5 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -27,6 +27,7 @@
 import android.os.CancellationSignal;
 import android.os.Environment;
 import android.os.FileObserver;
+import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
@@ -143,7 +144,7 @@
                 final RootInfo root = new RootInfo();
                 root.rootId = rootId;
                 root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
-                        | Root.FLAG_SUPPORTS_SEARCH;
+                        | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_DIR_SELECTION;
                 if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
                     root.title = getContext().getString(R.string.root_internal_storage);
                 } else {
@@ -240,8 +241,8 @@
                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
             } else {
                 flags |= Document.FLAG_SUPPORTS_WRITE;
+                flags |= Document.FLAG_SUPPORTS_DELETE;
             }
-            flags |= Document.FLAG_SUPPORTS_DELETE;
         }
 
         final String displayName = file.getName();
@@ -284,11 +285,26 @@
     }
 
     @Override
+    public boolean isChildDocument(String parentDocId, String docId) {
+        try {
+            final File parent = getFileForDocId(parentDocId).getCanonicalFile();
+            final File doc = getFileForDocId(docId).getCanonicalFile();
+            return FileUtils.contains(parent, doc);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(
+                    "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
+        }
+    }
+
+    @Override
     public String createDocument(String docId, String mimeType, String displayName)
             throws FileNotFoundException {
         final File parent = getFileForDocId(docId);
-        File file;
+        if (!parent.isDirectory()) {
+            throw new IllegalArgumentException("Parent document isn't a directory");
+        }
 
+        File file;
         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
             file = new File(parent, displayName);
             if (!file.mkdir()) {
@@ -317,6 +333,7 @@
 
     @Override
     public void deleteDocument(String docId) throws FileNotFoundException {
+        // TODO: extend to delete directories
         final File file = getFileForDocId(docId);
         if (!file.delete()) {
             throw new IllegalStateException("Failed to delete " + file);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1bbdf3b..51296c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5979,8 +5979,11 @@
         return perm;
     }
 
-    private final boolean checkUriPermissionLocked(
-            Uri uri, int uid, final int modeFlags, int minStrength) {
+    private final boolean checkUriPermissionLocked(Uri uri, int uid, final int modeFlags) {
+        final boolean persistable = (modeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
+        final int minStrength = persistable ? UriPermission.STRENGTH_PERSISTABLE
+                : UriPermission.STRENGTH_OWNED;
+
         // Root gets to do everything.
         if (uid == 0) {
             return true;
@@ -6024,8 +6027,8 @@
         if (pid == MY_PID) {
             return PackageManager.PERMISSION_GRANTED;
         }
-        synchronized(this) {
-            return checkUriPermissionLocked(uri, uid, modeFlags, UriPermission.STRENGTH_OWNED)
+        synchronized (this) {
+            return checkUriPermissionLocked(uri, uid, modeFlags)
                     ? PackageManager.PERMISSION_GRANTED
                     : PackageManager.PERMISSION_DENIED;
         }
@@ -6137,11 +6140,7 @@
         if (callingUid != Process.myUid()) {
             if (!checkHoldingPermissionsLocked(pm, pi, uri, callingUid, modeFlags)) {
                 // Require they hold a strong enough Uri permission
-                final boolean persistable =
-                        (modeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
-                final int minStrength = persistable ? UriPermission.STRENGTH_PERSISTABLE
-                        : UriPermission.STRENGTH_OWNED;
-                if (!checkUriPermissionLocked(uri, callingUid, modeFlags, minStrength)) {
+                if (!checkUriPermissionLocked(uri, callingUid, modeFlags)) {
                     throw new SecurityException("Uid " + callingUid
                             + " does not have permission to uri " + uri);
                 }