Merge "Reference docs by ROOT_ID and DOC_ID; recents."
diff --git a/api/current.txt b/api/current.txt
index 6c41433..402c2dc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20272,7 +20272,9 @@
   public final class DocumentsContract {
     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 buildDocumentUri(java.lang.String, java.lang.String, java.lang.String);
+    method public static android.net.Uri buildDocumentUri(android.net.Uri, 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 buildSearchUri(android.net.Uri, java.lang.String);
     method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
@@ -20287,7 +20289,7 @@
     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 java.lang.String ROOT_DOC_ID = "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
@@ -20295,16 +20297,16 @@
   }
 
   public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
+    field public static final java.lang.String DOC_ID = "doc_id";
     field public static final java.lang.String FLAGS = "flags";
-    field public static final java.lang.String GUID = "guid";
     field public static final java.lang.String LAST_MODIFIED = "last_modified";
     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_ID = "root_id";
     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";
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 979a5a3..30c9a0d 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -43,9 +43,10 @@
     private static final String TAG = "Documents";
 
     // content://com.example/roots/
-    // content://com.example/docs/0/
-    // content://com.example/docs/0/contents/
-    // content://com.example/docs/0/search/?query=pony
+    // content://com.example/roots/sdcard/
+    // content://com.example/roots/sdcard/docs/0/
+    // content://com.example/roots/sdcard/docs/0/contents/
+    // content://com.example/roots/sdcard/docs/0/search/?query=pony
 
     /**
      * MIME type of a document which is a directory that may contain additional
@@ -59,10 +60,10 @@
     public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
 
     /**
-     * {@link DocumentColumns#GUID} value representing the root directory of a
-     * storage backend.
+     * {@link DocumentColumns#DOC_ID} value representing the root directory of a
+     * storage root.
      */
-    public static final String ROOT_GUID = "0";
+    public static final String ROOT_DOC_ID = "0";
 
     /**
      * Flag indicating that a document is a directory that supports creation of
@@ -139,20 +140,28 @@
     public static final String PARAM_QUERY = "query";
 
     /**
-     * Build URI representing the custom roots in a storage backend.
+     * Build URI representing the 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
-     * storage backend.
-     */
-    public static Uri buildDocumentUri(String authority, String guid) {
+    public static Uri buildRootUri(String authority, String rootId) {
         return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(authority).appendPath(PATH_DOCS).appendPath(guid).build();
+                .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
+    }
+
+    public static Uri buildDocumentUri(String authority, String rootId, String docId) {
+        return buildDocumentUri(buildRootUri(authority, rootId), docId);
+    }
+
+    /**
+     * Build URI representing the given {@link DocumentColumns#DOC_ID} in a
+     * storage root.
+     */
+    public static Uri buildDocumentUri(Uri rootUri, String docId) {
+        return rootUri.buildUpon().appendPath(PATH_DOCS).appendPath(docId).build();
     }
 
     /**
@@ -184,15 +193,13 @@
      */
     public interface DocumentColumns extends OpenableColumns {
         /**
-         * The globally unique ID for a document within a storage backend.
-         * Values <em>must</em> never change once returned. This field is
-         * read-only to document clients.
+         * The ID for a document under a storage backend root. Values
+         * <em>must</em> never change once returned. This field is read-only to
+         * document clients.
          * <p>
          * Type: STRING
-         *
-         * @see DocumentsContract#ROOT_GUID
          */
-        public static final String GUID = "guid";
+        public static final String DOC_ID = "doc_id";
 
         /**
          * MIME type of a document, matching the value returned by
@@ -237,6 +244,8 @@
      * @see DocumentsContract#buildRootsUri(String)
      */
     public interface RootColumns {
+        public static final String ROOT_ID = "root_id";
+
         /**
          * Storage root type, use for clustering.
          * <p>
@@ -248,13 +257,6 @@
         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>
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 453ef45..b88099e 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -21,6 +21,11 @@
             </intent-filter>
         </activity>
 
+        <provider
+            android:name=".RecentsProvider"
+            android:authorities="com.android.documentsui.recents"
+            android:exported="false" />
+
         <!-- TODO: remove when we have real clients -->
         <activity android:name=".TestActivity" android:enabled="false">
             <intent-filter>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 1f22613..ef97dd5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -59,6 +59,8 @@
 public class DirectoryFragment extends Fragment {
 
     // TODO: show storage backend in item views when requested
+    // TODO: apply sort order locally
+    // TODO: apply MIME filtering locally
 
     private ListView mListView;
     private GridView mGridView;
@@ -70,14 +72,16 @@
 
     private int mFlags;
 
-    private static final String EXTRA_URI = "uri";
+    private static final String EXTRA_ROOT_URI = "rootUri";
+    private static final String EXTRA_DOCS_URI = "docsUri";
 
     private static final int LOADER_DOCUMENTS = 2;
 
-    public static void show(
-            FragmentManager fm, Uri uri, String displayName, boolean addToBackStack) {
+    public static void show(FragmentManager fm, Uri rootUri, Uri docsUri, String displayName,
+            boolean addToBackStack) {
         final Bundle args = new Bundle();
-        args.putParcelable(EXTRA_URI, uri);
+        args.putParcelable(EXTRA_ROOT_URI, rootUri);
+        args.putParcelable(EXTRA_DOCS_URI, docsUri);
 
         final DirectoryFragment fragment = new DirectoryFragment();
         fragment.setArguments(args);
@@ -116,8 +120,8 @@
         updateMode();
 
         // TODO: migrate flags query to loader
-        final Uri uri = getArguments().getParcelable(EXTRA_URI);
-        mFlags = getDocumentFlags(context, uri);
+        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
+        mFlags = getDocumentFlags(context, docsUri);
 
         mCallbacks = new LoaderCallbacks<Cursor>() {
             @Override
@@ -133,10 +137,10 @@
                 }
 
                 final Uri contentsUri;
-                if (uri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
-                    contentsUri = uri;
+                if (docsUri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
+                    contentsUri = docsUri;
                 } else {
-                    contentsUri = DocumentsContract.buildContentsUri(uri);
+                    contentsUri = DocumentsContract.buildContentsUri(docsUri);
                 }
 
                 return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
@@ -162,8 +166,8 @@
         getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
 
         // TODO: clean up tracking of current directory
-        final Uri uri = getArguments().getParcelable(EXTRA_URI);
-        ((DocumentsActivity) getActivity()).onDirectoryChanged(uri, mFlags);
+        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
+        ((DocumentsActivity) getActivity()).onDirectoryChanged(docsUri, mFlags);
     }
 
     @Override
@@ -245,8 +249,8 @@
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             final Cursor cursor = (Cursor) mAdapter.getItem(position);
-            final Uri uri = getArguments().getParcelable(EXTRA_URI);
-            final Document doc = Document.fromCursor(uri.getAuthority(), cursor);
+            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
+            final Document doc = Document.fromCursor(rootUri, cursor);
             ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
         }
     };
@@ -266,7 +270,7 @@
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             if (item.getItemId() == R.id.menu_open) {
-                final Uri uri = getArguments().getParcelable(EXTRA_URI);
+                final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
                 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
                 final ArrayList<Document> docs = Lists.newArrayList();
 
@@ -274,7 +278,7 @@
                 for (int i = 0; i < size; i++) {
                     if (checked.valueAt(i)) {
                         final Cursor cursor = (Cursor) mAdapter.getItem(checked.keyAt(i));
-                        docs.add(Document.fromCursor(uri.getAuthority(), cursor));
+                        docs.add(Document.fromCursor(rootUri, cursor));
                     }
                 }
 
@@ -336,17 +340,17 @@
             final TextView summary = (TextView) view.findViewById(android.R.id.summary);
             final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
 
-            final String guid = getCursorString(cursor, DocumentColumns.GUID);
+            final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
             final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
             final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
             final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
             final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
 
-            final Uri uri = getArguments().getParcelable(EXTRA_URI);
-            final String authority = uri.getAuthority();
+            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
+            final String authority = rootUri.getAuthority();
 
             if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
-                final Uri childUri = DocumentsContract.buildDocumentUri(authority, guid);
+                final Uri childUri = DocumentsContract.buildDocumentUri(rootUri, docId);
                 icon.setImageURI(childUri);
             } else {
                 icon.setImageDrawable(
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index dcd02d2..405ef36 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -91,7 +91,6 @@
     private static final String TAG = "Documents";
 
     // TODO: fragment to show recently opened documents
-    // TODO: pull actionbar icon from current backend
 
     private static final String TAG_CREATE_DIRECTORY = "create_directory";
 
@@ -250,7 +249,8 @@
             public boolean onQueryTextSubmit(String query) {
                 // TODO: clear existing directory stack?
                 final Uri searchUri = DocumentsContract.buildSearchUri(mCurrentDir, query);
-                DirectoryFragment.show(getFragmentManager(), searchUri, query, true);
+                DirectoryFragment.show(
+                        getFragmentManager(), mCurrentRoot.rootUri, searchUri, query, true);
                 mSearchView.setIconified(true);
                 return true;
             }
@@ -398,7 +398,7 @@
         final FragmentManager fm = getFragmentManager();
         if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
             // Nested directory picked, recurse using new fragment
-            DirectoryFragment.show(fm, doc.uri, doc.displayName, true);
+            DirectoryFragment.show(fm, doc.rootUri, doc.uri, doc.displayName, true);
         } else if (mAction == ACTION_OPEN) {
             // Explicit file picked, return
             onFinished(doc.uri);
@@ -418,6 +418,8 @@
     }
 
     public void onSaveRequested(String mimeType, String displayName) {
+        // TODO: handle overwrite by using last-selected GUID
+
         final ContentValues values = new ContentValues();
         values.put(DocumentColumns.MIME_TYPE, mimeType);
         values.put(DocumentColumns.DISPLAY_NAME, displayName);
@@ -426,7 +428,6 @@
         if (uri != null) {
             onFinished(uri);
         } else {
-            // TODO: ask for overwrite confirmation
             Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
         }
     }
@@ -470,35 +471,29 @@
     public static class Root {
         public DocumentsProviderInfo info;
         public int rootType;
+        public Uri rootUri;
         public Uri uri;
         public Drawable icon;
         public String title;
         public String summary;
 
-        public static Root fromInfo(Context context, DocumentsProviderInfo info) {
+        public static Root fromCursor(
+                Context context, DocumentsProviderInfo info, Cursor cursor) {
+            final String rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
+
             final Root root = new Root();
             final PackageManager pm = context.getPackageManager();
 
             root.info = info;
-            root.rootType = DocumentsContract.ROOT_TYPE_SERVICE;
+            root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
+            root.rootUri = DocumentsContract.buildRootUri(info.providerInfo.authority, rootId);
             root.uri = DocumentsContract.buildDocumentUri(
-                    info.providerInfo.authority, DocumentsContract.ROOT_GUID);
+                    root.rootUri, DocumentsContract.ROOT_DOC_ID);
             root.icon = info.providerInfo.loadIcon(pm);
             root.title = info.providerInfo.loadLabel(pm).toString();
             root.summary = null;
 
-            return root;
-        }
 
-        public static Root fromCursor(
-                Context context, DocumentsProviderInfo info, Cursor cursor) {
-            final Root root = fromInfo(context, info);
-
-            root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
-            root.uri = DocumentsContract.buildDocumentUri(info.providerInfo.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 {
@@ -534,21 +529,24 @@
     }
 
     public static class Document {
+        public Uri rootUri;
         public Uri uri;
         public String mimeType;
         public String displayName;
 
-        public static Document fromCursor(String authority, Cursor cursor) {
+        public static Document fromCursor(Uri rootUri, Cursor cursor) {
             final Document doc = new Document();
-            final String guid = getCursorString(cursor, DocumentColumns.GUID);
-            doc.uri = DocumentsContract.buildDocumentUri(authority, guid);
+            final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
+            doc.rootUri = rootUri;
+            doc.uri = DocumentsContract.buildDocumentUri(rootUri, docId);
             doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
             doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
             return doc;
         }
 
-        public static Document fromUri(ContentResolver resolver, Uri uri) {
+        public static Document fromUri(ContentResolver resolver, Uri rootUri, Uri uri) {
             final Document doc = new Document();
+            doc.rootUri = rootUri;
             doc.uri = uri;
 
             final Cursor cursor = resolver.query(uri, null, null, null, null);
@@ -639,19 +637,16 @@
 
                 sProviders.put(info.providerInfo.authority, info);
 
-                if (info.customRoots) {
-                    // TODO: populate roots on background thread, and cache results
-                    final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
-                    final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
-                    try {
-                        while (cursor.moveToNext()) {
-                            sRoots.add(Root.fromCursor(this, info, cursor));
-                        }
-                    } finally {
-                        cursor.close();
+                // TODO: remove deprecated customRoots flag
+                // TODO: populate roots on background thread, and cache results
+                final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
+                final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        sRoots.add(Root.fromCursor(this, info, cursor));
                     }
-                } else if (info != null) {
-                    sRoots.add(Root.fromInfo(this, info));
+                } finally {
+                    cursor.close();
                 }
             }
         }
@@ -721,8 +716,8 @@
             }
 
             mCurrentRoot = mRootsAdapter.getItem(position);
-            DirectoryFragment.show(
-                    getFragmentManager(), mCurrentRoot.uri, mCurrentRoot.title, false);
+            DirectoryFragment.show(getFragmentManager(), mCurrentRoot.rootUri, mCurrentRoot.uri,
+                    mCurrentRoot.title, false);
 
             mDrawerLayout.closeDrawers();
         }
@@ -784,13 +779,16 @@
                     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);
+                    if (uri != null) {
+                        // Navigate into newly created child
+                        final Document doc = Document.fromUri(
+                                resolver, activity.mCurrentRoot.rootUri, uri);
+                        activity.onDocumentPicked(doc);
+                    } else {
+                        Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+                    }
                 }
             });
             builder.setNegativeButton(android.R.string.cancel, null);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
new file mode 100644
index 0000000..e6ee614
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
@@ -0,0 +1,177 @@
+/*
+ * 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.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class RecentsProvider extends ContentProvider {
+    private static final String TAG = "RecentsProvider";
+
+    public static final String AUTHORITY = "com.android.documentsui.recents";
+
+    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final int URI_RECENT_OPEN = 1;
+    private static final int URI_RECENT_CREATE = 2;
+    private static final int URI_RESUME = 3;
+
+    static {
+        sMatcher.addURI(AUTHORITY, "recent_open", URI_RECENT_OPEN);
+        sMatcher.addURI(AUTHORITY, "recent_create", URI_RECENT_CREATE);
+        sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME);
+    }
+
+    private static final String TABLE_RECENT_OPEN = "recent_open";
+    private static final String TABLE_RECENT_CREATE = "recent_create";
+    private static final String TABLE_RESUME = "resume";
+
+    /**
+     * String of URIs pointing at a storage backend, stored as a JSON array,
+     * starting with root.
+     */
+    public static final String COL_PATH = "path";
+    public static final String COL_PACKAGE_NAME = "package_name";
+    public static final String COL_TIMESTAMP = "timestamp";
+
+    private DatabaseHelper mHelper;
+
+    private static class DatabaseHelper extends SQLiteOpenHelper {
+        private static final String DB_NAME = "recents";
+
+        private static final int VERSION_INIT = 1;
+
+        public DatabaseHelper(Context context) {
+            super(context, DB_NAME, null, VERSION_INIT);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_RECENT_OPEN + " (" +
+                    COL_PATH + " TEXT," +
+                    COL_TIMESTAMP + " INTEGER," +
+                    ")");
+
+            db.execSQL("CREATE TABLE " + TABLE_RECENT_CREATE + " (" +
+                    COL_PATH + " TEXT," +
+                    COL_TIMESTAMP + " INTEGER," +
+                    ")");
+
+            db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
+                    COL_PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
+                    COL_PATH + " TEXT," +
+                    COL_TIMESTAMP + " INTEGER," +
+                    ")");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.w(TAG, "Upgrading database; wiping app data");
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_OPEN);
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_CREATE);
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME);
+            onCreate(db);
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        mHelper = new DatabaseHelper(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        final SQLiteDatabase db = mHelper.getReadableDatabase();
+        switch (sMatcher.match(uri)) {
+            case URI_RECENT_OPEN: {
+                return db.query(TABLE_RECENT_OPEN, projection,
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+            }
+            case URI_RECENT_CREATE: {
+                return db.query(TABLE_RECENT_CREATE, projection,
+                        buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+            }
+            case URI_RESUME: {
+                final String packageName = uri.getPathSegments().get(1);
+                return db.query(TABLE_RESUME, projection, COL_PACKAGE_NAME + "=?",
+                        new String[] { packageName }, null, null, null);
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        final SQLiteDatabase db = mHelper.getWritableDatabase();
+        switch (sMatcher.match(uri)) {
+            case URI_RECENT_OPEN: {
+                db.insert(TABLE_RECENT_OPEN, null, values);
+                db.delete(TABLE_RECENT_OPEN, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
+                return uri;
+            }
+            case URI_RECENT_CREATE: {
+                db.insert(TABLE_RECENT_CREATE, null, values);
+                db.delete(TABLE_RECENT_CREATE, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
+                return uri;
+            }
+            case URI_RESUME: {
+                final String packageName = uri.getPathSegments().get(1);
+                values.put(COL_PACKAGE_NAME, packageName);
+                db.insert(TABLE_RESUME, null, values);
+                return uri;
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Unsupported Uri " + uri);
+    }
+
+    private static String buildWhereOlder(long deltaMillis) {
+        return COL_TIMESTAMP + "<" + (System.currentTimeMillis() - deltaMillis);
+    }
+
+    private static String buildWhereYounger(long deltaMillis) {
+        return COL_TIMESTAMP + ">" + (System.currentTimeMillis() - deltaMillis);
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
index b15d123..a086a43 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
@@ -19,7 +19,9 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -27,7 +29,15 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
 public class TestActivity extends Activity {
+    private static final String TAG = "TestActivity";
+
     private TextView mResult;
 
     @Override
@@ -113,5 +123,19 @@
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         mResult.setText("resultCode=" + resultCode + ", data=" + String.valueOf(data));
+
+        final Uri uri = data != null ? data.getData() : null;
+        if (uri != null) {
+            InputStream is = null;
+            try {
+                is = getContentResolver().openInputStream(uri);
+                final int length = Streams.readFullyNoClose(is).length;
+                Log.d(TAG, "read length=" + length);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to read " + uri, e);
+            } finally {
+                IoUtils.closeQuietly(is);
+            }
+        }
     }
 }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index bf5811b..dd7472b 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -45,15 +45,15 @@
 
     private static final String AUTHORITY = "com.android.externalstorage";
 
-    // TODO: support searching
     // TODO: support multiple storage devices
 
     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
     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;
+    private static final int URI_ROOTS_ID = 2;
+    private static final int URI_DOCS_ID = 3;
+    private static final int URI_DOCS_ID_CONTENTS = 4;
+    private static final int URI_DOCS_ID_SEARCH = 5;
 
     private HashMap<String, Root> mRoots = Maps.newHashMap();
 
@@ -68,9 +68,10 @@
 
     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);
+        sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
+        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
     }
 
     @Override
@@ -79,7 +80,7 @@
 
         final Root root = new Root();
         root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED;
-        root.name = "internal";
+        root.name = "primary";
         root.title = getContext().getString(R.string.root_internal_storage);
         root.path = Environment.getExternalStorageDirectory();
         mRoots.put(root.name, root);
@@ -90,49 +91,59 @@
     @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[] {
-                BaseColumns._ID,
-                DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE, DocumentColumns.GUID,
-                DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS };
+        final String[] rootsProjection = new String[] {
+                BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
+                RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
+        final String[] docsProjection = new String[] {
+                BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
+                DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
+                DocumentColumns.FLAGS };
 
-        final MatrixCursor cursor = new MatrixCursor(projection);
-        switch (match) {
+        switch (sMatcher.match(uri)) {
+            case URI_ROOTS: {
+                final MatrixCursor cursor = new MatrixCursor(rootsProjection);
+                for (Root root : mRoots.values()) {
+                    includeRoot(cursor, root);
+                }
+                return cursor;
+            }
+            case URI_ROOTS_ID: {
+                final String root = uri.getPathSegments().get(1);
+
+                final MatrixCursor cursor = new MatrixCursor(rootsProjection);
+                includeRoot(cursor, mRoots.get(root));
+                return cursor;
+            }
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                includeFile(cursor, guid);
-                break;
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+
+                final MatrixCursor cursor = new MatrixCursor(docsProjection);
+                final File file = docIdToFile(root, docId);
+                includeFile(cursor, root, file);
+                return cursor;
             }
             case URI_DOCS_ID_CONTENTS: {
-                final String guid = uri.getPathSegments().get(1);
-                final File parent = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+
+                final MatrixCursor cursor = new MatrixCursor(docsProjection);
+                final File parent = docIdToFile(root, docId);
                 for (File file : parent.listFiles()) {
-                    includeFile(cursor, fileToGuid(file));
+                    includeFile(cursor, root, file);
                 }
-                break;
+                return cursor;
             }
             case URI_DOCS_ID_SEARCH: {
-                final String guid = uri.getPathSegments().get(1);
-                final File parent = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
                 final String query = uri.getQueryParameter(DocumentsContract.PARAM_QUERY).toLowerCase();
 
+                final MatrixCursor cursor = new MatrixCursor(docsProjection);
+                final File parent = docIdToFile(root, docId);
+
                 final LinkedList<File> pending = new LinkedList<File>();
                 pending.add(parent);
                 while (!pending.isEmpty() && cursor.getCount() < 20) {
@@ -143,49 +154,52 @@
                         }
                     } else {
                         if (file.getName().toLowerCase().contains(query)) {
-                            includeFile(cursor, fileToGuid(file));
+                            includeFile(cursor, root, file);
                         }
                     }
                 }
-                break;
+                return cursor;
             }
             default: {
-                cursor.close();
                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
             }
         }
-
-        return cursor;
     }
 
-    private String fileToGuid(File file) {
+    private String fileToDocId(Root root, File file) {
+        String rootPath = root.path.getAbsolutePath();
         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()));
-            }
+        if (path.equals(rootPath)) {
+            return DocumentsContract.ROOT_DOC_ID;
         }
 
-        throw new IllegalArgumentException("Failed to find root for " + file);
-    }
-
-    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);
+        if (!rootPath.endsWith("/")) {
+            rootPath += "/";
         }
-
-        throw new IllegalArgumentException("Failed to find root for " + guid);
+        if (!path.startsWith(rootPath)) {
+            throw new IllegalArgumentException("File " + path + " outside root " + root.path);
+        } else {
+            return path.substring(rootPath.length());
+        }
     }
 
-    private void includeFile(MatrixCursor cursor, String guid) {
+    private File docIdToFile(Root root, String docId) {
+        if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+            return root.path;
+        } else {
+            return new File(root.path, docId);
+        }
+    }
+
+    private void includeRoot(MatrixCursor cursor, Root root) {
+        cursor.addRow(new Object[] {
+                root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary,
+                root.path.getFreeSpace() });
+    }
+
+    private void includeFile(MatrixCursor cursor, Root root, File file) {
         int flags = 0;
 
-        final File file = guidToFile(guid);
         if (file.isDirectory()) {
             flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH;
         }
@@ -202,17 +216,19 @@
             flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
         }
 
-        final long id = guid.hashCode();
+        final String docId = fileToDocId(root, file);
+        final long id = docId.hashCode();
         cursor.addRow(new Object[] {
-                id, file.getName(), file.length(), guid, mimeType, file.lastModified(), flags });
+                id, file.getName(), file.length(), docId, mimeType, file.lastModified(), flags });
     }
 
     @Override
     public String getType(Uri uri) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                return getTypeForFile(guidToFile(guid));
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+                return getTypeForFile(docIdToFile(root, docId));
             }
             default: {
                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -245,10 +261,11 @@
     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                final File file = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
 
                 // TODO: offer as thumbnail
+                final File file = docIdToFile(root, docId);
                 return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
             }
             default: {
@@ -261,8 +278,10 @@
     public Uri insert(Uri uri, ContentValues values) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                final File parent = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+
+                final File parent = docIdToFile(root, docId);
 
                 final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
                 final String name = validateDisplayName(
@@ -285,7 +304,8 @@
                     }
                 }
 
-                return DocumentsContract.buildDocumentUri(AUTHORITY, fileToGuid(file));
+                final String newDocId = fileToDocId(root, file);
+                return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
             }
             default: {
                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -297,8 +317,10 @@
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                final File file = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+
+                final File file = docIdToFile(root, docId);
                 final File newFile = new File(
                         file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
                 return file.renameTo(newFile) ? 1 : 0;
@@ -313,8 +335,10 @@
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final String guid = uri.getPathSegments().get(1);
-                final File file = guidToFile(guid);
+                final Root root = mRoots.get(uri.getPathSegments().get(1));
+                final String docId = uri.getPathSegments().get(3);
+
+                final File file = docIdToFile(root, docId);
                 return file.delete() ? 1 : 0;
             }
             default: {