Track and persist directory stacks; recents work.

Move to manual tracking of directory navigation stack so we have Uri
data to persist, instead of opaque fragment backstack.  Remember
directory stacks across launches on a per-app basis.

Start recording recently opened and created files.  Uniform Uri
parameter extraction utility methods in contract.

Change-Id: I79ed30ee10272bf7c53d339e797639c993f649bb
diff --git a/api/current.txt b/api/current.txt
index 6bf8fe0..5fd52e3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20266,12 +20266,17 @@
 
   public final class DocumentsContract {
     ctor public DocumentsContract();
+    method public static android.net.Uri buildContentsUri(java.lang.String, java.lang.String, java.lang.String);
     method public static android.net.Uri buildContentsUri(android.net.Uri);
     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(java.lang.String, java.lang.String, java.lang.String, java.lang.String);
     method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
+    method public static java.lang.String getDocId(android.net.Uri);
+    method public static java.lang.String getRootId(android.net.Uri);
+    method public static java.lang.String getSearchQuery(android.net.Uri);
     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";
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 30c9a0d..9c2bb49 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -34,6 +34,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 
 /**
  * The contract between a storage backend and the platform. Contains definitions
@@ -152,36 +153,71 @@
                 .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();
-    }
-
-    /**
-     * 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(Uri documentUri, String query) {
-        return documentUri.buildUpon()
-                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
+    public static Uri buildDocumentUri(String authority, String rootId, String docId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
+                .build();
     }
 
     /**
      * Build URI representing the contents of the given directory in a storage
      * backend. The given document must be {@link #MIME_TYPE_DIRECTORY}.
      */
-    public static Uri buildContentsUri(Uri documentUri) {
-        return documentUri.buildUpon().appendPath(PATH_CONTENTS).build();
+    public static Uri buildContentsUri(String authority, String rootId, String docId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
+                .appendPath(PATH_CONTENTS).build();
+    }
+
+    /**
+     * Build URI representing a search for matching documents under a directory
+     * in a storage backend.
+     */
+    public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
+                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
+    }
+
+    public static Uri buildDocumentUri(Uri relatedUri, String docId) {
+        return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
+    }
+
+    public static Uri buildContentsUri(Uri relatedUri) {
+        return buildContentsUri(
+                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
+    }
+
+    public static Uri buildSearchUri(Uri relatedUri, String query) {
+        return buildSearchUri(
+                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
+    }
+
+    public static String getRootId(Uri documentUri) {
+        final List<String> paths = documentUri.getPathSegments();
+        if (!PATH_ROOTS.equals(paths.get(0))) {
+            throw new IllegalArgumentException();
+        }
+        return paths.get(1);
+    }
+
+    public static String getDocId(Uri documentUri) {
+        final List<String> paths = documentUri.getPathSegments();
+        if (!PATH_ROOTS.equals(paths.get(0))) {
+            throw new IllegalArgumentException();
+        }
+        if (!PATH_DOCS.equals(paths.get(2))) {
+            throw new IllegalArgumentException();
+        }
+        return paths.get(3);
+    }
+
+    public static String getSearchQuery(Uri documentUri) {
+        return documentUri.getQueryParameter(PARAM_QUERY);
     }
 
     /**
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index ef97dd5..2740e53 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -67,31 +67,29 @@
 
     private AbsListView mCurrentView;
 
+    private static final int TYPE_NORMAL = 1;
+    private static final int TYPE_SEARCH = 2;
+    private static final int TYPE_RECENT_OPEN = 3;
+    private static final int TYPE_RECENT_CREATE = 4;
+
+    private int mType = TYPE_NORMAL;
+
     private DocumentsAdapter mAdapter;
     private LoaderCallbacks<Cursor> mCallbacks;
 
-    private int mFlags;
-
-    private static final String EXTRA_ROOT_URI = "rootUri";
-    private static final String EXTRA_DOCS_URI = "docsUri";
+    private static final String EXTRA_URI = "uri";
 
     private static final int LOADER_DOCUMENTS = 2;
 
-    public static void show(FragmentManager fm, Uri rootUri, Uri docsUri, String displayName,
-            boolean addToBackStack) {
+    public static void show(FragmentManager fm, Uri uri) {
         final Bundle args = new Bundle();
-        args.putParcelable(EXTRA_ROOT_URI, rootUri);
-        args.putParcelable(EXTRA_DOCS_URI, docsUri);
+        args.putParcelable(EXTRA_URI, uri);
 
         final DirectoryFragment fragment = new DirectoryFragment();
         fragment.setArguments(args);
 
         final FragmentTransaction ft = fm.beginTransaction();
         ft.replace(R.id.directory, fragment);
-        if (addToBackStack) {
-            ft.addToBackStack(displayName);
-        }
-        ft.setBreadCrumbTitle(displayName);
         ft.commitAllowingStateLoss();
     }
 
@@ -119,9 +117,17 @@
         mAdapter = new DocumentsAdapter(context);
         updateMode();
 
-        // TODO: migrate flags query to loader
-        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
-        mFlags = getDocumentFlags(context, docsUri);
+        final Uri uri = getArguments().getParcelable(EXTRA_URI);
+
+        if (uri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
+            mType = TYPE_SEARCH;
+        } else if (RecentsProvider.buildRecentOpen().equals(uri)) {
+            mType = TYPE_RECENT_OPEN;
+        } else if (RecentsProvider.buildRecentCreate().equals(uri)) {
+            mType = TYPE_RECENT_CREATE;
+        } else {
+            mType = TYPE_NORMAL;
+        }
 
         mCallbacks = new LoaderCallbacks<Cursor>() {
             @Override
@@ -137,10 +143,10 @@
                 }
 
                 final Uri contentsUri;
-                if (docsUri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
-                    contentsUri = docsUri;
+                if (mType == TYPE_NORMAL) {
+                    contentsUri = DocumentsContract.buildContentsUri(uri);
                 } else {
-                    contentsUri = DocumentsContract.buildContentsUri(docsUri);
+                    contentsUri = uri;
                 }
 
                 return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
@@ -164,10 +170,6 @@
     public void onStart() {
         super.onStart();
         getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
-
-        // TODO: clean up tracking of current directory
-        final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
-        ((DocumentsActivity) getActivity()).onDirectoryChanged(docsUri, mFlags);
     }
 
     @Override
@@ -249,8 +251,8 @@
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             final Cursor cursor = (Cursor) mAdapter.getItem(position);
-            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
-            final Document doc = Document.fromCursor(rootUri, cursor);
+            final Uri uri = getArguments().getParcelable(EXTRA_URI);
+            final Document doc = Document.fromCursor(uri, cursor);
             ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
         }
     };
@@ -270,7 +272,7 @@
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             if (item.getItemId() == R.id.menu_open) {
-                final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
+                final Uri uri = getArguments().getParcelable(EXTRA_URI);
                 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
                 final ArrayList<Document> docs = Lists.newArrayList();
 
@@ -278,7 +280,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(rootUri, cursor));
+                        docs.add(Document.fromCursor(uri, cursor));
                     }
                 }
 
@@ -346,15 +348,13 @@
             final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
             final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
 
-            final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
-            final String authority = rootUri.getAuthority();
-
+            final Uri uri = getArguments().getParcelable(EXTRA_URI);
             if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
-                final Uri childUri = DocumentsContract.buildDocumentUri(rootUri, docId);
+                final Uri childUri = DocumentsContract.buildDocumentUri(uri, docId);
                 icon.setImageURI(childUri);
             } else {
-                icon.setImageDrawable(
-                        DocumentsActivity.resolveDocumentIcon(context, authority, mimeType));
+                icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
+                        context, uri.getAuthority(), mimeType));
             }
 
             title.setText(displayName);
@@ -364,20 +364,6 @@
         }
     }
 
-    private static int getDocumentFlags(Context context, Uri uri) {
-        final Cursor cursor = context.getContentResolver().query(uri, new String[] {
-                DocumentColumns.FLAGS }, null, null, null);
-        try {
-            if (cursor.moveToFirst()) {
-                return getCursorInt(cursor, DocumentColumns.FLAGS);
-            } else {
-                return 0;
-            }
-        } finally {
-            cursor.close();
-        }
-    }
-
     public static String getCursorString(Cursor cursor, String columnName) {
         return cursor.getString(cursor.getColumnIndex(columnName));
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 405ef36..8f2e61d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.DirectoryFragment.getCursorInt;
+import static com.android.documentsui.DirectoryFragment.getCursorLong;
 import static com.android.documentsui.DirectoryFragment.getCursorString;
 
 import android.app.ActionBar;
@@ -25,7 +27,6 @@
 import android.app.Dialog;
 import android.app.DialogFragment;
 import android.app.FragmentManager;
-import android.app.FragmentManager.OnBackStackChangedListener;
 import android.content.ClipData;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -78,6 +79,8 @@
 
 import libcore.io.IoUtils;
 
+import org.json.JSONArray;
+import org.json.JSONException;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -85,12 +88,13 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 
 public class DocumentsActivity extends Activity {
     private static final String TAG = "Documents";
 
-    // TODO: fragment to show recently opened documents
+    // TODO: share backend root cache with recents provider
 
     private static final String TAG_CREATE_DIRECTORY = "create_directory";
 
@@ -106,18 +110,17 @@
     private ActionBarDrawerToggle mDrawerToggle;
 
     private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
-    private static ArrayList<Root> sRoots = Lists.newArrayList();
+    private static HashMap<String, Root> sRoots = Maps.newHashMap();
+
+    // TODO: remove once adapter split by type
+    private static ArrayList<Root> sRootsList = Lists.newArrayList();
 
     private RootsAdapter mRootsAdapter;
     private ListView mRootsList;
 
     private final DisplayState mDisplayState = new DisplayState();
 
-    private Root mCurrentRoot;
-
-    private Uri mCurrentDir;
-    private boolean mCurrentSupportsCreate;
-    private boolean mCurrentSupportsSearch;
+    private LinkedList<Document> mStack = new LinkedList<Document>();
 
     @Override
     public void onCreate(Bundle icicle) {
@@ -148,8 +151,6 @@
         setResult(Activity.RESULT_CANCELED);
         setContentView(R.layout.activity);
 
-        getFragmentManager().addOnBackStackChangedListener(mStackListener);
-
         if (mAction == ACTION_CREATE) {
             final String mimeType = getIntent().getType();
             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
@@ -157,7 +158,7 @@
         }
 
         mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
-        mRootsAdapter = new RootsAdapter(this, sRoots);
+        mRootsAdapter = new RootsAdapter(this, sRootsList);
         mRootsList = (ListView) findViewById(R.id.roots_list);
         mRootsList.setAdapter(mRootsAdapter);
         mRootsList.setOnItemClickListener(mRootsListener);
@@ -168,10 +169,24 @@
         mDrawerLayout.setDrawerListener(mDrawerListener);
         mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
 
-        mDrawerLayout.openDrawer(mRootsList);
-
-        updateActionBar();
         updateRoots();
+
+        // Restore last stack for calling package
+        // TODO: move into async loader
+        final String packageName = getCallingPackage();
+        final Cursor cursor = getContentResolver()
+                .query(RecentsProvider.buildResume(packageName), null, null, null, null);
+        try {
+            if (cursor.moveToFirst()) {
+                final String rawStack = cursor.getString(
+                        cursor.getColumnIndex(RecentsProvider.COL_PATH));
+                restoreStack(rawStack);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        updateDirectoryFragment();
     }
 
     private DrawerListener mDrawerListener = new DrawerListener() {
@@ -205,7 +220,6 @@
     }
 
     public void updateActionBar() {
-        final FragmentManager fm = getFragmentManager();
         final ActionBar actionBar = getActionBar();
 
         actionBar.setDisplayShowHomeEnabled(true);
@@ -223,13 +237,12 @@
 
         } else {
             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
-            if (mCurrentRoot != null) {
-                actionBar.setIcon(mCurrentRoot.icon);
-            }
+            final Root root = getCurrentRoot();
+            actionBar.setIcon(root != null ? root.icon : null);
             actionBar.setTitle(null);
             actionBar.setListNavigationCallbacks(mSortAdapter, mSortListener);
 
-            if (fm.getBackStackEntryCount() > 0) {
+            if (mStack.size() > 1) {
                 mDrawerToggle.setDrawerIndicatorEnabled(false);
             } else {
                 mDrawerToggle.setDrawerIndicatorEnabled(true);
@@ -247,10 +260,10 @@
         mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
             @Override
             public boolean onQueryTextSubmit(String query) {
-                // TODO: clear existing directory stack?
-                final Uri searchUri = DocumentsContract.buildSearchUri(mCurrentDir, query);
-                DirectoryFragment.show(
-                        getFragmentManager(), mCurrentRoot.rootUri, searchUri, query, true);
+                // TODO: use second directory stack for searches?
+                final Document cwd = getCurrentDirectory();
+                final Document searchDoc = Document.fromSearch(cwd.uri, query);
+                onDocumentPicked(searchDoc);
                 mSearchView.setIconified(true);
                 return true;
             }
@@ -268,13 +281,20 @@
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
 
+        final Document cwd = getCurrentDirectory();
+
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
         createDir.setVisible(mAction == ACTION_CREATE);
-        createDir.setEnabled(mCurrentSupportsCreate);
+        createDir.setEnabled(cwd != null && cwd.isCreateSupported());
 
         // TODO: close any search in-progress when hiding
         final MenuItem search = menu.findItem(R.id.menu_search);
-        search.setVisible(mCurrentSupportsSearch);
+        search.setVisible(cwd != null && cwd.isSearchSupported());
+
+        if (mAction == ACTION_CREATE) {
+            final FragmentManager fm = getFragmentManager();
+            SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
+        }
 
         return true;
     }
@@ -287,8 +307,7 @@
 
         final int id = item.getItemId();
         if (id == android.R.id.home) {
-            getFragmentManager().popBackStack();
-            updateActionBar();
+            onBackPressed();
             return true;
         } else if (id == R.id.menu_create_dir) {
             CreateDirectoryFragment.show(getFragmentManager());
@@ -299,12 +318,19 @@
         return super.onOptionsItemSelected(item);
     }
 
-    private OnBackStackChangedListener mStackListener = new OnBackStackChangedListener() {
-        @Override
-        public void onBackStackChanged() {
-            updateActionBar();
+    @Override
+    public void onBackPressed() {
+        final int size = mStack.size();
+        if (size > 1) {
+            mStack.pop();
+            updateDirectoryFragment();
+        } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsList)) {
+            // TODO: open root drawer once we can capture back key
+            super.onBackPressed();
+        } else {
+            super.onBackPressed();
         }
-    };
+    }
 
     // TODO: support additional sort orders
     private BaseAdapter mSortAdapter = new BaseAdapter() {
@@ -340,12 +366,9 @@
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
 
-            final FragmentManager fm = getFragmentManager();
-            final int count = fm.getBackStackEntryCount();
-            if (count > 0) {
-                title.setText(fm.getBackStackEntryAt(count - 1).getBreadCrumbTitle());
-            } else if (mCurrentRoot != null) {
-                title.setText(mCurrentRoot.title);
+            final Document cwd = getCurrentDirectory();
+            if (cwd != null) {
+                title.setText(cwd.displayName);
             } else {
                 title.setText(null);
             }
@@ -377,28 +400,42 @@
         }
     };
 
+    public Root getCurrentRoot() {
+        final Document cwd = getCurrentDirectory();
+        if (cwd != null) {
+            return sRoots.get(DocumentsContract.getRootId(cwd.uri));
+        } else {
+            return null;
+        }
+    }
+
+    public Document getCurrentDirectory() {
+        return mStack.peek();
+    }
+
     public DisplayState getDisplayState() {
         return mDisplayState;
     }
 
-    public void onDirectoryChanged(Uri uri, int flags) {
-        mCurrentDir = uri;
-        mCurrentSupportsCreate = (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
-        mCurrentSupportsSearch = (flags & DocumentsContract.FLAG_SUPPORTS_SEARCH) != 0;
-
-        if (mAction == ACTION_CREATE) {
-            final FragmentManager fm = getFragmentManager();
-            SaveFragment.get(fm).setSaveEnabled(mCurrentSupportsCreate);
+    private void updateDirectoryFragment() {
+        final FragmentManager fm = getFragmentManager();
+        final Document cwd = getCurrentDirectory();
+        if (cwd != null) {
+            DirectoryFragment.show(fm, cwd.uri);
+            mDrawerLayout.closeDrawer(mRootsList);
+        } else {
+            mDrawerLayout.openDrawer(mRootsList);
         }
-
+        updateActionBar();
         invalidateOptionsMenu();
+        dumpStack();
     }
 
     public void onDocumentPicked(Document doc) {
         final FragmentManager fm = getFragmentManager();
         if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
-            // Nested directory picked, recurse using new fragment
-            DirectoryFragment.show(fm, doc.rootUri, doc.uri, doc.displayName, true);
+            mStack.push(doc);
+            updateDirectoryFragment();
         } else if (mAction == ACTION_OPEN) {
             // Explicit file picked, return
             onFinished(doc.uri);
@@ -424,22 +461,70 @@
         values.put(DocumentColumns.MIME_TYPE, mimeType);
         values.put(DocumentColumns.DISPLAY_NAME, displayName);
 
-        final Uri uri = getContentResolver().insert(mCurrentDir, values);
-        if (uri != null) {
-            onFinished(uri);
+        final Document cwd = getCurrentDirectory();
+        final Uri childUri = getContentResolver().insert(cwd.uri, values);
+        if (childUri != null) {
+            onFinished(childUri);
         } else {
             Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
         }
     }
 
+    private String saveStack() {
+        final JSONArray stack = new JSONArray();
+        for (int i = 0; i < mStack.size(); i++) {
+            stack.put(mStack.get(i).uri);
+        }
+        return stack.toString();
+    }
+
+    private void restoreStack(String rawStack) {
+        Log.d(TAG, "restoreStack: " + rawStack);
+        mStack.clear();
+        try {
+            final JSONArray stack = new JSONArray(rawStack);
+            for (int i = 0; i < stack.length(); i++) {
+                final Uri uri = Uri.parse(stack.getString(i));
+                final Document doc = Document.fromUri(getContentResolver(), uri);
+                mStack.add(doc);
+            }
+        } catch (JSONException e) {
+            Log.w(TAG, "Failed to decode stack", e);
+        }
+    }
+
     private void onFinished(Uri... uris) {
         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
+        final ContentResolver resolver = getContentResolver();
+        final ContentValues values = new ContentValues();
+
+        final String stack = saveStack();
+        if (mAction == ACTION_CREATE) {
+            // Remember stack for last create
+            values.clear();
+            values.put(RecentsProvider.COL_PATH, stack);
+            resolver.insert(RecentsProvider.buildRecentCreate(), values);
+
+        } else if (mAction == ACTION_OPEN) {
+            // Remember opened items
+            for (Uri uri : uris) {
+                values.clear();
+                values.put(RecentsProvider.COL_URI, uri.toString());
+                resolver.insert(RecentsProvider.buildRecentOpen(), values);
+            }
+        }
+
+        // Remember location for next app launch
+        final String packageName = getCallingPackage();
+        values.clear();
+        values.put(RecentsProvider.COL_PATH, stack);
+        resolver.insert(RecentsProvider.buildResume(packageName), values);
+
         final Intent intent = new Intent();
         if (uris.length == 1) {
             intent.setData(uris[0]);
         } else if (uris.length > 1) {
-            final ContentResolver resolver = getContentResolver();
             final ClipData clipData = new ClipData(null, mAcceptMimes, new ClipData.Item(uris[0]));
             for (int i = 1; i < uris.length; i++) {
                 clipData.addItem(new ClipData.Item(uris[i]));
@@ -470,8 +555,8 @@
 
     public static class Root {
         public DocumentsProviderInfo info;
+        public String rootId;
         public int rootType;
-        public Uri rootUri;
         public Uri uri;
         public Drawable icon;
         public String title;
@@ -479,21 +564,18 @@
 
         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();
 
+            final Root root = new Root();
             root.info = info;
+            root.rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
             root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
-            root.rootUri = DocumentsContract.buildRootUri(info.providerInfo.authority, rootId);
             root.uri = DocumentsContract.buildDocumentUri(
-                    root.rootUri, DocumentsContract.ROOT_DOC_ID);
+                    info.providerInfo.authority, root.rootId, DocumentsContract.ROOT_DOC_ID);
             root.icon = info.providerInfo.loadIcon(pm);
             root.title = info.providerInfo.loadLabel(pm).toString();
             root.summary = null;
 
-
             final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
             if (icon != 0) {
                 try {
@@ -529,24 +611,28 @@
     }
 
     public static class Document {
-        public Uri rootUri;
         public Uri uri;
         public String mimeType;
         public String displayName;
+        public long lastModified;
+        public int flags;
 
-        public static Document fromCursor(Uri rootUri, Cursor cursor) {
-            final Document doc = new Document();
+        public static Document fromCursor(Uri parent, Cursor cursor) {
+            final String authority = parent.getAuthority();
+            final String rootId = DocumentsContract.getRootId(parent);
             final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
-            doc.rootUri = rootUri;
-            doc.uri = DocumentsContract.buildDocumentUri(rootUri, docId);
+
+            final Document doc = new Document();
+            doc.uri = DocumentsContract.buildDocumentUri(authority, rootId, docId);
             doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
             doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
+            doc.lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
+            doc.flags = getCursorInt(cursor, DocumentColumns.FLAGS);
             return doc;
         }
 
-        public static Document fromUri(ContentResolver resolver, Uri rootUri, Uri uri) {
+        public static Document fromUri(ContentResolver resolver, Uri uri) {
             final Document doc = new Document();
-            doc.rootUri = rootUri;
             doc.uri = uri;
 
             final Cursor cursor = resolver.query(uri, null, null, null, null);
@@ -556,12 +642,41 @@
                 }
                 doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
                 doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
+                doc.lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
+                doc.flags = getCursorInt(cursor, DocumentColumns.FLAGS);
             } finally {
                 cursor.close();
             }
 
             return doc;
         }
+
+        public static Document fromSearch(Uri relatedUri, String query) {
+            final Document doc = new Document();
+            doc.uri = DocumentsContract.buildSearchUri(relatedUri, query);
+            doc.mimeType = DocumentsContract.MIME_TYPE_DIRECTORY;
+            doc.displayName = query;
+            doc.lastModified = System.currentTimeMillis();
+            doc.flags = 0;
+            return doc;
+        }
+
+        @Override
+        public String toString() {
+            return "'" + displayName + "' " + uri;
+        }
+
+        public boolean isCreateSupported() {
+            return (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
+        }
+
+        public boolean isSearchSupported() {
+            return (flags & DocumentsContract.FLAG_SUPPORTS_SEARCH) != 0;
+        }
+
+        public boolean isThumbnailSupported() {
+            return (flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0;
+        }
     }
 
     public static boolean mimeMatches(String filter, String[] tests) {
@@ -622,6 +737,7 @@
     private void updateRoots() {
         sProviders.clear();
         sRoots.clear();
+        sRootsList.clear();
 
         final PackageManager pm = getPackageManager();
         final List<ProviderInfo> providers = pm.queryContentProviders(
@@ -643,7 +759,9 @@
                 final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
                 try {
                     while (cursor.moveToNext()) {
-                        sRoots.add(Root.fromCursor(this, info, cursor));
+                        final Root root = Root.fromCursor(this, info, cursor);
+                        sRoots.put(root.rootId, root);
+                        sRootsList.add(root);
                     }
                 } finally {
                     cursor.close();
@@ -710,19 +828,25 @@
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             // Clear entire backstack and start in new root
-            final FragmentManager fm = getFragmentManager();
-            while (fm.getBackStackEntryCount() > 0) {
-                fm.popBackStackImmediate();
-            }
+            mStack.clear();
 
-            mCurrentRoot = mRootsAdapter.getItem(position);
-            DirectoryFragment.show(getFragmentManager(), mCurrentRoot.rootUri, mCurrentRoot.uri,
-                    mCurrentRoot.title, false);
+            final Root root = mRootsAdapter.getItem(position);
+
+            final ContentResolver resolver = getContentResolver();
+            final Document doc = Document.fromUri(resolver, root.uri);
+            onDocumentPicked(doc);
 
             mDrawerLayout.closeDrawers();
         }
     };
 
+    private void dumpStack() {
+        Log.d(TAG, "Current stack:");
+        for (Document doc : mStack) {
+            Log.d(TAG, "--> " + doc);
+        }
+    }
+
     public static class RootsAdapter extends ArrayAdapter<Root> {
         public RootsAdapter(Context context, List<Root> list) {
             super(context, android.R.layout.simple_list_item_1, list);
@@ -780,12 +904,13 @@
                     values.put(DocumentColumns.DISPLAY_NAME, displayName);
 
                     final DocumentsActivity activity = (DocumentsActivity) getActivity();
-                    final Uri uri = resolver.insert(activity.mCurrentDir, values);
-                    if (uri != null) {
+                    final Document cwd = activity.getCurrentDirectory();
+
+                    final Uri childUri = resolver.insert(cwd.uri, values);
+                    if (childUri != null) {
                         // Navigate into newly created child
-                        final Document doc = Document.fromUri(
-                                resolver, activity.mCurrentRoot.rootUri, uri);
-                        activity.onDocumentPicked(doc);
+                        final Document childDoc = Document.fromUri(resolver, childUri);
+                        activity.onDocumentPicked(childDoc);
                     } else {
                         Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
                     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
index e6ee614..dbcb039 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
@@ -17,6 +17,7 @@
 package com.android.documentsui;
 
 import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.UriMatcher;
@@ -30,6 +31,9 @@
 public class RecentsProvider extends ContentProvider {
     private static final String TAG = "RecentsProvider";
 
+    // TODO: offer view of recents that handles backend root resolution before
+    // returning cursor, include extra columns
+
     public static final String AUTHORITY = "com.android.documentsui.recents";
 
     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@@ -53,13 +57,29 @@
      * starting with root.
      */
     public static final String COL_PATH = "path";
+    public static final String COL_URI = "uri";
     public static final String COL_PACKAGE_NAME = "package_name";
     public static final String COL_TIMESTAMP = "timestamp";
 
+    public static Uri buildRecentOpen() {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(AUTHORITY).appendPath("recent_open").build();
+    }
+
+    public static Uri buildRecentCreate() {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(AUTHORITY).appendPath("recent_create").build();
+    }
+
+    public static Uri buildResume(String packageName) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(AUTHORITY).appendPath("resume").appendPath(packageName).build();
+    }
+
     private DatabaseHelper mHelper;
 
     private static class DatabaseHelper extends SQLiteOpenHelper {
-        private static final String DB_NAME = "recents";
+        private static final String DB_NAME = "recents.db";
 
         private static final int VERSION_INIT = 1;
 
@@ -70,19 +90,19 @@
         @Override
         public void onCreate(SQLiteDatabase db) {
             db.execSQL("CREATE TABLE " + TABLE_RECENT_OPEN + " (" +
-                    COL_PATH + " TEXT," +
-                    COL_TIMESTAMP + " INTEGER," +
+                    COL_URI + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
+                    COL_TIMESTAMP + " INTEGER" +
                     ")");
 
             db.execSQL("CREATE TABLE " + TABLE_RECENT_CREATE + " (" +
-                    COL_PATH + " TEXT," +
-                    COL_TIMESTAMP + " INTEGER," +
+                    COL_PATH + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
+                    COL_TIMESTAMP + " INTEGER" +
                     ")");
 
             db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
                     COL_PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
                     COL_PATH + " TEXT," +
-                    COL_TIMESTAMP + " INTEGER," +
+                    COL_TIMESTAMP + " INTEGER" +
                     ")");
         }
 
@@ -136,11 +156,13 @@
         final SQLiteDatabase db = mHelper.getWritableDatabase();
         switch (sMatcher.match(uri)) {
             case URI_RECENT_OPEN: {
+                values.put(COL_TIMESTAMP, System.currentTimeMillis());
                 db.insert(TABLE_RECENT_OPEN, null, values);
                 db.delete(TABLE_RECENT_OPEN, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
                 return uri;
             }
             case URI_RECENT_CREATE: {
+                values.put(COL_TIMESTAMP, System.currentTimeMillis());
                 db.insert(TABLE_RECENT_CREATE, null, values);
                 db.delete(TABLE_RECENT_CREATE, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
                 return uri;
@@ -148,6 +170,7 @@
             case URI_RESUME: {
                 final String packageName = uri.getPathSegments().get(1);
                 values.put(COL_PACKAGE_NAME, packageName);
+                values.put(COL_TIMESTAMP, System.currentTimeMillis());
                 db.insert(TABLE_RESUME, null, values);
                 return uri;
             }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index dd7472b..5c12484 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -110,15 +110,15 @@
                 return cursor;
             }
             case URI_ROOTS_ID: {
-                final String root = uri.getPathSegments().get(1);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
 
                 final MatrixCursor cursor = new MatrixCursor(rootsProjection);
-                includeRoot(cursor, mRoots.get(root));
+                includeRoot(cursor, root);
                 return cursor;
             }
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 final MatrixCursor cursor = new MatrixCursor(docsProjection);
                 final File file = docIdToFile(root, docId);
@@ -126,20 +126,22 @@
                 return cursor;
             }
             case URI_DOCS_ID_CONTENTS: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 final MatrixCursor cursor = new MatrixCursor(docsProjection);
                 final File parent = docIdToFile(root, docId);
+
                 for (File file : parent.listFiles()) {
                     includeFile(cursor, root, file);
                 }
+
                 return cursor;
             }
             case URI_DOCS_ID_SEARCH: {
-                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 Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
+                final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
 
                 final MatrixCursor cursor = new MatrixCursor(docsProjection);
                 final File parent = docIdToFile(root, docId);
@@ -158,6 +160,7 @@
                         }
                     }
                 }
+
                 return cursor;
             }
             default: {
@@ -218,16 +221,24 @@
 
         final String docId = fileToDocId(root, file);
         final long id = docId.hashCode();
+
+        final String displayName;
+        if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+            displayName = root.title;
+        } else {
+            displayName = file.getName();
+        }
+
         cursor.addRow(new Object[] {
-                id, file.getName(), file.length(), docId, mimeType, file.lastModified(), flags });
+                id, displayName, file.length(), docId, mimeType, file.lastModified(), flags });
     }
 
     @Override
     public String getType(Uri uri) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
                 return getTypeForFile(docIdToFile(root, docId));
             }
             default: {
@@ -261,8 +272,8 @@
     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 // TODO: offer as thumbnail
                 final File file = docIdToFile(root, docId);
@@ -278,8 +289,8 @@
     public Uri insert(Uri uri, ContentValues values) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 final File parent = docIdToFile(root, docId);
 
@@ -317,8 +328,8 @@
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 final File file = docIdToFile(root, docId);
                 final File newFile = new File(
@@ -335,8 +346,8 @@
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {
-                final Root root = mRoots.get(uri.getPathSegments().get(1));
-                final String docId = uri.getPathSegments().get(3);
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
 
                 final File file = docIdToFile(root, docId);
                 return file.delete() ? 1 : 0;