Merge "Reduce code duplication between DocumentsActivity and StandaloneActivity."
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 8039b71..efe71d4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -16,17 +16,47 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
+import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
+import static com.android.documentsui.DirectoryFragment.ANIM_UP;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.Executor;
 
+import libcore.io.IoUtils;
 import android.app.Activity;
 import android.app.Fragment;
-import android.content.pm.ResolveInfo;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.util.Log;
 import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnActionExpandListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.SearchView;
+import android.widget.SearchView.OnQueryTextListener;
+import android.widget.TextView;
 
+import com.android.documentsui.RecentsProvider.ResumeColumns;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.model.DurableUtils;
@@ -34,20 +64,125 @@
 import com.google.common.collect.Maps;
 
 abstract class BaseActivity extends Activity {
+
+    static final String EXTRA_STATE = "state";
+
+    private final String mTag;
+    RootsCache mRoots;
+
     public abstract State getDisplayState();
-    public abstract RootInfo getCurrentRoot();
-    public abstract void onStateChanged();
-    public abstract void setRootsDrawerOpen(boolean open);
     public abstract void onDocumentPicked(DocumentInfo doc);
     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
-    public abstract DocumentInfo getCurrentDirectory();
-    public abstract void setPending(boolean pending);
-    public abstract void onStackPicked(DocumentStack stack);
-    public abstract void onPickRequested(DocumentInfo pickTarget);
-    public abstract void onAppPicked(ResolveInfo info);
-    public abstract void onRootPicked(RootInfo root, boolean closeDrawer);
-    public abstract void onSaveRequested(DocumentInfo replaceTarget);
-    public abstract void onSaveRequested(String mimeType, String displayName);
+    abstract void onTaskFinished(Uri... uris);
+    abstract void onDirectoryChanged(int anim);
+    abstract void updateActionBar();
+    abstract void saveStackBlocking();
+
+    public BaseActivity(String tag) {
+        mTag = tag;
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        mRoots = DocumentsApplication.getRootsCache(this);
+    }
+
+    void onStackRestored(boolean restored, boolean external) {}
+
+    void onRootPicked(RootInfo root) {
+        State state = getDisplayState();
+
+        // Clear entire backstack and start in new root
+        state.stack.root = root;
+        state.stack.clear();
+        state.stackTouched = true;
+
+        if (!mRoots.isRecentsRoot(root)) {
+            new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
+        } else {
+            onCurrentDirectoryChanged(ANIM_SIDE);
+        }
+    }
+
+    void expandMenus(Menu menu) {
+        for (int i = 0; i < menu.size(); i++) {
+            final MenuItem item = menu.getItem(i);
+            switch (item.getItemId()) {
+                case R.id.menu_advanced:
+                case R.id.menu_file_size:
+                    break;
+                default:
+                    item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+            }
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        final int id = item.getItemId();
+        if (id == android.R.id.home) {
+            onBackPressed();
+            return true;
+        } else if (id == R.id.menu_create_dir) {
+            CreateDirectoryFragment.show(getFragmentManager());
+            return true;
+        } else if (id == R.id.menu_search) {
+            return false;
+        } else if (id == R.id.menu_sort_name) {
+            setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
+            return true;
+        } else if (id == R.id.menu_sort_date) {
+            setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
+            return true;
+        } else if (id == R.id.menu_sort_size) {
+            setUserSortOrder(State.SORT_ORDER_SIZE);
+            return true;
+        } else if (id == R.id.menu_grid) {
+            setUserMode(State.MODE_GRID);
+            return true;
+        } else if (id == R.id.menu_list) {
+            setUserMode(State.MODE_LIST);
+            return true;
+        } else if (id == R.id.menu_advanced) {
+            setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
+            return true;
+        } else if (id == R.id.menu_file_size) {
+            setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
+            return true;
+        } else if (id == R.id.menu_settings) {
+            final RootInfo root = getCurrentRoot();
+            final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
+            intent.setDataAndType(DocumentsContract.buildRootUri(root.authority, root.rootId),
+                    DocumentsContract.Root.MIME_TYPE_ITEM);
+            startActivity(intent);
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * Call this when directory changes. Prior to root fragment update
+     * the (abstract) directoryChanged method will be called.
+     * @param anim
+     */
+    final void onCurrentDirectoryChanged(int anim) {
+        onDirectoryChanged(anim);
+
+        final RootsFragment roots = RootsFragment.get(getFragmentManager());
+        if (roots != null) {
+            roots.onCurrentRootChanged();
+        }
+
+        updateActionBar();
+        invalidateOptionsMenu();
+    }
+
+    final String getCallingPackageMaybeExtra() {
+        final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
+        return (extra != null) ? extra : getCallingPackage();
+    }
 
     public static BaseActivity get(Fragment fragment) {
         return (BaseActivity) fragment.getActivity();
@@ -169,4 +304,382 @@
             }
         };
     }
+
+    void setDisplayAdvancedDevices(boolean display) {
+        State state = getDisplayState();
+        LocalPreferences.setDisplayAdvancedDevices(this, display);
+        state.showAdvanced = state.forceAdvanced | display;
+        RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
+        invalidateOptionsMenu();
+    }
+
+    void setDisplayFileSize(boolean display) {
+        LocalPreferences.setDisplayFileSize(this, display);
+        getDisplayState().showSize = display;
+        DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
+        invalidateOptionsMenu();
+    }
+
+    void onStateChanged() {
+        invalidateOptionsMenu();
+    }
+
+    /**
+     * Set state sort order based on explicit user action.
+     */
+    void setUserSortOrder(int sortOrder) {
+        getDisplayState().userSortOrder = sortOrder;
+        DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
+    }
+
+    /**
+     * Set state mode based on explicit user action.
+     */
+    void setUserMode(int mode) {
+        getDisplayState().userMode = mode;
+        DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
+    }
+
+    void setPending(boolean pending) {
+        final SaveFragment save = SaveFragment.get(getFragmentManager());
+        if (save != null) {
+            save.setPending(pending);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle state) {
+        super.onSaveInstanceState(state);
+        state.putParcelable(EXTRA_STATE, getDisplayState());
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle state) {
+        super.onRestoreInstanceState(state);
+    }
+
+    RootInfo getCurrentRoot() {
+        State state = getDisplayState();
+        if (state.stack.root != null) {
+            return state.stack.root;
+        } else {
+            return mRoots.getRecentsRoot();
+        }
+    }
+
+    public DocumentInfo getCurrentDirectory() {
+        return getDisplayState().stack.peek();
+    }
+
+    public Executor getCurrentExecutor() {
+        final DocumentInfo cwd = getCurrentDirectory();
+        if (cwd != null && cwd.authority != null) {
+            return ProviderExecutor.forAuthority(cwd.authority);
+        } else {
+            return AsyncTask.THREAD_POOL_EXECUTOR;
+        }
+    }
+
+    public void onStackPicked(DocumentStack stack) {
+        try {
+            // Update the restored stack to ensure we have freshest data
+            stack.updateDocuments(getContentResolver());
+
+            State state = getDisplayState();
+            state.stack = stack;
+            state.stackTouched = true;
+            onCurrentDirectoryChanged(ANIM_SIDE);
+
+        } catch (FileNotFoundException e) {
+            Log.w(mTag, "Failed to restore stack: " + e);
+        }
+    }
+
+    final class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
+        private RootInfo mRoot;
+
+        public PickRootTask(RootInfo root) {
+            mRoot = root;
+        }
+
+        @Override
+        protected DocumentInfo doInBackground(Void... params) {
+            try {
+                final Uri uri = DocumentsContract.buildDocumentUri(
+                        mRoot.authority, mRoot.documentId);
+                return DocumentInfo.fromUri(getContentResolver(), uri);
+            } catch (FileNotFoundException e) {
+                Log.w(mTag, "Failed to find root", e);
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(DocumentInfo result) {
+            if (result != null) {
+                State state = getDisplayState();
+                state.stack.push(result);
+                state.stackTouched = true;
+                onCurrentDirectoryChanged(ANIM_SIDE);
+            }
+        }
+    }
+
+    final class RestoreStackTask extends AsyncTask<Void, Void, Void> {
+        private volatile boolean mRestoredStack;
+        private volatile boolean mExternal;
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            State state = getDisplayState();
+            RootsCache roots = DocumentsApplication.getRootsCache(BaseActivity.this);
+
+            // Restore last stack for calling package
+            final String packageName = getCallingPackageMaybeExtra();
+            final Cursor cursor = getContentResolver()
+                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
+            try {
+                if (cursor.moveToFirst()) {
+                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
+                    final byte[] rawStack = cursor.getBlob(
+                            cursor.getColumnIndex(ResumeColumns.STACK));
+                    DurableUtils.readFromArray(rawStack, state.stack);
+                    mRestoredStack = true;
+                }
+            } catch (IOException e) {
+                Log.w(mTag, "Failed to resume: " + e);
+            } finally {
+                IoUtils.closeQuietly(cursor);
+            }
+
+            if (mRestoredStack) {
+                // Update the restored stack to ensure we have freshest data
+                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(state);
+                try {
+                    state.stack.updateRoot(matchingRoots);
+                    state.stack.updateDocuments(getContentResolver());
+                } catch (FileNotFoundException e) {
+                    Log.w(mTag, "Failed to restore stack: " + e);
+                    state.stack.reset();
+                    mRestoredStack = false;
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            if (isDestroyed()) return;
+            getDisplayState().restored = true;
+            onCurrentDirectoryChanged(ANIM_NONE);
+
+            onStackRestored(mRestoredStack, mExternal);
+
+            getDisplayState().restored = true;
+            onCurrentDirectoryChanged(ANIM_NONE);
+        }
+    }
+
+    final class ItemSelectedListener implements OnItemSelectedListener {
+
+        boolean mIgnoreNextNavigation;
+
+        @Override
+        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+            if (mIgnoreNextNavigation) {
+                mIgnoreNextNavigation = false;
+                return;
+            }
+
+            State state = getDisplayState();
+            while (state.stack.size() > position + 1) {
+                state.stackTouched = true;
+                state.stack.pop();
+            }
+            onCurrentDirectoryChanged(ANIM_UP);
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> parent) {
+            // Ignored
+        }
+    }
+
+    /**
+     * Class providing toolbar with runtime access to useful activity data.
+     */
+    final class StackAdapter extends BaseAdapter {
+        @Override
+        public int getCount() {
+            return getDisplayState().stack.size();
+        }
+
+        @Override
+        public DocumentInfo getItem(int position) {
+            State state = getDisplayState();
+            return state.stack.get(state.stack.size() - position - 1);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.item_subdir_title, parent, false);
+            }
+
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final DocumentInfo doc = getItem(position);
+
+            if (position == 0) {
+                final RootInfo root = getCurrentRoot();
+                title.setText(root.title);
+            } else {
+                title.setText(doc.displayName);
+            }
+
+            return convertView;
+        }
+
+        @Override
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.item_subdir, parent, false);
+            }
+
+            final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final DocumentInfo doc = getItem(position);
+
+            if (position == 0) {
+                final RootInfo root = getCurrentRoot();
+                title.setText(root.title);
+                subdir.setVisibility(View.GONE);
+            } else {
+                title.setText(doc.displayName);
+                subdir.setVisibility(View.VISIBLE);
+            }
+
+            return convertView;
+        }
+    }
+
+    /**
+     * Facade over the various search parts in the menu.
+     */
+    final class SearchManager implements
+            SearchView.OnCloseListener, OnActionExpandListener, OnQueryTextListener {
+
+        protected boolean mSearchExpanded;
+        protected boolean mIgnoreNextClose;
+        protected boolean mIgnoreNextCollapse;
+
+        private MenuItem mMenu;
+        private SearchView mView;
+
+        public void install(MenuItem menu) {
+            assert(mMenu == null);
+            mMenu = menu;
+            mView = (SearchView) menu.getActionView();
+
+            mMenu.setOnActionExpandListener(this);
+            mView.setOnQueryTextListener(this);
+            mView.setOnCloseListener(this);
+        }
+
+        /**
+         * @param root Info about the current directory.
+         */
+        void update(RootInfo root) {
+            if (mMenu == null) {
+                Log.d(mTag, "showMenu called before Search MenuItem installed.");
+                return;
+            }
+            State state = getDisplayState();
+            if (state.currentSearch != null) {
+                mMenu.expandActionView();
+
+                mView.setIconified(false);
+                mView.clearFocus();
+                mView.setQuery(state.currentSearch, false);
+            } else {
+                mIgnoreNextClose = true;
+                mView.setIconified(true);
+                mView.clearFocus();
+
+                mIgnoreNextCollapse = true;
+                mMenu.collapseActionView();
+            }
+
+            showMenu(root != null
+                    && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0));
+        }
+
+        void showMenu(boolean visible) {
+            if (mMenu == null) {
+                Log.d(mTag, "showMenu called before Search MenuItem installed.");
+                return;
+            }
+            mMenu.setVisible(visible);
+        }
+
+        boolean isSearching() {
+            return getDisplayState().currentSearch != null;
+        }
+
+        boolean isExpanded() {
+            return mSearchExpanded;
+        }
+
+        @Override
+        public boolean onClose() {
+            mSearchExpanded = false;
+            if (mIgnoreNextClose) {
+                mIgnoreNextClose = false;
+                return false;
+            }
+
+            getDisplayState().currentSearch = null;
+            onCurrentDirectoryChanged(ANIM_NONE);
+            return false;
+        }
+        @Override
+        public boolean onMenuItemActionExpand(MenuItem item) {
+            mSearchExpanded = true;
+            updateActionBar();
+            return true;
+        }
+
+        @Override
+        public boolean onMenuItemActionCollapse(MenuItem item) {
+            mSearchExpanded = false;
+            if (mIgnoreNextCollapse) {
+                mIgnoreNextCollapse = false;
+                return true;
+            }
+
+            getDisplayState().currentSearch = null;
+            onCurrentDirectoryChanged(ANIM_NONE);
+            return true;
+        }
+        @Override
+        public boolean onQueryTextSubmit(String query) {
+            mSearchExpanded = true;
+            getDisplayState().currentSearch = query;
+            mView.clearFocus();
+            onCurrentDirectoryChanged(ANIM_NONE);
+            return true;
+        }
+
+        @Override
+        public boolean onQueryTextChange(String newText) {
+            return false;
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index ef644f4..7cf58cc 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -28,7 +28,6 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
-
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.Fragment;
@@ -311,8 +310,9 @@
                 updateDisplayState();
 
                 // When launched into empty recents, show drawer
-                if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {
-                    ((BaseActivity) context).setRootsDrawerOpen(true);
+                if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched &&
+                        context instanceof DocumentsActivity) {
+                    ((DocumentsActivity) context).setRootsDrawerOpen(true);
                 }
 
                 // Restore any previous instance state
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index a2a789f..9d828de 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -21,16 +21,17 @@
 import static com.android.documentsui.BaseActivity.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
 import static com.android.documentsui.BaseActivity.State.ACTION_OPEN;
-import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_TREE;
 import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_COPY_DESTINATION;
-import static com.android.documentsui.BaseActivity.State.MODE_GRID;
-import static com.android.documentsui.BaseActivity.State.MODE_LIST;
+import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_TREE;
 import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
 import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
-import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
 import static com.android.documentsui.DirectoryFragment.ANIM_UP;
 
+import java.util.Arrays;
+import java.util.List;
+
 import android.app.Activity;
+import android.app.Fragment;
 import android.app.FragmentManager;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
@@ -42,7 +43,6 @@
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.graphics.Point;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -54,51 +54,27 @@
 import android.support.v4.widget.DrawerLayout;
 import android.support.v4.widget.DrawerLayout.DrawerListener;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.MenuItem.OnActionExpandListener;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
 import android.widget.BaseAdapter;
-import android.widget.ImageView;
-import android.widget.SearchView;
-import android.widget.SearchView.OnQueryTextListener;
 import android.widget.Spinner;
-import android.widget.TextView;
 import android.widget.Toast;
 import android.widget.Toolbar;
 
-import libcore.io.IoUtils;
-
 import com.android.documentsui.RecentsProvider.RecentColumns;
 import com.android.documentsui.RecentsProvider.ResumeColumns;
 import com.android.documentsui.model.DocumentInfo;
-import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.model.DurableUtils;
 import com.android.documentsui.model.RootInfo;
 
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Executor;
-
 public class DocumentsActivity extends BaseActivity {
+    private static final int CODE_FORWARD = 42;
     public static final String TAG = "Documents";
 
-    private static final String EXTRA_STATE = "state";
-
-    private static final int CODE_FORWARD = 42;
-
     private boolean mShowAsDialog;
 
-    private SearchView mSearchView;
-
     private Toolbar mToolbar;
     private Spinner mToolbarStack;
 
@@ -110,21 +86,20 @@
 
     private DirectoryContainerView mDirectoryContainer;
 
-    private boolean mIgnoreNextNavigation;
-    private boolean mIgnoreNextClose;
-    private boolean mIgnoreNextCollapse;
-
-    private boolean mSearchExpanded;
-
-    private RootsCache mRoots;
     private State mState;
 
+    private SearchManager mSearchManager;
+    private ItemSelectedListener mStackListener;
+    private BaseAdapter mStackAdapter;
+
+    public DocumentsActivity() {
+        super(TAG);
+    }
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        mRoots = DocumentsApplication.getRootsCache(this);
-
         setResult(Activity.RESULT_CANCELED);
         setContentView(R.layout.activity);
 
@@ -157,16 +132,16 @@
 
         mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
 
-        if (icicle != null) {
-            mState = icicle.getParcelable(EXTRA_STATE);
-        } else {
-            buildDefaultState();
-        }
+        mState = (icicle != null)
+                ? icicle.<State>getParcelable(EXTRA_STATE)
+                : buildDefaultState();
 
         mToolbar = (Toolbar) findViewById(R.id.toolbar);
         mToolbar.setTitleTextAppearance(context,
                 android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
 
+        mStackAdapter = new StackAdapter();
+        mStackListener = new ItemSelectedListener();
         mToolbarStack = (Spinner) findViewById(R.id.stack);
         mToolbarStack.setOnItemSelectedListener(mStackListener);
 
@@ -176,6 +151,7 @@
                     android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
         }
 
+        mSearchManager = new SearchManager();
         setActionBar(mToolbar);
 
         // Hide roots when we're managing a specific root
@@ -220,55 +196,57 @@
         }
     }
 
-    private void buildDefaultState() {
-        mState = new State();
+    private State buildDefaultState() {
+        State state = new State();
 
         final Intent intent = getIntent();
         final String action = intent.getAction();
         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
-            mState.action = ACTION_OPEN;
+            state.action = ACTION_OPEN;
         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
-            mState.action = ACTION_CREATE;
+            state.action = ACTION_CREATE;
         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
-            mState.action = ACTION_GET_CONTENT;
+            state.action = ACTION_GET_CONTENT;
         } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
-            mState.action = ACTION_OPEN_TREE;
+            state.action = ACTION_OPEN_TREE;
         } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
-            mState.action = ACTION_MANAGE;
+            state.action = ACTION_MANAGE;
         } else if (DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT.equals(action)) {
-            mState.action = ACTION_BROWSE;
+            state.action = ACTION_BROWSE;
         } else if (DocumentsIntent.ACTION_OPEN_COPY_DESTINATION.equals(action)) {
-            mState.action = ACTION_OPEN_COPY_DESTINATION;
+            state.action = ACTION_OPEN_COPY_DESTINATION;
         }
 
-        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
-            mState.allowMultiple = intent.getBooleanExtra(
+        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
+            state.allowMultiple = intent.getBooleanExtra(
                     Intent.EXTRA_ALLOW_MULTIPLE, false);
         }
 
-        if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
-            mState.acceptMimes = new String[] { "*/*" };
-            mState.allowMultiple = true;
+        if (state.action == ACTION_MANAGE || state.action == ACTION_BROWSE) {
+            state.acceptMimes = new String[] { "*/*" };
+            state.allowMultiple = true;
         } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
-            mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
+            state.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
         } else {
-            mState.acceptMimes = new String[] { intent.getType() };
+            state.acceptMimes = new String[] { intent.getType() };
         }
 
-        mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
-        mState.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
-        mState.showAdvanced = mState.forceAdvanced
+        state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
+        state.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
+        state.showAdvanced = state.forceAdvanced
                 | LocalPreferences.getDisplayAdvancedDevices(this);
 
-        if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
-            mState.showSize = true;
+        if (state.action == ACTION_MANAGE || state.action == ACTION_BROWSE) {
+            state.showSize = true;
         } else {
-            mState.showSize = LocalPreferences.getDisplayFileSize(this);
+            state.showSize = LocalPreferences.getDisplayFileSize(this);
         }
-        if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
-            mState.directoryCopy = intent.getBooleanExtra(
+        if (state.action == ACTION_OPEN_COPY_DESTINATION) {
+            state.directoryCopy = intent.getBooleanExtra(
                     BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, false);
         }
+
+        return state;
     }
 
     private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
@@ -290,7 +268,7 @@
             mState.restored = true;
 
             if (root != null) {
-                onRootPicked(root, true);
+                onRootPicked(root);
             } else {
                 Log.w(TAG, "Failed to find root: " + mRootUri);
                 finish();
@@ -298,71 +276,55 @@
         }
     }
 
-    private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
-        private volatile boolean mRestoredStack;
-        private volatile boolean mExternal;
+    @Override
+    void onStackRestored(boolean restored, boolean external) {
+        // Show drawer when no stack restored, but only when requesting
+        // non-visual content. However, if we last used an external app,
+        // drawer is always shown.
 
-        @Override
-        protected Void doInBackground(Void... params) {
-            // Restore last stack for calling package
-            final String packageName = getCallingPackageMaybeExtra();
-            final Cursor cursor = getContentResolver()
-                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
-            try {
-                if (cursor.moveToFirst()) {
-                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
-                    final byte[] rawStack = cursor.getBlob(
-                            cursor.getColumnIndex(ResumeColumns.STACK));
-                    DurableUtils.readFromArray(rawStack, mState.stack);
-                    mRestoredStack = true;
-                }
-            } catch (IOException e) {
-                Log.w(TAG, "Failed to resume: " + e);
-            } finally {
-                IoUtils.closeQuietly(cursor);
-            }
-
-            if (mRestoredStack) {
-                // Update the restored stack to ensure we have freshest data
-                final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
-                try {
-                    mState.stack.updateRoot(matchingRoots);
-                    mState.stack.updateDocuments(getContentResolver());
-                } catch (FileNotFoundException e) {
-                    Log.w(TAG, "Failed to restore stack: " + e);
-                    mState.stack.reset();
-                    mRestoredStack = false;
-                }
-            }
-
-            return null;
+        boolean showDrawer = false;
+        if (!restored) {
+            showDrawer = true;
+        }
+        if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
+            showDrawer = false;
+        }
+        if (external && mState.action == ACTION_GET_CONTENT) {
+            showDrawer = true;
         }
 
-        @Override
-        protected void onPostExecute(Void result) {
-            if (isDestroyed()) return;
-            mState.restored = true;
+        if (showDrawer) {
+            setRootsDrawerOpen(true);
+        }
+    }
 
-            // Show drawer when no stack restored, but only when requesting
-            // non-visual content. However, if we last used an external app,
-            // drawer is always shown.
+    public void onAppPicked(ResolveInfo info) {
+        final Intent intent = new Intent(getIntent());
+        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+        intent.setComponent(new ComponentName(
+                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
+        startActivityForResult(intent, CODE_FORWARD);
+    }
 
-            boolean showDrawer = false;
-            if (!mRestoredStack) {
-                showDrawer = true;
-            }
-            if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
-                showDrawer = false;
-            }
-            if (mExternal && mState.action == ACTION_GET_CONTENT) {
-                showDrawer = true;
-            }
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        Log.d(TAG, "onActivityResult() code=" + resultCode);
 
-            if (showDrawer) {
-                setRootsDrawerOpen(true);
-            }
+        // Only relay back results when not canceled; otherwise stick around to
+        // let the user pick another app/backend.
+        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
 
-            onCurrentDirectoryChanged(ANIM_NONE);
+            // Remember that we last picked via external app
+            final String packageName = getCallingPackageMaybeExtra();
+            final ContentValues values = new ContentValues();
+            values.put(ResumeColumns.EXTERNAL, 1);
+            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
+
+            // Pass back result to original caller
+            setResult(resultCode, data);
+            finish();
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
@@ -397,7 +359,6 @@
         updateActionBar();
     }
 
-    @Override
     public void setRootsDrawerOpen(boolean open) {
         if (!mShowAsDialog) {
             if (open) {
@@ -416,6 +377,7 @@
         }
     }
 
+    @Override
     public void updateActionBar() {
         if (mRootsToolbar != null) {
             if (mState.action == ACTION_OPEN ||
@@ -447,7 +409,7 @@
             });
         }
 
-        if (mSearchExpanded) {
+        if (mSearchManager.isExpanded()) {
             mToolbar.setTitle(null);
             mToolbarStack.setVisibility(View.GONE);
             mToolbarStack.setAdapter(null);
@@ -461,7 +423,7 @@
                 mToolbarStack.setVisibility(View.VISIBLE);
                 mToolbarStack.setAdapter(mStackAdapter);
 
-                mIgnoreNextNavigation = true;
+                mStackListener.mIgnoreNextNavigation = true;
                 mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
             }
         }
@@ -469,79 +431,18 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
+        boolean showMenu = super.onCreateOptionsMenu(menu);
+
         getMenuInflater().inflate(R.menu.activity, menu);
 
         // Most actions are visible when showing as dialog
         if (mShowAsDialog) {
-            for (int i = 0; i < menu.size(); i++) {
-                final MenuItem item = menu.getItem(i);
-                switch (item.getItemId()) {
-                    case R.id.menu_advanced:
-                    case R.id.menu_file_size:
-                        break;
-                    default:
-                        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-                }
-            }
+            expandMenus(menu);
         }
 
-        final MenuItem searchMenu = menu.findItem(R.id.menu_search);
-        mSearchView = (SearchView) searchMenu.getActionView();
-        mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
-            @Override
-            public boolean onQueryTextSubmit(String query) {
-                mSearchExpanded = true;
-                mState.currentSearch = query;
-                mSearchView.clearFocus();
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return true;
-            }
+        this.mSearchManager.install(menu.findItem(R.id.menu_search));
 
-            @Override
-            public boolean onQueryTextChange(String newText) {
-                return false;
-            }
-        });
-
-        searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
-            @Override
-            public boolean onMenuItemActionExpand(MenuItem item) {
-                mSearchExpanded = true;
-                updateActionBar();
-                return true;
-            }
-
-            @Override
-            public boolean onMenuItemActionCollapse(MenuItem item) {
-                mSearchExpanded = false;
-                if (mIgnoreNextCollapse) {
-                    mIgnoreNextCollapse = false;
-                    return true;
-                }
-
-                mState.currentSearch = null;
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return true;
-            }
-        });
-
-        mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
-            @Override
-            public boolean onClose() {
-                mSearchExpanded = false;
-                if (mIgnoreNextClose) {
-                    mIgnoreNextClose = false;
-                    return false;
-                }
-
-                mState.currentSearch = null;
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return false;
-            }
-        });
-
-        return true;
+        return showMenu;
     }
 
     @Override
@@ -554,7 +455,6 @@
         final DocumentInfo cwd = getCurrentDirectory();
 
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
-        final MenuItem search = menu.findItem(R.id.menu_search);
         final MenuItem sort = menu.findItem(R.id.menu_sort);
         final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
         final MenuItem grid = menu.findItem(R.id.menu_grid);
@@ -564,36 +464,23 @@
         final MenuItem settings = menu.findItem(R.id.menu_settings);
 
         sort.setVisible(cwd != null);
-        grid.setVisible(mState.derivedMode != MODE_GRID);
-        list.setVisible(mState.derivedMode != MODE_LIST);
+        grid.setVisible(mState.derivedMode != State.MODE_GRID);
+        list.setVisible(mState.derivedMode != State.MODE_LIST);
 
-        if (mState.currentSearch != null) {
-            // Search uses backend ranking; no sorting
-            sort.setVisible(false);
 
-            search.expandActionView();
+        mSearchManager.update(root);
 
-            mSearchView.setIconified(false);
-            mSearchView.clearFocus();
-            mSearchView.setQuery(mState.currentSearch, false);
-        } else {
-            mIgnoreNextClose = true;
-            mSearchView.setIconified(true);
-            mSearchView.clearFocus();
-
-            mIgnoreNextCollapse = true;
-            search.collapseActionView();
-        }
+        // Search uses backend ranking; no sorting
+        sort.setVisible(mSearchManager.isSearching());
 
         // Only sort by size when visible
         sortSize.setVisible(mState.showSize);
 
-        boolean searchVisible;
         boolean fileSizeVisible = !(mState.action == ACTION_MANAGE
                 || mState.action == ACTION_BROWSE);
         if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
             createDir.setVisible(cwd != null && cwd.isCreateSupported());
-            searchVisible = false;
+            mSearchManager.showMenu(false);
 
             // No display options in recent directories
             if (cwd == null) {
@@ -607,14 +494,8 @@
             }
         } else {
             createDir.setVisible(false);
-
-            searchVisible = root != null
-                    && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0);
         }
 
-        // TODO: close any search in-progress when hiding
-        search.setVisible(searchVisible);
-
         advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this)
                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
         fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
@@ -634,90 +515,7 @@
         if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
             return true;
         }
-
-        final int id = item.getItemId();
-        if (id == android.R.id.home) {
-            onBackPressed();
-            return true;
-        } else if (id == R.id.menu_create_dir) {
-            CreateDirectoryFragment.show(getFragmentManager());
-            return true;
-        } else if (id == R.id.menu_search) {
-            return false;
-        } else if (id == R.id.menu_sort_name) {
-            setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
-            return true;
-        } else if (id == R.id.menu_sort_date) {
-            setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
-            return true;
-        } else if (id == R.id.menu_sort_size) {
-            setUserSortOrder(State.SORT_ORDER_SIZE);
-            return true;
-        } else if (id == R.id.menu_grid) {
-            setUserMode(State.MODE_GRID);
-            return true;
-        } else if (id == R.id.menu_list) {
-            setUserMode(State.MODE_LIST);
-            return true;
-        } else if (id == R.id.menu_advanced) {
-            setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
-            return true;
-        } else if (id == R.id.menu_file_size) {
-            setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
-            return true;
-        } else if (id == R.id.menu_settings) {
-            final RootInfo root = getCurrentRoot();
-            final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
-            intent.setDataAndType(DocumentsContract.buildRootUri(root.authority, root.rootId),
-                    DocumentsContract.Root.MIME_TYPE_ITEM);
-            startActivity(intent);
-            return true;
-        } else {
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    private void setDisplayAdvancedDevices(boolean display) {
-        LocalPreferences.setDisplayAdvancedDevices(this, display);
-        mState.showAdvanced = mState.forceAdvanced | display;
-        RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
-        invalidateOptionsMenu();
-    }
-
-    private void setDisplayFileSize(boolean display) {
-        LocalPreferences.setDisplayFileSize(this, display);
-        mState.showSize = display;
-        DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
-        invalidateOptionsMenu();
-    }
-
-    @Override
-    public void onStateChanged() {
-        invalidateOptionsMenu();
-    }
-
-    /**
-     * Set state sort order based on explicit user action.
-     */
-    private void setUserSortOrder(int sortOrder) {
-        mState.userSortOrder = sortOrder;
-        DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
-    }
-
-    /**
-     * Set state mode based on explicit user action.
-     */
-    private void setUserMode(int mode) {
-        mState.userMode = mode;
-        DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
-    }
-
-    @Override
-    public void setPending(boolean pending) {
-        final SaveFragment save = SaveFragment.get(getFragmentManager());
-        if (save != null) {
-            save.setPending(pending);
-        }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -740,131 +538,12 @@
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle state) {
-        super.onSaveInstanceState(state);
-        state.putParcelable(EXTRA_STATE, mState);
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Bundle state) {
-        super.onRestoreInstanceState(state);
-    }
-
-    private BaseAdapter mStackAdapter = new BaseAdapter() {
-        @Override
-        public int getCount() {
-            return mState.stack.size();
-        }
-
-        @Override
-        public DocumentInfo getItem(int position) {
-            return mState.stack.get(mState.stack.size() - position - 1);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.item_subdir_title, parent, false);
-            }
-
-            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
-            final DocumentInfo doc = getItem(position);
-
-            if (position == 0) {
-                final RootInfo root = getCurrentRoot();
-                title.setText(root.title);
-            } else {
-                title.setText(doc.displayName);
-            }
-
-            return convertView;
-        }
-
-        @Override
-        public View getDropDownView(int position, View convertView, ViewGroup parent) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.item_subdir, parent, false);
-            }
-
-            final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
-            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
-            final DocumentInfo doc = getItem(position);
-
-            if (position == 0) {
-                final RootInfo root = getCurrentRoot();
-                title.setText(root.title);
-                subdir.setVisibility(View.GONE);
-            } else {
-                title.setText(doc.displayName);
-                subdir.setVisibility(View.VISIBLE);
-            }
-
-            return convertView;
-        }
-    };
-
-    private OnItemSelectedListener mStackListener = new OnItemSelectedListener() {
-        @Override
-        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-            if (mIgnoreNextNavigation) {
-                mIgnoreNextNavigation = false;
-                return;
-            }
-
-            while (mState.stack.size() > position + 1) {
-                mState.stackTouched = true;
-                mState.stack.pop();
-            }
-            onCurrentDirectoryChanged(ANIM_UP);
-        }
-
-        @Override
-        public void onNothingSelected(AdapterView<?> parent) {
-            // Ignored
-        }
-    };
-
-    @Override
-    public RootInfo getCurrentRoot() {
-        if (mState.stack.root != null) {
-            return mState.stack.root;
-        } else {
-            return mRoots.getRecentsRoot();
-        }
-    }
-
-    @Override
-    public DocumentInfo getCurrentDirectory() {
-        return mState.stack.peek();
-    }
-
-    private String getCallingPackageMaybeExtra() {
-        final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
-        return (extra != null) ? extra : getCallingPackage();
-    }
-
-    public Executor getCurrentExecutor() {
-        final DocumentInfo cwd = getCurrentDirectory();
-        if (cwd != null && cwd.authority != null) {
-            return ProviderExecutor.forAuthority(cwd.authority);
-        } else {
-            return AsyncTask.THREAD_POOL_EXECUTOR;
-        }
-    }
-
-    @Override
     public State getDisplayState() {
         return mState;
     }
 
-    private void onCurrentDirectoryChanged(int anim) {
+    @Override
+    void onDirectoryChanged(int anim) {
         final FragmentManager fm = getFragmentManager();
         final RootInfo root = getCurrentRoot();
         final DocumentInfo cwd = getCurrentDirectory();
@@ -883,7 +562,7 @@
                 // Start recents in grid when requesting visual things
                 final boolean visualMimes = MimePredicate.mimeMatches(
                         MimePredicate.VISUAL_MIMES, mState.acceptMimes);
-                mState.userMode = visualMimes ? MODE_GRID : MODE_LIST;
+                mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
                 mState.derivedMode = mState.userMode;
             }
         } else {
@@ -913,108 +592,20 @@
                 pick.setPickTarget(mState.action, cwd, displayName);
             }
         }
+    }
 
-        final RootsFragment roots = RootsFragment.get(fm);
-        if (roots != null) {
-            roots.onCurrentRootChanged();
-        }
+    void onSaveRequested(DocumentInfo replaceTarget) {
+        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
+    }
 
-        updateActionBar();
-        invalidateOptionsMenu();
-        dumpStack();
+    void onSaveRequested(String mimeType, String displayName) {
+        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
     }
 
     @Override
-    public void onStackPicked(DocumentStack stack) {
-        try {
-            // Update the restored stack to ensure we have freshest data
-            stack.updateDocuments(getContentResolver());
-
-            mState.stack = stack;
-            mState.stackTouched = true;
-            onCurrentDirectoryChanged(ANIM_SIDE);
-
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "Failed to restore stack: " + e);
-        }
-    }
-
-    @Override
-    public void onRootPicked(RootInfo root, boolean closeDrawer) {
-        // Clear entire backstack and start in new root
-        mState.stack.root = root;
-        mState.stack.clear();
-        mState.stackTouched = true;
-
-        if (!mRoots.isRecentsRoot(root)) {
-            new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
-        } else {
-            onCurrentDirectoryChanged(ANIM_SIDE);
-        }
-
-        if (closeDrawer) {
-            setRootsDrawerOpen(false);
-        }
-    }
-
-    private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
-        private RootInfo mRoot;
-
-        public PickRootTask(RootInfo root) {
-            mRoot = root;
-        }
-
-        @Override
-        protected DocumentInfo doInBackground(Void... params) {
-            try {
-                final Uri uri = DocumentsContract.buildDocumentUri(
-                        mRoot.authority, mRoot.documentId);
-                return DocumentInfo.fromUri(getContentResolver(), uri);
-            } catch (FileNotFoundException e) {
-                Log.w(TAG, "Failed to find root", e);
-                return null;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(DocumentInfo result) {
-            if (result != null) {
-                mState.stack.push(result);
-                mState.stackTouched = true;
-                onCurrentDirectoryChanged(ANIM_SIDE);
-            }
-        }
-    }
-
-    @Override
-    public void onAppPicked(ResolveInfo info) {
-        final Intent intent = new Intent(getIntent());
-        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-        intent.setComponent(new ComponentName(
-                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
-        startActivityForResult(intent, CODE_FORWARD);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        Log.d(TAG, "onActivityResult() code=" + resultCode);
-
-        // Only relay back results when not canceled; otherwise stick around to
-        // let the user pick another app/backend.
-        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
-
-            // Remember that we last picked via external app
-            final String packageName = getCallingPackageMaybeExtra();
-            final ContentValues values = new ContentValues();
-            values.put(ResumeColumns.EXTERNAL, 1);
-            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
-
-            // Pass back result to original caller
-            setResult(resultCode, data);
-            finish();
-        } else {
-            super.onActivityResult(requestCode, resultCode, data);
-        }
+    void onRootPicked(RootInfo root) {
+        super.onRootPicked(root);
+        setRootsDrawerOpen(false);
     }
 
     @Override
@@ -1076,17 +667,6 @@
         }
     }
 
-    @Override
-    public void onSaveRequested(DocumentInfo replaceTarget) {
-        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
-    }
-
-    @Override
-    public void onSaveRequested(String mimeType, String displayName) {
-        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
-    }
-
-    @Override
     public void onPickRequested(DocumentInfo pickTarget) {
         Uri result;
         if (mState.action == ACTION_OPEN_TREE) {
@@ -1101,7 +681,8 @@
         new PickFinishTask(result).executeOnExecutor(getCurrentExecutor());
     }
 
-    private void saveStackBlocking() {
+    @Override
+    void saveStackBlocking() {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
@@ -1124,7 +705,8 @@
         resolver.insert(RecentsProvider.buildResume(packageName), values);
     }
 
-    private void onFinished(Uri... uris) {
+    @Override
+    void onTaskFinished(Uri... uris) {
         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
         final Intent intent = new Intent();
@@ -1159,7 +741,52 @@
         finish();
     }
 
-    private class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
+    public static DocumentsActivity get(Fragment fragment) {
+        return (DocumentsActivity) fragment.getActivity();
+    }
+
+    private final class PickFinishTask extends AsyncTask<Void, Void, Void> {
+        private final Uri mUri;
+
+        public PickFinishTask(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            saveStackBlocking();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            onTaskFinished(mUri);
+        }
+    }
+
+    final class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
+        private final Uri[] mUris;
+
+        public ExistingFinishTask(Uri... uris) {
+            mUris = uris;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            saveStackBlocking();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            onTaskFinished(mUris);
+        }
+    }
+
+    /**
+     * Task that creates a new document in the background.
+     */
+    final class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
         private final String mMimeType;
         private final String mDisplayName;
 
@@ -1201,7 +828,7 @@
         @Override
         protected void onPostExecute(Uri result) {
             if (result != null) {
-                onFinished(result);
+                onTaskFinished(result);
             } else {
                 Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
                         .show();
@@ -1210,50 +837,4 @@
             setPending(false);
         }
     }
-
-    private class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
-        private final Uri[] mUris;
-
-        public ExistingFinishTask(Uri... uris) {
-            mUris = uris;
-        }
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            onFinished(mUris);
-        }
-    }
-
-    private class PickFinishTask extends AsyncTask<Void, Void, Void> {
-        private final Uri mUri;
-
-        public PickFinishTask(Uri uri) {
-            mUri = uri;
-        }
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            onFinished(mUri);
-        }
-    }
-
-    private void dumpStack() {
-        Log.d(TAG, "Current stack: ");
-        Log.d(TAG, " * " + mState.stack.root);
-        for (DocumentInfo doc : mState.stack) {
-            Log.d(TAG, " +-- " + doc);
-        }
-    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
index 7ea51b9..e899379 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -75,7 +75,7 @@
     private View.OnClickListener mPickListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
-            final BaseActivity activity = BaseActivity.get(PickFragment.this);
+            final DocumentsActivity activity = DocumentsActivity.get(PickFragment.this);
             activity.onPickRequested(mPickTarget);
         }
     };
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index 26aecc5..e11d7d9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -109,8 +109,9 @@
                 mAdapter.swapStacks(data);
 
                 // When launched into empty recents, show drawer
-                if (mAdapter.isEmpty() && !state.stackTouched) {
-                    ((BaseActivity) context).setRootsDrawerOpen(true);
+                if (mAdapter.isEmpty() && !state.stackTouched &&
+                        context instanceof DocumentsActivity) {
+                    ((DocumentsActivity) context).setRootsDrawerOpen(true);
                 }
             }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index ed5e123..fd67a77 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -174,11 +174,12 @@
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final BaseActivity activity = BaseActivity.get(RootsFragment.this);
-            final Item item = mAdapter.getItem(position);
+            Item item = mAdapter.getItem(position);
             if (item instanceof RootItem) {
-                activity.onRootPicked(((RootItem) item).root, true);
+                BaseActivity activity = BaseActivity.get(RootsFragment.this);
+                activity.onRootPicked(((RootItem) item).root);
             } else if (item instanceof AppItem) {
+                DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
                 activity.onAppPicked(((AppItem) item).info);
             } else {
                 throw new IllegalStateException("Unknown root: " + item);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index a13fccc..ce98db2 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -113,7 +113,7 @@
     private View.OnClickListener mSaveListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
-            final BaseActivity activity = BaseActivity.get(SaveFragment.this);
+            final DocumentsActivity activity = DocumentsActivity.get(SaveFragment.this);
             if (mReplaceTarget != null) {
                 activity.onSaveRequested(mReplaceTarget);
             } else {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
index 976f21d..f89b182 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java
@@ -16,50 +16,29 @@
 
 package com.android.documentsui;
 
-
 import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
 import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
-import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
 import static com.android.documentsui.DirectoryFragment.ANIM_UP;
 import android.app.Activity;
-import android.app.Fragment;
 import android.app.FragmentManager;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
-import android.content.ComponentName;
-import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.database.Cursor;
 import android.graphics.Point;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.Debug;
 import android.provider.DocumentsContract;
-import android.support.v4.app.ActionBarDrawerToggle;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v4.widget.DrawerLayout.DrawerListener;
+import android.provider.DocumentsContract.Root;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.MenuItem.OnActionExpandListener;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
 import android.widget.BaseAdapter;
-import android.widget.ImageView;
-import android.widget.SearchView;
-import android.widget.SearchView.OnQueryTextListener;
 import android.widget.Spinner;
-import android.widget.TextView;
 import android.widget.Toast;
 import android.widget.Toolbar;
 
@@ -70,55 +49,37 @@
 import com.android.documentsui.model.DurableUtils;
 import com.android.documentsui.model.RootInfo;
 
-import libcore.io.IoUtils;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.Executor;
 
+/**
+ * Activity providing a directly launchable file management activity.
+ */
 public class StandaloneActivity extends BaseActivity {
     public static final String TAG = "StandaloneFileManagement";
 
-    private static final String EXTRA_STATE = "state";
-
-    private static final int CODE_FORWARD = 42;
-
-    private SearchView mSearchView;
-
     private Toolbar mToolbar;
     private Spinner mToolbarStack;
-
     private Toolbar mRootsToolbar;
-
-    private ActionBarDrawerToggle mDrawerToggle;
-
     private DirectoryContainerView mDirectoryContainer;
-
-    private boolean mIgnoreNextNavigation;
-    private boolean mIgnoreNextClose;
-    private boolean mIgnoreNextCollapse;
-
-    private boolean mSearchExpanded;
-
-    private RootsCache mRoots;
+    private SearchManager mSearchManager;
     private State mState;
+    private ItemSelectedListener mStackListener;
+    private BaseAdapter mStackAdapter;
+
+    public StandaloneActivity() {
+        super(TAG);
+    }
 
     @Override
     public void onCreate(Bundle icicle) {
-        // Debug.waitForDebugger();
         super.onCreate(icicle);
 
-        mRoots = DocumentsApplication.getRootsCache(this);
-
         setResult(Activity.RESULT_CANCELED);
         setContentView(R.layout.activity);
 
         final Context context = this;
-        final Resources res = getResources();
 
         // Strongly define our horizontal dimension; we leave vertical as
         final WindowManager.LayoutParams a = getWindow().getAttributes();
@@ -131,16 +92,17 @@
 
         mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
 
-        if (icicle != null) {
-            mState = icicle.getParcelable(EXTRA_STATE);
-        } else {
-            buildDefaultState();
-        }
+        mState = (icicle != null)
+            ? icicle.<State>getParcelable(EXTRA_STATE)
+            : buildDefaultState();
 
+        mSearchManager = new SearchManager();
         mToolbar = (Toolbar) findViewById(R.id.toolbar);
         mToolbar.setTitleTextAppearance(context,
                 android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
 
+        mStackAdapter = new StackAdapter();
+        mStackListener = new ItemSelectedListener();
         mToolbarStack = (Spinner) findViewById(R.id.stack);
         mToolbarStack.setOnItemSelectedListener(mStackListener);
 
@@ -167,87 +129,33 @@
         }
     }
 
-    private void buildDefaultState() {
-        mState = new State();
+    private State buildDefaultState() {
+        State state = new State();
 
         final Intent intent = getIntent();
-        mState.action = State.ACTION_BROWSE_ALL;
-        mState.acceptMimes = new String[] { "*/*" };
-        mState.allowMultiple = true;
-        mState.acceptMimes = new String[] { intent.getType() };
-        mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
-        mState.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
-        mState.showAdvanced = mState.forceAdvanced
+        state.action = State.ACTION_BROWSE_ALL;
+        state.acceptMimes = new String[] { "*/*" };
+        state.allowMultiple = true;
+        state.acceptMimes = new String[] { intent.getType() };
+        state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
+        state.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
+        state.showAdvanced = state.forceAdvanced
                 | LocalPreferences.getDisplayAdvancedDevices(this);
-        mState.showSize = true;
+        state.showSize = true;
         final DocumentStack stack = intent.getParcelableExtra(CopyService.EXTRA_STACK);
         if (stack != null)
-            mState.stack = stack;
-    }
+            state.stack = stack;
 
-    private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
-        private volatile boolean mRestoredStack;
-        private volatile boolean mExternal;
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            // Restore last stack for calling package
-            final String packageName = getCallingPackageMaybeExtra();
-            final Cursor cursor = getContentResolver()
-                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
-            try {
-                if (cursor.moveToFirst()) {
-                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
-                    final byte[] rawStack = cursor.getBlob(
-                            cursor.getColumnIndex(ResumeColumns.STACK));
-                    DurableUtils.readFromArray(rawStack, mState.stack);
-                    mRestoredStack = true;
-                }
-            } catch (IOException e) {
-                Log.w(TAG, "Failed to resume: " + e);
-            } finally {
-                IoUtils.closeQuietly(cursor);
-            }
-
-            if (mRestoredStack) {
-                // Update the restored stack to ensure we have freshest data
-                final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
-                try {
-                    mState.stack.updateRoot(matchingRoots);
-                    mState.stack.updateDocuments(getContentResolver());
-                } catch (FileNotFoundException e) {
-                    Log.w(TAG, "Failed to restore stack: " + e);
-                    mState.stack.reset();
-                    mRestoredStack = false;
-                }
-            }
-
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            if (isDestroyed()) return;
-            mState.restored = true;
-            onCurrentDirectoryChanged(ANIM_NONE);
-        }
+        return state;
     }
 
     @Override
     protected void onPostCreate(Bundle savedInstanceState) {
         super.onPostCreate(savedInstanceState);
-        if (mDrawerToggle != null) {
-            mDrawerToggle.syncState();
-        }
         updateActionBar();
     }
 
     @Override
-    public void setRootsDrawerOpen(boolean open) {
-        Log.w(TAG, "Trying to change state of roots drawer to > " + (open ? "open" : "closed"));
-      // throw new UnsupportedOperationException();
-    }
-
     public void updateActionBar() {
         final RootInfo root = getCurrentRoot();
         mToolbar.setNavigationIcon(
@@ -255,7 +163,7 @@
         mToolbar.setNavigationContentDescription(R.string.drawer_open);
         mToolbar.setNavigationOnClickListener(null);
 
-        if (mSearchExpanded) {
+        if (mSearchManager.isExpanded()) {
             mToolbar.setTitle(null);
             mToolbarStack.setVisibility(View.GONE);
             mToolbarStack.setAdapter(null);
@@ -269,7 +177,7 @@
                 mToolbarStack.setVisibility(View.VISIBLE);
                 mToolbarStack.setAdapter(mStackAdapter);
 
-                mIgnoreNextNavigation = true;
+                mStackListener.mIgnoreNextNavigation = true;
                 mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
             }
         }
@@ -277,220 +185,60 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
+        boolean showMenu = super.onCreateOptionsMenu(menu);
         getMenuInflater().inflate(R.menu.activity, menu);
 
-        for (int i = 0; i < menu.size(); i++) {
-            final MenuItem item = menu.getItem(i);
-            switch (item.getItemId()) {
-                case R.id.menu_advanced:
-                case R.id.menu_file_size:
-                    break;
-                default:
-                    item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-            }
-        }
+        expandMenus(menu);
 
-        final MenuItem searchMenu = menu.findItem(R.id.menu_search);
-        mSearchView = (SearchView) searchMenu.getActionView();
-        mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
-            @Override
-            public boolean onQueryTextSubmit(String query) {
-                mSearchExpanded = true;
-                mState.currentSearch = query;
-                mSearchView.clearFocus();
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return true;
-            }
+        this.mSearchManager.install(menu.findItem(R.id.menu_search));
 
-            @Override
-            public boolean onQueryTextChange(String newText) {
-                return false;
-            }
-        });
-
-        searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
-            @Override
-            public boolean onMenuItemActionExpand(MenuItem item) {
-                mSearchExpanded = true;
-                updateActionBar();
-                return true;
-            }
-
-            @Override
-            public boolean onMenuItemActionCollapse(MenuItem item) {
-                mSearchExpanded = false;
-                if (mIgnoreNextCollapse) {
-                    mIgnoreNextCollapse = false;
-                    return true;
-                }
-
-                mState.currentSearch = null;
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return true;
-            }
-        });
-
-        mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
-            @Override
-            public boolean onClose() {
-                mSearchExpanded = false;
-                if (mIgnoreNextClose) {
-                    mIgnoreNextClose = false;
-                    return false;
-                }
-
-                mState.currentSearch = null;
-                onCurrentDirectoryChanged(ANIM_NONE);
-                return false;
-            }
-        });
-
-        return true;
+        return showMenu;
     }
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
 
-        final FragmentManager fm = getFragmentManager();
-
         final RootInfo root = getCurrentRoot();
         final DocumentInfo cwd = getCurrentDirectory();
 
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
-        final MenuItem search = menu.findItem(R.id.menu_search);
         final MenuItem sort = menu.findItem(R.id.menu_sort);
         final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
         final MenuItem grid = menu.findItem(R.id.menu_grid);
         final MenuItem list = menu.findItem(R.id.menu_list);
         final MenuItem advanced = menu.findItem(R.id.menu_advanced);
         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
+        final MenuItem settings = menu.findItem(R.id.menu_settings);
 
-        sort.setVisible(cwd != null);
         grid.setVisible(mState.derivedMode != State.MODE_GRID);
         list.setVisible(mState.derivedMode != State.MODE_LIST);
 
-        if (mState.currentSearch != null) {
-            // Search uses backend ranking; no sorting
-            sort.setVisible(false);
-
-            search.expandActionView();
-
-            mSearchView.setIconified(false);
-            mSearchView.clearFocus();
-            mSearchView.setQuery(mState.currentSearch, false);
-        } else {
-            mIgnoreNextClose = true;
-            mSearchView.setIconified(true);
-            mSearchView.clearFocus();
-
-            mIgnoreNextCollapse = true;
-            search.collapseActionView();
-        }
+        mSearchManager.update(root);
+        sort.setVisible(cwd != null && !mSearchManager.isSearching());
 
         // Only sort by size when visible
         sortSize.setVisible(mState.showSize);
 
-        fileSize.setVisible(true);
-        search.setVisible(true);
-        createDir.setVisible(true);
-        advanced.setVisible(true);
+        createDir.setVisible(cwd != null
+                && cwd.isCreateSupported()
+                && !mSearchManager.isSearching()
+                && !root.isDownloads());
+
+        fileSize.setVisible(cwd != null);
+        advanced.setVisible(cwd != null);
 
         advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this)
                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
         fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
                 ? R.string.menu_file_size_hide : R.string.menu_file_size_show);
 
+        settings.setVisible((root.flags & Root.FLAG_HAS_SETTINGS) != 0);
 
         return true;
     }
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
-            return true;
-        }
-
-        final int id = item.getItemId();
-        if (id == android.R.id.home) {
-            onBackPressed();
-            return true;
-        } else if (id == R.id.menu_create_dir) {
-            CreateDirectoryFragment.show(getFragmentManager());
-            return true;
-        } else if (id == R.id.menu_search) {
-            return false;
-        } else if (id == R.id.menu_sort_name) {
-            setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
-            return true;
-        } else if (id == R.id.menu_sort_date) {
-            setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
-            return true;
-        } else if (id == R.id.menu_sort_size) {
-            setUserSortOrder(State.SORT_ORDER_SIZE);
-            return true;
-        } else if (id == R.id.menu_grid) {
-            setUserMode(State.MODE_GRID);
-            return true;
-        } else if (id == R.id.menu_list) {
-            setUserMode(State.MODE_LIST);
-            return true;
-        } else if (id == R.id.menu_advanced) {
-            setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
-            return true;
-        } else if (id == R.id.menu_file_size) {
-            setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
-            return true;
-        } else {
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    private void setDisplayAdvancedDevices(boolean display) {
-        LocalPreferences.setDisplayAdvancedDevices(this, display);
-        mState.showAdvanced = mState.forceAdvanced | display;
-        RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
-        invalidateOptionsMenu();
-    }
-
-    private void setDisplayFileSize(boolean display) {
-        LocalPreferences.setDisplayFileSize(this, display);
-        mState.showSize = display;
-        DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
-        invalidateOptionsMenu();
-    }
-
-    @Override
-    public void onStateChanged() {
-        invalidateOptionsMenu();
-    }
-
-    /**
-     * Set state sort order based on explicit user action.
-     */
-    private void setUserSortOrder(int sortOrder) {
-        mState.userSortOrder = sortOrder;
-        DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
-    }
-
-    /**
-     * Set state mode based on explicit user action.
-     */
-    private void setUserMode(int mode) {
-        mState.userMode = mode;
-        DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
-    }
-
-    @Override
-    public void setPending(boolean pending) {
-        final SaveFragment save = SaveFragment.get(getFragmentManager());
-        if (save != null) {
-            save.setPending(pending);
-        }
-    }
-
-    @Override
     public void onBackPressed() {
         if (!mState.stackTouched) {
             super.onBackPressed();
@@ -507,130 +255,12 @@
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle state) {
-        super.onSaveInstanceState(state);
-        state.putParcelable(EXTRA_STATE, mState);
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Bundle state) {
-        super.onRestoreInstanceState(state);
-    }
-
-    private BaseAdapter mStackAdapter = new BaseAdapter() {
-        @Override
-        public int getCount() {
-            return mState.stack.size();
-        }
-
-        @Override
-        public DocumentInfo getItem(int position) {
-            return mState.stack.get(mState.stack.size() - position - 1);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.item_subdir_title, parent, false);
-            }
-
-            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
-            final DocumentInfo doc = getItem(position);
-
-            if (position == 0) {
-                final RootInfo root = getCurrentRoot();
-                title.setText(root.title);
-            } else {
-                title.setText(doc.displayName);
-            }
-
-            return convertView;
-        }
-
-        @Override
-        public View getDropDownView(int position, View convertView, ViewGroup parent) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.item_subdir, parent, false);
-            }
-
-            final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
-            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
-            final DocumentInfo doc = getItem(position);
-
-            if (position == 0) {
-                final RootInfo root = getCurrentRoot();
-                title.setText(root.title);
-                subdir.setVisibility(View.GONE);
-            } else {
-                title.setText(doc.displayName);
-                subdir.setVisibility(View.VISIBLE);
-            }
-
-            return convertView;
-        }
-    };
-
-    private OnItemSelectedListener mStackListener = new OnItemSelectedListener() {
-        @Override
-        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-            if (mIgnoreNextNavigation) {
-                mIgnoreNextNavigation = false;
-                return;
-            }
-
-            while (mState.stack.size() > position + 1) {
-                mState.stackTouched = true;
-                mState.stack.pop();
-            }
-            onCurrentDirectoryChanged(ANIM_UP);
-        }
-
-        @Override
-        public void onNothingSelected(AdapterView<?> parent) {
-            // Ignored
-        }
-    };
-
-    @Override
-    public RootInfo getCurrentRoot() {
-        if (mState.stack.root != null) {
-            return mState.stack.root;
-        } else {
-            return mRoots.getRecentsRoot();
-        }
-    }
-
-    public DocumentInfo getCurrentDirectory() {
-        return mState.stack.peek();
-    }
-
-    private String getCallingPackageMaybeExtra() {
-        final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
-        return (extra != null) ? extra : getCallingPackage();
-    }
-
-    public Executor getCurrentExecutor() {
-        final DocumentInfo cwd = getCurrentDirectory();
-        if (cwd != null && cwd.authority != null) {
-            return ProviderExecutor.forAuthority(cwd.authority);
-        } else {
-            return AsyncTask.THREAD_POOL_EXECUTOR;
-        }
-    }
-
-    @Override
     public State getDisplayState() {
         return mState;
     }
 
-    private void onCurrentDirectoryChanged(int anim) {
+    @Override
+    void onDirectoryChanged(int anim) {
         final FragmentManager fm = getFragmentManager();
         final RootInfo root = getCurrentRoot();
         final DocumentInfo cwd = getCurrentDirectory();
@@ -654,109 +284,10 @@
                 DirectoryFragment.showNormal(fm, root, cwd, anim);
             }
         }
-
-        final RootsFragment roots = RootsFragment.get(fm);
-        if (roots != null) {
-            roots.onCurrentRootChanged();
-        }
-
-        updateActionBar();
-        invalidateOptionsMenu();
-        dumpStack();
-    }
-
-    @Override
-    public void onStackPicked(DocumentStack stack) {
-        try {
-            // Update the restored stack to ensure we have freshest data
-            stack.updateDocuments(getContentResolver());
-
-            mState.stack = stack;
-            mState.stackTouched = true;
-            onCurrentDirectoryChanged(ANIM_SIDE);
-
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "Failed to restore stack: " + e);
-        }
-    }
-
-    @Override
-    public void onRootPicked(RootInfo root, boolean closeDrawer) {
-        // Clear entire backstack and start in new root
-        mState.stack.root = root;
-        mState.stack.clear();
-        mState.stackTouched = true;
-
-        if (!mRoots.isRecentsRoot(root)) {
-            new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
-        } else {
-            onCurrentDirectoryChanged(ANIM_SIDE);
-        }
-    }
-
-    private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
-        private RootInfo mRoot;
-
-        public PickRootTask(RootInfo root) {
-            mRoot = root;
-        }
-
-        @Override
-        protected DocumentInfo doInBackground(Void... params) {
-            try {
-                final Uri uri = DocumentsContract.buildDocumentUri(
-                        mRoot.authority, mRoot.documentId);
-                return DocumentInfo.fromUri(getContentResolver(), uri);
-            } catch (FileNotFoundException e) {
-                Log.w(TAG, "Failed to find root", e);
-                return null;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(DocumentInfo result) {
-            if (result != null) {
-                mState.stack.push(result);
-                mState.stackTouched = true;
-                onCurrentDirectoryChanged(ANIM_SIDE);
-            }
-        }
-    }
-
-    @Override
-    public void onAppPicked(ResolveInfo info) {
-        final Intent intent = new Intent(getIntent());
-        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-        intent.setComponent(new ComponentName(
-                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
-        startActivityForResult(intent, CODE_FORWARD);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        Log.d(TAG, "onActivityResult() code=" + resultCode);
-
-        // Only relay back results when not canceled; otherwise stick around to
-        // let the user pick another app/backend.
-        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
-
-            // Remember that we last picked via external app
-            final String packageName = getCallingPackageMaybeExtra();
-            final ContentValues values = new ContentValues();
-            values.put(ResumeColumns.EXTERNAL, 1);
-            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
-
-            // Pass back result to original caller
-            setResult(resultCode, data);
-            finish();
-        } else {
-            super.onActivityResult(requestCode, resultCode, data);
-        }
     }
 
     @Override
     public void onDocumentPicked(DocumentInfo doc) {
-        final FragmentManager fm = getFragmentManager();
         if (doc.isDirectory()) {
             mState.stack.push(doc);
             mState.stackTouched = true;
@@ -780,27 +311,12 @@
     }
 
     @Override
-    public void onSaveRequested(DocumentInfo replaceTarget) {
-        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
-    }
-
-    @Override
-    public void onSaveRequested(String mimeType, String displayName) {
-        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
-    }
-
-    @Override
-    public void onPickRequested(DocumentInfo pickTarget) {
-        final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority,
-                pickTarget.documentId);
-        new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
-    }
-
-    private void saveStackBlocking() {
+    void saveStackBlocking() {
         final ContentResolver resolver = getContentResolver();
         final ContentValues values = new ContentValues();
 
-        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
+        final byte[] rawStack = DurableUtils.writeToArrayOrNull(
+                getDisplayState().stack);
 
         // Remember location for next app launch
         final String packageName = getCallingPackageMaybeExtra();
@@ -810,7 +326,8 @@
         resolver.insert(RecentsProvider.buildResume(packageName), values);
     }
 
-    private void onFinished(Uri... uris) {
+    @Override
+    void onTaskFinished(Uri... uris) {
         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
 
         final Intent intent = new Intent();
@@ -832,106 +349,4 @@
         setResult(Activity.RESULT_OK, intent);
         finish();
     }
-
-    private class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
-        private final String mMimeType;
-        private final String mDisplayName;
-
-        public CreateFinishTask(String mimeType, String displayName) {
-            mMimeType = mimeType;
-            mDisplayName = displayName;
-        }
-
-        @Override
-        protected void onPreExecute() {
-            setPending(true);
-        }
-
-        @Override
-        protected Uri doInBackground(Void... params) {
-            final ContentResolver resolver = getContentResolver();
-            final DocumentInfo cwd = getCurrentDirectory();
-
-            ContentProviderClient client = null;
-            Uri childUri = null;
-            try {
-                client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                        resolver, cwd.derivedUri.getAuthority());
-                childUri = DocumentsContract.createDocument(
-                        client, cwd.derivedUri, mMimeType, mDisplayName);
-            } catch (Exception e) {
-                Log.w(TAG, "Failed to create document", e);
-            } finally {
-                ContentProviderClient.releaseQuietly(client);
-            }
-
-            if (childUri != null) {
-                saveStackBlocking();
-            }
-
-            return childUri;
-        }
-
-        @Override
-        protected void onPostExecute(Uri result) {
-            if (result != null) {
-                onFinished(result);
-            } else {
-                Toast.makeText(StandaloneActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
-                        .show();
-            }
-
-            setPending(false);
-        }
-    }
-
-    private class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
-        private final Uri[] mUris;
-
-        public ExistingFinishTask(Uri... uris) {
-            mUris = uris;
-        }
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            onFinished(mUris);
-        }
-    }
-
-    private class PickFinishTask extends AsyncTask<Void, Void, Void> {
-        private final Uri mUri;
-
-        public PickFinishTask(Uri uri) {
-            mUri = uri;
-        }
-
-        @Override
-        protected Void doInBackground(Void... params) {
-            saveStackBlocking();
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            onFinished(mUri);
-        }
-    }
-
-    private void dumpStack() {
-        Log.d(TAG, "Current stack: ");
-        Log.d(TAG, " * " + mState.stack.root);
-        for (DocumentInfo doc : mState.stack) {
-            Log.d(TAG, " +-- " + doc);
-        }
-    }
-
-    public static BaseActivity get(Fragment fragment) {
-        return (BaseActivity) fragment.getActivity();
-    }
 }