Merge "Define storage roots, external GUIDs, creation."
diff --git a/api/current.txt b/api/current.txt
index fc641bc..db1d832 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20259,15 +20259,25 @@
     ctor public DocumentsContract();
     method public static android.net.Uri buildContentsUri(android.net.Uri);
     method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
-    method public static android.net.Uri buildSearchUri(java.lang.String, java.lang.String);
+    method public static android.net.Uri buildRootsUri(java.lang.String);
+    method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
     method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
     method public static boolean renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String);
+    field public static final java.lang.String EXTRA_HAS_MORE = "has_more";
+    field public static final java.lang.String EXTRA_REQUEST_MORE = "request_more";
     field public static final java.lang.String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
     field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
+    field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4
     field public static final int FLAG_SUPPORTS_RENAME = 2; // 0x2
-    field public static final int FLAG_SUPPORTS_THUMBNAIL = 4; // 0x4
+    field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
+    field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
     field public static final java.lang.String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc";
+    field public static final java.lang.String PARAM_QUERY = "query";
     field public static final java.lang.String ROOT_GUID = "0";
+    field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
+    field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
+    field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
+    field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
   }
 
   public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
@@ -20277,6 +20287,15 @@
     field public static final java.lang.String MIME_TYPE = "mime_type";
   }
 
+  public static abstract interface DocumentsContract.RootColumns {
+    field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
+    field public static final java.lang.String GUID = "guid";
+    field public static final java.lang.String ICON = "icon";
+    field public static final java.lang.String ROOT_TYPE = "root_type";
+    field public static final java.lang.String SUMMARY = "summary";
+    field public static final java.lang.String TITLE = "title";
+  }
+
   public final deprecated class LiveFolders implements android.provider.BaseColumns {
     field public static final java.lang.String ACTION_CREATE_LIVE_FOLDER = "android.intent.action.CREATE_LIVE_FOLDER";
     field public static final java.lang.String DESCRIPTION = "description";
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index c26f6d4..979a5a3 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -16,10 +16,13 @@
 
 package android.provider;
 
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
+import android.content.pm.ProviderInfo;
 import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Point;
@@ -39,9 +42,10 @@
 public final class DocumentsContract {
     private static final String TAG = "Documents";
 
+    // content://com.example/roots/
     // content://com.example/docs/0/
     // content://com.example/docs/0/contents/
-    // content://com.example/search/?query=pony
+    // content://com.example/docs/0/search/?query=pony
 
     /**
      * MIME type of a document which is a directory that may contain additional
@@ -78,25 +82,69 @@
     public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
 
     /**
+     * Flag indicating that a document is deletable.
+     *
+     * @see DocumentColumns#FLAGS
+     */
+    public static final int FLAG_SUPPORTS_DELETE = 1 << 2;
+
+    /**
      * Flag indicating that a document can be represented as a thumbnail.
      *
      * @see DocumentColumns#FLAGS
      * @see #getThumbnail(ContentResolver, Uri, Point)
      */
-    public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 2;
+    public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
+
+    /**
+     * Flag indicating that a document is a directory that supports search.
+     *
+     * @see DocumentColumns#FLAGS
+     */
+    public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
 
     /**
      * Optimal dimensions for a document thumbnail request, stored as a
      * {@link Point} object. This is only a hint, and the returned thumbnail may
      * have different dimensions.
+     *
+     * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
      */
     public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
 
+    /**
+     * Extra boolean flag included in a directory {@link Cursor#getExtras()}
+     * indicating that the backend can provide additional data if requested,
+     * such as additional search results.
+     */
+    public static final String EXTRA_HAS_MORE = "has_more";
+
+    /**
+     * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
+     * directory to request that additional data should be fetched. When
+     * requested data is ready, the provider should send a change notification
+     * to cause a requery.
+     *
+     * @see Cursor#respond(Bundle)
+     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
+     *      boolean)
+     */
+    public static final String EXTRA_REQUEST_MORE = "request_more";
+
+    private static final String PATH_ROOTS = "roots";
     private static final String PATH_DOCS = "docs";
     private static final String PATH_CONTENTS = "contents";
     private static final String PATH_SEARCH = "search";
 
-    private static final String PARAM_QUERY = "query";
+    public static final String PARAM_QUERY = "query";
+
+    /**
+     * Build URI representing the custom roots in a storage backend.
+     */
+    public static Uri buildRootsUri(String authority) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(authority).appendPath(PATH_ROOTS).build();
+    }
 
     /**
      * Build URI representing the given {@link DocumentColumns#GUID} in a
@@ -108,11 +156,14 @@
     }
 
     /**
-     * Build URI representing a search for matching documents in a storage
-     * backend.
+     * Build URI representing a search for matching documents under a directory
+     * in a storage backend.
+     *
+     * @param documentUri directory to search under, which must have
+     *            {@link #FLAG_SUPPORTS_SEARCH}.
      */
-    public static Uri buildSearchUri(String authority, String query) {
-        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+    public static Uri buildSearchUri(Uri documentUri, String query) {
+        return documentUri.buildUpon()
                 .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
     }
 
@@ -134,7 +185,8 @@
     public interface DocumentColumns extends OpenableColumns {
         /**
          * The globally unique ID for a document within a storage backend.
-         * Values <em>must</em> never change once returned.
+         * Values <em>must</em> never change once returned. This field is
+         * read-only to document clients.
          * <p>
          * Type: STRING
          *
@@ -144,7 +196,9 @@
 
         /**
          * MIME type of a document, matching the value returned by
-         * {@link ContentResolver#getType(android.net.Uri)}.
+         * {@link ContentResolver#getType(android.net.Uri)}. This field must be
+         * provided when a new document is created, but after that the field is
+         * read-only.
          * <p>
          * Type: STRING
          *
@@ -154,7 +208,8 @@
 
         /**
          * Timestamp when a document was last modified, in milliseconds since
-         * January 1, 1970 00:00:00.0 UTC.
+         * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
+         * clients.
          * <p>
          * Type: INTEGER (long)
          *
@@ -163,13 +218,74 @@
         public static final String LAST_MODIFIED = "last_modified";
 
         /**
-         * Flags that apply to a specific document.
+         * Flags that apply to a specific document. This field is read-only to
+         * document clients.
          * <p>
          * Type: INTEGER (int)
          */
         public static final String FLAGS = "flags";
     }
 
+    public static final int ROOT_TYPE_SERVICE = 1;
+    public static final int ROOT_TYPE_SHORTCUT = 2;
+    public static final int ROOT_TYPE_DEVICE = 3;
+    public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
+
+    /**
+     * These are standard columns for the roots URI.
+     *
+     * @see DocumentsContract#buildRootsUri(String)
+     */
+    public interface RootColumns {
+        /**
+         * Storage root type, use for clustering.
+         * <p>
+         * Type: INTEGER (int)
+         *
+         * @see DocumentsContract#ROOT_TYPE_SERVICE
+         * @see DocumentsContract#ROOT_TYPE_DEVICE
+         */
+        public static final String ROOT_TYPE = "root_type";
+
+        /**
+         * GUID of directory entry for this storage root.
+         * <p>
+         * Type: STRING
+         */
+        public static final String GUID = "guid";
+
+        /**
+         * Icon resource ID for this storage root, or {@code 0} to use the
+         * default {@link ProviderInfo#icon}.
+         * <p>
+         * Type: INTEGER (int)
+         */
+        public static final String ICON = "icon";
+
+        /**
+         * Title for this storage root, or {@code null} to use the default
+         * {@link ProviderInfo#labelRes}.
+         * <p>
+         * Type: STRING
+         */
+        public static final String TITLE = "title";
+
+        /**
+         * Summary for this storage root, or {@code null} to omit.
+         * <p>
+         * Type: STRING
+         */
+        public static final String SUMMARY = "summary";
+
+        /**
+         * Number of free bytes of available in this storage root, or -1 if
+         * unknown or unbounded.
+         * <p>
+         * Type: INTEGER (long)
+         */
+        public static final String AVAILABLE_BYTES = "available_bytes";
+    }
+
     /**
      * Return thumbnail representing the document at the given URI. Callers are
      * responsible for their own caching. Given document must have
diff --git a/packages/DocumentsUI/res/layout/dialog_create_dir.xml b/packages/DocumentsUI/res/layout/dialog_create_dir.xml
new file mode 100644
index 0000000..54e26b4
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/dialog_create_dir.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="?android:attr/listPreferredItemPaddingEnd">
+
+    <EditText
+        android:id="@android:id/text1"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</FrameLayout>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BackendFragment.java b/packages/DocumentsUI/src/com/android/documentsui/BackendFragment.java
index 2980e23..fc13487 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BackendFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BackendFragment.java
@@ -21,9 +21,15 @@
 import android.app.FragmentTransaction;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ProviderInfo;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.RootColumns;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -39,11 +45,11 @@
 import java.util.List;
 
 /**
- * Display all known storage backends.
+ * Display all known storage roots.
  */
 public class BackendFragment extends Fragment {
 
-    // TODO: handle multiple accounts from single backend
+    // TODO: cluster backends by type
 
     private GridView mGridView;
     private BackendAdapter mAdapter;
@@ -57,19 +63,69 @@
         ft.commitAllowingStateLoss();
     }
 
+    public static class Root {
+        public int rootType;
+        public Uri uri;
+        public Drawable icon;
+        public String title;
+        public String summary;
+
+        public static Root fromCursor(Context context, ProviderInfo info, Cursor cursor) {
+            final Root root = new Root();
+
+            root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
+            root.uri = DocumentsContract.buildDocumentUri(
+                    info.authority, cursor.getString(cursor.getColumnIndex(RootColumns.GUID)));
+
+            final PackageManager pm = context.getPackageManager();
+            final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
+            if (icon != 0) {
+                try {
+                    root.icon = pm.getResourcesForApplication(info.applicationInfo)
+                            .getDrawable(icon);
+                } catch (NotFoundException e) {
+                    throw new RuntimeException(e);
+                } catch (NameNotFoundException e) {
+                    throw new RuntimeException(e);
+                }
+            } else {
+                root.icon = info.loadIcon(pm);
+            }
+
+            root.title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
+            if (root.title == null) {
+                root.title = info.loadLabel(pm).toString();
+            }
+
+            root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));
+
+            return root;
+        }
+    }
+
     @Override
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final Context context = inflater.getContext();
 
-        // Gather known storage providers
+        // Gather roots from known storage providers
         final List<ProviderInfo> providers = context.getPackageManager()
                 .queryContentProviders(null, -1, PackageManager.GET_META_DATA);
-        final List<ProviderInfo> backends = Lists.newArrayList();
+        final List<Root> roots = Lists.newArrayList();
         for (ProviderInfo info : providers) {
             if (info.metaData != null
                     && info.metaData.containsKey(DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
-                backends.add(info);
+                // TODO: populate roots on background thread, and cache results
+                final Uri uri = DocumentsContract.buildRootsUri(info.authority);
+                final Cursor cursor = context.getContentResolver()
+                        .query(uri, null, null, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        roots.add(Root.fromCursor(context, info, cursor));
+                    }
+                } finally {
+                    cursor.close();
+                }
             }
         }
 
@@ -78,7 +134,7 @@
         mGridView = (GridView) view.findViewById(R.id.grid);
         mGridView.setOnItemClickListener(mItemListener);
 
-        mAdapter = new BackendAdapter(context, backends);
+        mAdapter = new BackendAdapter(context, roots);
         mGridView.setAdapter(mAdapter);
         mGridView.setNumColumns(GridView.AUTO_FIT);
 
@@ -88,13 +144,13 @@
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final ProviderInfo info = mAdapter.getItem(position);
-            ((DocumentsActivity) getActivity()).onBackendPicked(info);
+            final Root root = mAdapter.getItem(position);
+            ((DocumentsActivity) getActivity()).onRootPicked(root);
         }
     };
 
-    public static class BackendAdapter extends ArrayAdapter<ProviderInfo> {
-        public BackendAdapter(Context context, List<ProviderInfo> list) {
+    public static class BackendAdapter extends ArrayAdapter<Root> {
+        public BackendAdapter(Context context, List<Root> list) {
             super(context, android.R.layout.simple_list_item_1, list);
         }
 
@@ -109,9 +165,9 @@
             final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
 
             final PackageManager pm = parent.getContext().getPackageManager();
-            final ProviderInfo info = getItem(position);
-            icon.setImageDrawable(info.loadIcon(pm));
-            text1.setText(info.loadLabel(pm));
+            final Root root = getItem(position);
+            icon.setImageDrawable(root.icon);
+            text1.setText(root.title);
 
             return convertView;
         }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index c45d2b4..e61cea6 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -21,15 +21,20 @@
 import android.app.ActionBar;
 import android.app.ActionBar.OnNavigationListener;
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
 import android.app.FragmentManager;
 import android.app.FragmentManager.BackStackEntry;
 import android.app.FragmentManager.OnBackStackChangedListener;
 import android.content.ClipData;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
@@ -44,8 +49,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
+import android.widget.EditText;
 import android.widget.TextView;
 
+import com.android.documentsui.BackendFragment.Root;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -160,7 +168,7 @@
             getFragmentManager().popBackStack();
             updateActionBar();
         } else if (id == R.id.menu_create_dir) {
-            // TODO: show dialog to create directory
+            CreateDirectoryFragment.show(getFragmentManager());
         }
         return super.onOptionsItemSelected(item);
     }
@@ -232,11 +240,8 @@
         invalidateOptionsMenu();
     }
 
-    public void onBackendPicked(ProviderInfo info) {
-        final Uri uri = DocumentsContract.buildDocumentUri(
-                info.authority, DocumentsContract.ROOT_GUID);
-        final CharSequence displayName = info.loadLabel(getPackageManager());
-        DirectoryFragment.show(getFragmentManager(), uri, displayName.toString());
+    public void onRootPicked(Root root) {
+        DirectoryFragment.show(getFragmentManager(), root.uri, root.title);
     }
 
     public void onDocumentPicked(Document doc) {
@@ -263,8 +268,12 @@
     }
 
     public void onSaveRequested(String mimeType, String displayName) {
-        // TODO: create file, confirming before overwriting
-        final Uri uri = null;
+        final ContentValues values = new ContentValues();
+        values.put(DocumentColumns.MIME_TYPE, mimeType);
+        values.put(DocumentColumns.DISPLAY_NAME, displayName);
+
+        // TODO: handle errors from remote side
+        final Uri uri = getContentResolver().insert(mCurrentDir, values);
         onFinished(uri);
     }
 
@@ -283,11 +292,10 @@
             intent.setClipData(clipData);
         }
 
-        intent.addFlags(
-                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
-        if (mAction == ACTION_CREATE) {
-            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
-        }
+        // TODO: omit WRITE and PERSIST for GET_CONTENT
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+                | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
 
         setResult(Activity.RESULT_OK, intent);
         finish();
@@ -318,6 +326,24 @@
             doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
             return doc;
         }
+
+        public static Document fromUri(ContentResolver resolver, Uri uri) {
+            final Document doc = new Document();
+            doc.uri = uri;
+
+            final Cursor cursor = resolver.query(uri, null, null, null, null);
+            try {
+                if (!cursor.moveToFirst()) {
+                    throw new IllegalArgumentException("Missing details for " + uri);
+                }
+                doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
+                doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
+            } finally {
+                cursor.close();
+            }
+
+            return doc;
+        }
     }
 
     public static boolean mimeMatches(String filter, String[] tests) {
@@ -359,4 +385,50 @@
             }
         }
     }
+
+    private static final String TAG_CREATE_DIRECTORY = "create_directory";
+
+    public static class CreateDirectoryFragment extends DialogFragment {
+        public static void show(FragmentManager fm) {
+            final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
+            dialog.show(fm, TAG_CREATE_DIRECTORY);
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            final Context context = getActivity();
+            final ContentResolver resolver = context.getContentResolver();
+
+            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+            final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
+            final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
+
+            builder.setTitle(R.string.menu_create_dir);
+            builder.setView(view);
+
+            builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    final String displayName = text1.getText().toString();
+
+                    final ContentValues values = new ContentValues();
+                    values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
+                    values.put(DocumentColumns.DISPLAY_NAME, displayName);
+
+                    // TODO: handle errors from remote side
+                    final DocumentsActivity activity = (DocumentsActivity) getActivity();
+                    final Uri uri = resolver.insert(activity.mCurrentDir, values);
+
+                    // Navigate into newly created child
+                    final Document doc = Document.fromUri(resolver, uri);
+                    activity.onDocumentPicked(doc);
+                }
+            });
+            builder.setNegativeButton(android.R.string.cancel, null);
+
+            return builder.create();
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index 19a1d2a..a2a4f7c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -82,7 +82,7 @@
         @Override
         public void onClick(View v) {
             final String mimeType = getArguments().getString(EXTRA_MIME_TYPE);
-            final String displayName = getArguments().getString(EXTRA_DISPLAY_NAME);
+            final String displayName = mDisplayName.getText().toString();
             ((DocumentsActivity) getActivity()).onSaveRequested(mimeType, displayName);
         }
     };
diff --git a/packages/ExternalStorageProvider/res/values/strings.xml b/packages/ExternalStorageProvider/res/values/strings.xml
index 4374cfc..0eaf500a 100644
--- a/packages/ExternalStorageProvider/res/values/strings.xml
+++ b/packages/ExternalStorageProvider/res/values/strings.xml
@@ -16,4 +16,5 @@
 
 <resources>
     <string name="app_label">External Storage</string>
+    <string name="root_internal_storage">Internal storage</string>
 </resources>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index f75e3bd..bf5811b 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -28,14 +28,17 @@
 import android.provider.BaseColumns;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.RootColumns;
+import android.util.Log;
 import android.webkit.MimeTypeMap;
 
-import com.android.internal.annotations.GuardedBy;
-import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.util.ArrayList;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
 
 public class ExternalStorageProvider extends ContentProvider {
     private static final String TAG = "ExternalStorage";
@@ -44,33 +47,65 @@
 
     // TODO: support searching
     // TODO: support multiple storage devices
-    // TODO: persist GUIDs across launches
 
     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
-    private static final int URI_DOCS_ID = 1;
-    private static final int URI_DOCS_ID_CONTENTS = 2;
-    private static final int URI_SEARCH = 3;
+    private static final int URI_ROOTS = 1;
+    private static final int URI_DOCS_ID = 2;
+    private static final int URI_DOCS_ID_CONTENTS = 3;
+    private static final int URI_DOCS_ID_SEARCH = 4;
 
-    static {
-        sMatcher.addURI(AUTHORITY, "docs/#", URI_DOCS_ID);
-        sMatcher.addURI(AUTHORITY, "docs/#/contents", URI_DOCS_ID_CONTENTS);
-        sMatcher.addURI(AUTHORITY, "search", URI_SEARCH);
+    private HashMap<String, Root> mRoots = Maps.newHashMap();
+
+    private static class Root {
+        public int rootType;
+        public String name;
+        public int icon = 0;
+        public String title = null;
+        public String summary = null;
+        public File path;
     }
 
-    @GuardedBy("mFiles")
-    private ArrayList<File> mFiles = Lists.newArrayList();
+    static {
+        sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
+        sMatcher.addURI(AUTHORITY, "docs/*", URI_DOCS_ID);
+        sMatcher.addURI(AUTHORITY, "docs/*/contents", URI_DOCS_ID_CONTENTS);
+        sMatcher.addURI(AUTHORITY, "docs/*/search", URI_DOCS_ID_SEARCH);
+    }
 
     @Override
     public boolean onCreate() {
-        mFiles.clear();
-        mFiles.add(Environment.getExternalStorageDirectory());
+        mRoots.clear();
+
+        final Root root = new Root();
+        root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED;
+        root.name = "internal";
+        root.title = getContext().getString(R.string.root_internal_storage);
+        root.path = Environment.getExternalStorageDirectory();
+        mRoots.put(root.name, root);
+
         return true;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        final int match = sMatcher.match(uri);
+        if (match == URI_ROOTS) {
+            // TODO: support custom projections
+            projection = new String[] {
+                    RootColumns.ROOT_TYPE, RootColumns.GUID, RootColumns.ICON, RootColumns.TITLE,
+                    RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
+
+            final MatrixCursor cursor = new MatrixCursor(projection);
+            for (Root root : mRoots.values()) {
+                final String guid = fileToGuid(root.path);
+                cursor.addRow(new Object[] {
+                        root.rootType, guid, root.icon, root.title, root.summary,
+                        root.path.getFreeSpace() });
+            }
+            return cursor;
+        }
 
         // TODO: support custom projections
         projection = new String[] {
@@ -79,21 +114,37 @@
                 DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS };
 
         final MatrixCursor cursor = new MatrixCursor(projection);
-        switch (sMatcher.match(uri)) {
+        switch (match) {
             case URI_DOCS_ID: {
-                final int id = Integer.parseInt(uri.getPathSegments().get(1));
-                synchronized (mFiles) {
-                    includeFileLocked(cursor, id);
-                }
+                final String guid = uri.getPathSegments().get(1);
+                includeFile(cursor, guid);
                 break;
             }
             case URI_DOCS_ID_CONTENTS: {
-                final int parentId = Integer.parseInt(uri.getPathSegments().get(1));
-                synchronized (mFiles) {
-                    final File parent = mFiles.get(parentId);
-                    for (File file : parent.listFiles()) {
-                        final int id = findOrCreateFileLocked(file);
-                        includeFileLocked(cursor, id);
+                final String guid = uri.getPathSegments().get(1);
+                final File parent = guidToFile(guid);
+                for (File file : parent.listFiles()) {
+                    includeFile(cursor, fileToGuid(file));
+                }
+                break;
+            }
+            case URI_DOCS_ID_SEARCH: {
+                final String guid = uri.getPathSegments().get(1);
+                final File parent = guidToFile(guid);
+                final String query = uri.getQueryParameter(DocumentsContract.PARAM_QUERY).toLowerCase();
+
+                final LinkedList<File> pending = new LinkedList<File>();
+                pending.add(parent);
+                while (!pending.isEmpty() && cursor.getCount() < 20) {
+                    final File file = pending.removeFirst();
+                    if (file.isDirectory()) {
+                        for (File child : file.listFiles()) {
+                            pending.add(child);
+                        }
+                    } else {
+                        if (file.getName().toLowerCase().contains(query)) {
+                            includeFile(cursor, fileToGuid(file));
+                        }
                     }
                 }
                 break;
@@ -107,43 +158,61 @@
         return cursor;
     }
 
-    private int findOrCreateFileLocked(File file) {
-        int id = mFiles.indexOf(file);
-        if (id == -1) {
-            id = mFiles.size();
-            mFiles.add(file);
+    private String fileToGuid(File file) {
+        final String path = file.getAbsolutePath();
+        for (Root root : mRoots.values()) {
+            final String rootPath = root.path.getAbsolutePath();
+            if (path.startsWith(rootPath)) {
+                return root.name + ':' + Uri.encode(path.substring(rootPath.length()));
+            }
         }
-        return id;
+
+        throw new IllegalArgumentException("Failed to find root for " + file);
     }
 
-    private void includeFileLocked(MatrixCursor cursor, int id) {
-        final File file = mFiles.get(id);
+    private File guidToFile(String guid) {
+        final int split = guid.indexOf(':');
+        final String name = guid.substring(0, split);
+        final Root root = mRoots.get(name);
+        if (root != null) {
+            final String path = Uri.decode(guid.substring(split + 1));
+            return new File(root.path, path);
+        }
+
+        throw new IllegalArgumentException("Failed to find root for " + guid);
+    }
+
+    private void includeFile(MatrixCursor cursor, String guid) {
         int flags = 0;
 
+        final File file = guidToFile(guid);
+        if (file.isDirectory()) {
+            flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH;
+        }
         if (file.isDirectory() && file.canWrite()) {
             flags |= DocumentsContract.FLAG_SUPPORTS_CREATE;
         }
         if (file.canWrite()) {
             flags |= DocumentsContract.FLAG_SUPPORTS_RENAME;
+            flags |= DocumentsContract.FLAG_SUPPORTS_DELETE;
         }
 
-        final String mimeType = getTypeLocked(id);
+        final String mimeType = getTypeForFile(file);
         if (mimeType.startsWith("image/")) {
             flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
         }
 
+        final long id = guid.hashCode();
         cursor.addRow(new Object[] {
-                id, file.getName(), file.length(), id, mimeType, file.lastModified(), flags });
+                id, file.getName(), file.length(), guid, mimeType, file.lastModified(), flags });
     }
 
     @Override
     public String getType(Uri uri) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final int id = Integer.parseInt(uri.getPathSegments().get(1));
-                synchronized (mFiles) {
-                    return getTypeLocked(id);
-                }
+                final String guid = uri.getPathSegments().get(1);
+                return getTypeForFile(guidToFile(guid));
             }
             default: {
                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -151,16 +220,18 @@
         }
     }
 
-    private String getTypeLocked(int id) {
-        final File file = mFiles.get(id);
-
+    private String getTypeForFile(File file) {
         if (file.isDirectory()) {
             return DocumentsContract.MIME_TYPE_DIRECTORY;
+        } else {
+            return getTypeForName(file.getName());
         }
+    }
 
-        final int lastDot = file.getName().lastIndexOf('.');
+    private String getTypeForName(String name) {
+        final int lastDot = name.lastIndexOf('.');
         if (lastDot >= 0) {
-            final String extension = file.getName().substring(lastDot + 1);
+            final String extension = name.substring(lastDot + 1);
             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
             if (mime != null) {
                 return mime;
@@ -174,12 +245,11 @@
     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final int id = Integer.parseInt(uri.getPathSegments().get(1));
-                synchronized (mFiles) {
-                    final File file = mFiles.get(id);
-                    // TODO: turn into thumbnail
-                    return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
-                }
+                final String guid = uri.getPathSegments().get(1);
+                final File file = guidToFile(guid);
+
+                // TODO: offer as thumbnail
+                return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
             }
             default: {
                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -189,16 +259,84 @@
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
-        throw new UnsupportedOperationException();
+        switch (sMatcher.match(uri)) {
+            case URI_DOCS_ID: {
+                final String guid = uri.getPathSegments().get(1);
+                final File parent = guidToFile(guid);
+
+                final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
+                final String name = validateDisplayName(
+                        values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
+
+                final File file = new File(parent, name);
+                if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+                    if (!file.mkdir()) {
+                        return null;
+                    }
+
+                } else {
+                    try {
+                        if (!file.createNewFile()) {
+                            return null;
+                        }
+                    } catch (IOException e) {
+                        Log.w(TAG, "Failed to create file", e);
+                        return null;
+                    }
+                }
+
+                return DocumentsContract.buildDocumentUri(AUTHORITY, fileToGuid(file));
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
     }
 
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException();
+        switch (sMatcher.match(uri)) {
+            case URI_DOCS_ID: {
+                final String guid = uri.getPathSegments().get(1);
+                final File file = guidToFile(guid);
+                final File newFile = new File(
+                        file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
+                return file.renameTo(newFile) ? 1 : 0;
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
     }
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException();
+        switch (sMatcher.match(uri)) {
+            case URI_DOCS_ID: {
+                final String guid = uri.getPathSegments().get(1);
+                final File file = guidToFile(guid);
+                return file.delete() ? 1 : 0;
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    private String validateDisplayName(String displayName, String mimeType) {
+        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+            return displayName;
+        } else {
+            // Try appending meaningful extension if needed
+            if (!mimeType.equals(getTypeForName(displayName))) {
+                final String extension = MimeTypeMap.getSingleton()
+                        .getExtensionFromMimeType(mimeType);
+                if (extension != null) {
+                    displayName += "." + extension;
+                }
+            }
+
+            return displayName;
+        }
     }
 }