Merge "Allow Drag and Drop to form new selection with CTRL held down." into nyc-andromeda-dev
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index bfed16c..7a3a9d2 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -132,14 +132,14 @@
       <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> item</item>
     </plurals>
     <string name="delete_filename_confirmation_message" msgid="8338069763240613258">"\'<xliff:g id="NAME">%1$s</xliff:g>\' verwijderen?"</string>
-    <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"Map \'<xliff:g id="NAME">%1$s</xliff:g>\' en de bijbehorende inhoud verwijderen?"</string>
+    <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"Map \'<xliff:g id="NAME">%1$s</xliff:g>\' en de bijbehorende content verwijderen?"</string>
     <plurals name="delete_files_confirmation_message" formatted="false" msgid="4866664063250034142">
       <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> bestanden verwijderen?</item>
       <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> bestand verwijderen?</item>
     </plurals>
     <plurals name="delete_folders_confirmation_message" formatted="false" msgid="1028946402799686388">
-      <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> mappen en de bijbehorende inhoud verwijderen?</item>
-      <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> map en de bijbehorende inhoud verwijderen?</item>
+      <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> mappen en de bijbehorende content verwijderen?</item>
+      <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> map en de bijbehorende content verwijderen?</item>
     </plurals>
     <plurals name="delete_items_confirmation_message" formatted="false" msgid="7285090426511028179">
       <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> items verwijderen?</item>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 466e90b..2c34546 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -46,7 +46,7 @@
     <string name="button_copy" msgid="8219059853840996027">"Копирај"</string>
     <string name="button_move" msgid="8596460499325291272">"Премести"</string>
     <string name="button_dismiss" msgid="7235249361023803349">"Одбаци"</string>
-    <string name="button_retry" msgid="4011461781916631389">"Покушај поново"</string>
+    <string name="button_retry" msgid="4011461781916631389">"Пробај поново"</string>
     <string name="not_sorted" msgid="7813496644889115530">"Нису сортирани"</string>
     <string name="sort_dimension_name" msgid="6325591541414177579">"Назив"</string>
     <string name="sort_dimension_summary" msgid="7724534446881397860">"Резиме"</string>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 5088149..0809a37 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -22,6 +22,7 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
 import com.android.documentsui.base.BooleanConsumer;
@@ -31,6 +32,7 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.AnimationView.AnimationType;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.files.LauncherActivity;
@@ -55,6 +57,7 @@
     protected final RootsAccess mRoots;
     protected final DocumentsAccess mDocs;
     protected final SelectionManager mSelectionMgr;
+    protected final SearchViewManager mSearchMgr;
     protected final Lookup<String, Executor> mExecutors;
 
     public AbstractActionHandler(
@@ -63,6 +66,7 @@
             RootsAccess roots,
             DocumentsAccess docs,
             SelectionManager selectionMgr,
+            SearchViewManager searchMgr,
             Lookup<String, Executor> executors) {
 
         assert(activity != null);
@@ -76,6 +80,7 @@
         mRoots = roots;
         mDocs = docs;
         mSelectionMgr = selectionMgr;
+        mSearchMgr = searchMgr;
         mExecutors = executors;
     }
 
@@ -151,11 +156,31 @@
     }
 
     @Override
+    public void openContainerDocument(DocumentInfo doc) {
+        assert(doc.isContainer());
+
+        mActivity.notifyDirectoryNavigated(doc.derivedUri);
+
+        mState.pushDocument(doc);
+        // Show an opening animation only if pressing "back" would get us back to the
+        // previous directory. Especially after opening a root document, pressing
+        // back, wouldn't go to the previous root, but close the activity.
+        final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
+                ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
+        mActivity.refreshCurrentRootAndDirectory(anim);
+    }
+
+    @Override
     public void deleteSelectedDocuments() {
         throw new UnsupportedOperationException("Delete not supported!");
     }
 
     @Override
+    public void shareSelectedDocuments() {
+        throw new UnsupportedOperationException("Share not supported!");
+    }
+
+    @Override
     public final void loadDocument(Uri uri) {
         new OpenUriForViewTask<>(mActivity, mState, mRoots, mDocs, uri)
                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
@@ -174,6 +199,7 @@
     protected Selection getStableSelection() {
         return mSelectionMgr.getSelection(new Selection());
     }
+
     /**
      * A class primarily for the support of isolating our tests
      * from our concrete activity implementations.
@@ -184,9 +210,11 @@
         // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
         void onDocumentsPicked(List<DocumentInfo> docs);
         void onDocumentPicked(DocumentInfo doc);
-        void openContainerDocument(DocumentInfo doc);
         RootInfo getCurrentRoot();
         DocumentInfo getCurrentDirectory();
         void setRootsDrawerOpen(boolean open);
+
+        @VisibleForTesting
+        void notifyDirectoryNavigated(Uri docUri);
     }
 }
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index fe6be0e..98d24ed 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -65,8 +65,12 @@
 
     void showChooserForDoc(DocumentInfo doc);
 
+    void openContainerDocument(DocumentInfo doc);
+
     void deleteSelectedDocuments();
 
+    void shareSelectedDocuments();
+
     /**
      * Called when initial activity setup is complete. Implementations
      * should override this method to set the initial location of the
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 005f887..235757e 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -32,11 +32,9 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
-import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.MessageQueue.IdleHandler;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Root;
@@ -59,7 +57,6 @@
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.LocalPreferences;
-import com.android.documentsui.base.PairedTask;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
@@ -80,12 +77,11 @@
 import com.android.documentsui.ui.MessageBuilder;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.Executor;
 
-public abstract class BaseActivity
+public abstract class BaseActivity<T extends ActionHandler>
         extends Activity implements CommonAddons, NavigationViewManager.Environment {
 
     private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
@@ -95,25 +91,23 @@
 
     protected @Nullable RetainedState mRetainedState;
     protected RootsCache mRoots;
+    protected DocumentsAccess mDocs;
     protected MessageBuilder mMessages;
     protected DrawerController mDrawer;
     protected NavigationViewManager mNavigator;
     protected FocusManager mFocusManager;
     protected SortController mSortController;
 
+    protected T mActions;
+
     private final List<EventListener> mEventListeners = new ArrayList<>();
     private final String mTag;
-    private final ContentObserver mRootsCacheObserver = new ContentObserver(
-            new Handler()) {
-                @Override
-                public void onChange(boolean selfChange) {
-                    new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
-                }
-            };
 
     @LayoutRes
     private int mLayoutId;
 
+    private RootsMonitor<BaseActivity> mRootsMonitor;
+
     private boolean mNavDrawerHasFocus;
     private long mStartTime;
 
@@ -202,9 +196,8 @@
         // support to that fragment.
         mRetainedState = (RetainedState) getLastNonConfigurationInstance();
         mRoots = DocumentsApplication.getRootsCache(this);
+        mDocs = DocumentsAccess.create(this);
         mMessages = new MessageBuilder(this);
-        getContentResolver().registerContentObserver(
-                RootsCache.sNotificationUri, false, mRootsCacheObserver);
 
         DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
         setActionBar(toolbar);
@@ -246,6 +239,20 @@
     }
 
     @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+
+        mRootsMonitor = new RootsMonitor<>(
+                this,
+                mActions,
+                mRoots,
+                mDocs,
+                mState,
+                mSearchManager);
+        mRootsMonitor.start();
+    }
+
+    @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         boolean showMenu = super.onCreateOptionsMenu(menu);
 
@@ -267,7 +274,7 @@
 
     @Override
     protected void onDestroy() {
-        getContentResolver().unregisterContentObserver(mRootsCacheObserver);
+        mRootsMonitor.stop();
         super.onDestroy();
     }
 
@@ -344,7 +351,7 @@
             new GetRootDocumentTask(
                     root,
                     this,
-                    this::openContainerDocument)
+                    mActions::openContainerDocument)
                     .executeOnExecutor(getExecutorForCurrentDirectory());
         }
     }
@@ -406,22 +413,6 @@
                 && !root.isDownloads();
     }
 
-    // TODO: Move to ActionHandler...currently blocked by the notifyDirectory....business.
-    @Override
-    public void openContainerDocument(DocumentInfo doc) {
-        assert(doc.isContainer());
-
-        notifyDirectoryNavigated(doc.derivedUri);
-
-        mState.pushDocument(doc);
-        // Show an opening animation only if pressing "back" would get us back to the
-        // previous directory. Especially after opening a root document, pressing
-        // back, wouldn't go to the previous root, but close the activity.
-        final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
-                ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
-        refreshCurrentRootAndDirectory(anim);
-    }
-
     /**
      * Refreshes the content of the director and the menu/action bar.
      * The current directory name and selection will get updated.
@@ -638,21 +629,26 @@
         return super.onKeyDown(keyCode, event);
     }
 
+    @VisibleForTesting
     public void addEventListener(EventListener listener) {
         mEventListeners.add(listener);
     }
 
+    @VisibleForTesting
     public void removeEventListener(EventListener listener) {
         mEventListeners.remove(listener);
     }
 
+    @VisibleForTesting
     public void notifyDirectoryLoaded(Uri uri) {
         for (EventListener listener : mEventListeners) {
             listener.onDirectoryLoaded(uri);
         }
     }
 
-    void notifyDirectoryNavigated(Uri uri) {
+    @VisibleForTesting
+    @Override
+    public void notifyDirectoryNavigated(Uri uri) {
         for (EventListener listener : mEventListeners) {
             listener.onDirectoryNavigated(uri);
         }
@@ -728,62 +724,6 @@
         });
     }
 
-    private static final class HandleRootsChangedTask
-            extends PairedTask<BaseActivity, RootInfo, RootInfo> {
-        RootInfo mCurrentRoot;
-        DocumentInfo mDefaultRootDocument;
-
-        public HandleRootsChangedTask(BaseActivity activity) {
-            super(activity);
-        }
-
-        @Override
-        protected RootInfo run(RootInfo... roots) {
-            assert(roots.length == 1);
-            mCurrentRoot = roots[0];
-            final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
-            for (final RootInfo root : cachedRoots) {
-                if (root.getUri().equals(mCurrentRoot.getUri())) {
-                    // We don't need to change the current root as the current root was not removed.
-                    return null;
-                }
-            }
-
-            // Choose the default root.
-            final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
-            assert(defaultRoot != null);
-            if (!defaultRoot.isRecents()) {
-                mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
-            }
-            return defaultRoot;
-        }
-
-        @Override
-        protected void finish(RootInfo defaultRoot) {
-            if (defaultRoot == null) {
-                return;
-            }
-
-            // If the activity has been launched for the specific root and it is removed, finish the
-            // activity.
-            final Uri uri = mOwner.getIntent().getData();
-            if (uri != null && uri.equals(mCurrentRoot.getUri())) {
-                mOwner.finish();
-                return;
-            }
-
-            // Clear entire backstack and start in new root.
-            mOwner.mState.onRootChanged(defaultRoot);
-            mOwner.mSearchManager.update(defaultRoot);
-
-            if (defaultRoot.isRecents()) {
-                mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
-            } else {
-                mOwner.openContainerDocument(mDefaultRootDocument);
-            }
-        }
-    }
-
     public final class RetainedState {
         public @Nullable Selection selection;
 
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index d2c7d10..56e76bb 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -20,9 +20,11 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
+import android.os.Handler;
 import android.os.OperationCanceledException;
 import android.os.RemoteException;
 import android.provider.DocumentsContract.Document;
@@ -42,8 +44,7 @@
 
     private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
 
-    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
-
+    private final LockingContentObserver mObserver;
     private final RootInfo mRoot;
     private final Uri mUri;
     private final SortModel mModel;
@@ -59,6 +60,7 @@
             DocumentInfo doc,
             Uri uri,
             SortModel model,
+            DirectoryReloadLock lock,
             boolean inSearchMode) {
 
         super(context, ProviderExecutor.forAuthority(root.authority));
@@ -67,6 +69,7 @@
         mModel = model;
         mDoc = doc;
         mSearchMode = inSearchMode;
+        mObserver = new LockingContentObserver(lock, this::onContentChanged);
     }
 
     @Override
@@ -181,4 +184,25 @@
 
         getContext().getContentResolver().unregisterContentObserver(mObserver);
     }
+
+    private static final class LockingContentObserver extends ContentObserver {
+        private final DirectoryReloadLock mLock;
+        private final Runnable mContentChangedCallback;
+
+        public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) {
+            super(new Handler());
+            mLock = lock;
+            mContentChangedCallback = contentChangedCallback;
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mLock.tryUpdate(mContentChangedCallback);
+        }
+    }
 }
diff --git a/src/com/android/documentsui/DirectoryReloadLock.java b/src/com/android/documentsui/DirectoryReloadLock.java
new file mode 100644
index 0000000..8033bb7
--- /dev/null
+++ b/src/com/android/documentsui/DirectoryReloadLock.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.selection.BandController;
+
+/**
+ * A lock used by {@link DirectoryLoader} and {@link BandController} to ensure refresh is blocked
+ * while Band Selection is active.
+ */
+public final class DirectoryReloadLock {
+    private int mPauseCount = 0;
+    private @Nullable Runnable mCallback;
+
+    /**
+     * Increment the block count by 1
+     */
+    @MainThread
+    public void block() {
+        Shared.checkMainLoop();
+        mPauseCount++;
+    }
+
+    /**
+     * Decrement the block count by 1; If no other object is trying to block and there exists some
+     * callback, that callback will be run
+     */
+    @MainThread
+    public void unblock() {
+        Shared.checkMainLoop();
+        mPauseCount--;
+        if (mPauseCount == 0 && mCallback != null) {
+            mCallback.run();
+            mCallback = null;
+        }
+    }
+
+    /**
+     * Attempts to run the given Runnable if not-blocked, or else the Runnable is set to be ran next
+     * (replacing any previous set Runnables).
+     */
+    public void tryUpdate(Runnable update) {
+        if (mPauseCount == 0) {
+            update.run();
+        } else {
+            mCallback = update;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/IconUtils.java b/src/com/android/documentsui/IconUtils.java
index 52f3e73..5a87f87 100644
--- a/src/com/android/documentsui/IconUtils.java
+++ b/src/com/android/documentsui/IconUtils.java
@@ -44,12 +44,6 @@
     public static Drawable loadMimeIcon(
             Context context, String mimeType, String authority, String docId, int mode) {
         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
-            // TODO: eventually move these hacky assets into that package
-            if ("com.android.providers.media.documents".equals(authority)
-                    && docId.startsWith("album")) {
-                return context.getDrawable(R.drawable.ic_doc_album);
-            }
-
             if (mode == State.MODE_GRID) {
                 return context.getDrawable(R.drawable.ic_grid_folder);
             } else {
diff --git a/src/com/android/documentsui/OnRootsChangedTask.java b/src/com/android/documentsui/OnRootsChangedTask.java
deleted file mode 100644
index 0776e8f..0000000
--- a/src/com/android/documentsui/OnRootsChangedTask.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.documentsui;
-
-import android.net.Uri;
-
-import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.PairedTask;
-import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.dirlist.AnimationView;
-
-import java.util.Collection;
-
-final class OnRootsChangedTask
-        extends PairedTask<BaseActivity, RootInfo, RootInfo> {
-    RootInfo mCurrentRoot;
-    DocumentInfo mDefaultRootDocument;
-
-    public OnRootsChangedTask(BaseActivity activity) {
-        super(activity);
-    }
-
-    @Override
-    protected RootInfo run(RootInfo... roots) {
-        assert(roots.length == 1);
-        mCurrentRoot = roots[0];
-        final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
-        for (final RootInfo root : cachedRoots) {
-            if (root.getUri().equals(mCurrentRoot.getUri())) {
-                // We don't need to change the current root as the current root was not removed.
-                return null;
-            }
-        }
-
-        // Choose the default root.
-        final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
-        assert(defaultRoot != null);
-        if (!defaultRoot.isRecents()) {
-            mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
-        }
-        return defaultRoot;
-    }
-
-    @Override
-    protected void finish(RootInfo defaultRoot) {
-        if (defaultRoot == null) {
-            return;
-        }
-
-        // If the activity has been launched for the specific root and it is removed, finish the
-        // activity.
-        final Uri uri = mOwner.getIntent().getData();
-        if (uri != null && uri.equals(mCurrentRoot.getUri())) {
-            mOwner.finish();
-            return;
-        }
-
-        // Clear entire backstack and start in new root.
-        mOwner.mState.onRootChanged(defaultRoot);
-        mOwner.mSearchManager.update(defaultRoot);
-
-        if (defaultRoot.isRecents()) {
-            mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
-        } else {
-            mOwner.openContainerDocument(mDefaultRootDocument);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index 6ce1896..c3ccbba 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -140,7 +140,7 @@
         // Collect all finished tasks
         boolean allDone = true;
         int totalQuerySize = 0;
-        List<Cursor> cursors = new ArrayList<>();
+        List<Cursor> cursors = new ArrayList<>(mTasks.size());
         for (RecentsTask task : mTasks.values()) {
             if (task.isDone()) {
                 try {
@@ -168,6 +168,10 @@
                     throw new RuntimeException(e);
                 } catch (ExecutionException e) {
                     // We already logged on other side
+                } catch (Exception e) {
+                    // Catch exceptions thrown when we read the cursor.
+                    Log.e(TAG, "Failed to query Recents for authority: " + task.authority
+                            + ". Skip this authority in Recents.", e);
                 }
             } else {
                 allDone = false;
@@ -189,7 +193,6 @@
             merged = new MatrixCursor(new String[0]);
         }
 
-
         final Cursor sorted = mState.sortModel.sortCursor(merged);
 
         // Tell the UI if this is an in-progress result. When loading is complete, another update is
diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java
new file mode 100644
index 0000000..feead7a
--- /dev/null
+++ b/src/com/android/documentsui/RootsMonitor.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.documentsui.AbstractActionHandler.CommonAddons;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.PairedTask;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.AnimationView;
+import com.android.documentsui.roots.RootsAccess;
+
+import java.util.Collection;
+
+/**
+ * Monitors roots change and refresh the page when necessary.
+ */
+final class RootsMonitor<T extends Activity & CommonAddons> {
+
+    private final ContentResolver mResolver;
+    private final ContentObserver mObserver;
+
+    RootsMonitor(
+            final T activity,
+            final ActionHandler actions,
+            final RootsAccess roots,
+            final DocumentsAccess docs,
+            final State state,
+            final SearchViewManager searchMgr) {
+        mResolver = activity.getContentResolver();
+
+        mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+            @Override
+            public void onChange(boolean selfChange) {
+                new HandleRootsChangedTask<T>(
+                        activity,
+                        actions,
+                        roots,
+                        docs,
+                        state,
+                        searchMgr).execute(activity.getCurrentRoot());
+            }
+        };
+    }
+
+    void start() {
+        mResolver.registerContentObserver(RootsAccess.NOTIFICATION_URI, false, mObserver);
+    }
+
+    void stop() {
+        mResolver.unregisterContentObserver(mObserver);
+    }
+
+    private static class HandleRootsChangedTask<T extends Activity & CommonAddons>
+            extends PairedTask<T, RootInfo, RootInfo> {
+        private final ActionHandler mActions;
+        private final RootsAccess mRoots;
+        private final DocumentsAccess mDocs;
+        private final State mState;
+        private final SearchViewManager mSearchMgr;
+
+        private RootInfo mCurrentRoot;
+        private DocumentInfo mDefaultRootDocument;
+
+        private HandleRootsChangedTask(
+                T activity,
+                ActionHandler actions,
+                RootsAccess roots,
+                DocumentsAccess docs,
+                State state,
+                SearchViewManager searchMgr) {
+            super(activity);
+            mActions = actions;
+            mRoots = roots;
+            mDocs = docs;
+            mState = state;
+            mSearchMgr = searchMgr;
+        }
+
+        @Override
+        protected RootInfo run(RootInfo... roots) {
+            assert (roots.length == 1);
+            mCurrentRoot = roots[0];
+            final Collection<RootInfo> cachedRoots = mRoots.getRootsBlocking();
+            for (final RootInfo root : cachedRoots) {
+                if (root.getUri().equals(mCurrentRoot.getUri())) {
+                    // We don't need to change the current root as the current root was not removed.
+                    return null;
+                }
+            }
+
+            // Choose the default root.
+            final RootInfo defaultRoot = mRoots.getDefaultRootBlocking(mState);
+            assert (defaultRoot != null);
+            if (!defaultRoot.isRecents()) {
+                mDefaultRootDocument = mDocs.getRootDocument(defaultRoot);
+            }
+            return defaultRoot;
+        }
+
+        @Override
+        protected void finish(RootInfo defaultRoot) {
+            if (defaultRoot == null) {
+                return;
+            }
+
+            // If the activity has been launched for the specific root and it is removed, finish the
+            // activity.
+            final Uri uri = mOwner.getIntent().getData();
+            if (uri != null && uri.equals(mCurrentRoot.getUri())) {
+                mOwner.finish();
+                return;
+            }
+
+            // Clear entire backstack and start in new root.
+            mState.onRootChanged(defaultRoot);
+            mSearchMgr.update(defaultRoot);
+
+            if (defaultRoot.isRecents()) {
+                mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+            } else {
+                mActions.openContainerDocument(mDefaultRootDocument);
+            }
+        }
+    }
+}
diff --git a/src/com/android/documentsui/base/FilteringCursorWrapper.java b/src/com/android/documentsui/base/FilteringCursorWrapper.java
index 6604171..0288f4f 100644
--- a/src/com/android/documentsui/base/FilteringCursorWrapper.java
+++ b/src/com/android/documentsui/base/FilteringCursorWrapper.java
@@ -55,13 +55,13 @@
         while (cursor.moveToNext() && mCount < count) {
             final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
             final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-            if (rejectMimes != null && MimePredicate.mimeMatches(rejectMimes, mimeType)) {
+            if (rejectMimes != null && MimeTypes.mimeMatches(rejectMimes, mimeType)) {
                 continue;
             }
             if (lastModified < rejectBefore) {
                 continue;
             }
-            if (MimePredicate.mimeMatches(acceptMimes, mimeType)) {
+            if (MimeTypes.mimeMatches(acceptMimes, mimeType)) {
                 mPosition[mCount++] = cursor.getPosition();
             }
         }
diff --git a/src/com/android/documentsui/base/MimePredicate.java b/src/com/android/documentsui/base/MimeTypes.java
similarity index 76%
rename from src/com/android/documentsui/base/MimePredicate.java
rename to src/com/android/documentsui/base/MimeTypes.java
index b3ba862..0f61e67 100644
--- a/src/com/android/documentsui/base/MimePredicate.java
+++ b/src/com/android/documentsui/base/MimeTypes.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,16 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.documentsui.base;
 
 import android.annotation.Nullable;
 import android.provider.DocumentsContract.Document;
 
-import com.android.internal.util.Predicate;
+import java.util.List;
 
-public class MimePredicate implements Predicate<DocumentInfo> {
-    private final String[] mFilters;
+public final class MimeTypes {
+
+    private MimeTypes() {}
 
     private static final String APK_TYPE = "application/vnd.android.package-archive";
     /**
@@ -31,19 +31,28 @@
      */
     public static final String[] VISUAL_MIMES = new String[] { "image/*", "video/*" };
 
-    public MimePredicate(String[] filters) {
-        mFilters = filters;
-    }
+    public static String findCommonMimeType(List<String> mimeTypes) {
+        String[] commonType = mimeTypes.get(0).split("/");
+        if (commonType.length != 2) {
+            return "*/*";
+        }
 
-    @Override
-    public boolean apply(DocumentInfo doc) {
-        if (doc.isDirectory()) {
-            return true;
+        for (int i = 1; i < mimeTypes.size(); i++) {
+            String[] type = mimeTypes.get(i).split("/");
+            if (type.length != 2) continue;
+
+            if (!commonType[1].equals(type[1])) {
+                commonType[1] = "*";
+            }
+
+            if (!commonType[0].equals(type[0])) {
+                commonType[0] = "*";
+                commonType[1] = "*";
+                break;
+            }
         }
-        if (mimeMatches(mFilters, doc.mimeType)) {
-            return true;
-        }
-        return false;
+
+        return commonType[0] + "/" + commonType[1];
     }
 
     public static boolean mimeMatches(String[] filters, String[] tests) {
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index f84f915..3a19ef7 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -350,7 +350,7 @@
     }
 
     /**
-     * @deprecate use {@link DocumentsAccess#getRootDocumentBlocking}.
+     * @deprecate use {@link DocumentsAccess#getRootDocument}.
      */
     @Deprecated
     public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 306644d..e230311 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -67,6 +67,7 @@
 import com.android.documentsui.BaseActivity.RetainedState;
 import com.android.documentsui.DirectoryLoader;
 import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.ItemDragListener;
@@ -117,8 +118,7 @@
  * Display the documents inside a single directory.
  */
 public class DirectoryFragment extends Fragment
-        implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult>,
-        ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
+        implements ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
 
     @IntDef(flag = true, value = {
             TYPE_NORMAL,
@@ -142,8 +142,12 @@
     private static final int CACHE_EVICT_LIMIT = 100;
     private static final int REFRESH_SPINNER_DISMISS_DELAY = 500;
 
+    private BaseActivity<?> mActivity;
+    private State mState;
     private final Model mModel = new Model();
     private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
+    private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment();
+    private final LoaderCallbacks<DirectoryResult> mLoaderCallbacks = new LoaderBindings();
 
     // This dependency is informally "injected" from the owning Activity in our onCreate method.
     private ActivityConfig mActivityConfig;
@@ -183,6 +187,7 @@
     private View mProgressBar;
 
     private DirectoryState mLocalState;
+    private DirectoryReloadLock mReloadLock = new DirectoryReloadLock();
 
     // Note, we use !null to indicate that selection was restored (from rotation).
     // So don't fiddle with this field unless you've got the bigger picture in mind.
@@ -191,7 +196,7 @@
     private SortModel.UpdateListener mSortListener = (model, updateType) -> {
         // Only when sort order has changed do we need to trigger another loading.
         if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
-            getLoaderManager().restartLoader(LOADER_ID, null, this);
+            getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
         }
     };
 
@@ -199,10 +204,11 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
+        BaseActivity activity = (BaseActivity<?>) getActivity();
         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
 
         mMessageBar = MessageBar.create(getChildFragmentManager());
-        mProgressBar = getActivity().findViewById(R.id.progressbar);
+        mProgressBar = activity.findViewById(R.id.progressbar);
         assert(mProgressBar != null);
 
         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
@@ -217,10 +223,10 @@
                         cancelThumbnailTask(holder.itemView);
                     }
                 });
-        mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
+        mRecView.setItemAnimator(new DirectoryItemAnimator(activity));
         mFileList = view.findViewById(R.id.file_list);
 
-        mActivityConfig = getBaseActivity().getActivityConfig();
+        mActivityConfig = activity.getActivityConfig();
         mDragHoverListener = mActivityConfig.dragAndDropEnabled()
                 ? DragHoverListener.create(new DirectoryDragListener(this), mRecView)
                 : null;
@@ -250,8 +256,8 @@
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
-        final Context context = getActivity();
-        final State state = getDisplayState();
+        mActivity = (BaseActivity) getActivity();
+        mState = mActivity.getDisplayState();
 
         // Read arguments when object created for the first time.
         // Restore state if fragment recreated.
@@ -261,7 +267,7 @@
         mLocalState.restore(args);
 
         // Restore any selection we may have squirreled away in retained state.
-        @Nullable RetainedState retained = getBaseActivity().getRetainedState();
+        @Nullable RetainedState retained = mActivity.getRetainedState();
         if (retained != null && retained.hasSelection()) {
             // We claim the selection for ourselves and null it out once used
             // so we don't have a rando selection hanging around in RetainedState.
@@ -269,11 +275,11 @@
             retained.selection = null;
         }
 
-        mIconHelper = new IconHelper(context, MODE_GRID);
+        mIconHelper = new IconHelper(mActivity, MODE_GRID);
         mClipper = DocumentsApplication.getDocumentClipper(getContext());
 
         mAdapter = new SectionBreakDocumentsAdapterWrapper(
-                this, new ModelBackedDocumentsAdapter(this, mIconHelper));
+                mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper));
 
         mRecView.setAdapter(mAdapter);
 
@@ -294,24 +300,23 @@
         mModel.addUpdateListener(mAdapter.getModelUpdateListener());
         mModel.addUpdateListener(mModelUpdateListener);
 
-
-        final BaseActivity activity = getBaseActivity();
-        mSelectionMgr = activity.getSelectionManager(mAdapter, this::canSetSelectionState);
-        mFocusManager = activity.getFocusManager(mRecView, mModel);
-        mActions = activity.getActionHandler(mModel, mLocalState.mSearchMode);
-        mMenuManager = activity.getMenuManager();
-        mDialogs = activity.getDialogController();
+        mSelectionMgr = mActivity.getSelectionManager(mAdapter, this::canSetSelectionState);
+        mFocusManager = mActivity.getFocusManager(mRecView, mModel);
+        mActions = mActivity.getActionHandler(mModel, mLocalState.mSearchMode);
+        mMenuManager = mActivity.getMenuManager();
+        mDialogs = mActivity.getDialogController();
 
         mSelectionMetadata = new SelectionMetadata(mModel::getItem);
         mSelectionMgr.addItemCallback(mSelectionMetadata);
 
-        GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView);
+        GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView, mReloadLock);
 
-        if (state.allowMultiple) {
+        if (mState.allowMultiple) {
             mBandController = new BandController(
                     mRecView,
                     mAdapter,
                     mSelectionMgr,
+                    mReloadLock,
                     (int pos) -> {
                         // The band selection model only operates on documents and directories.
                         // Exclude other types of adapter items like whitespace and dividers.
@@ -323,17 +328,17 @@
         DragStartListener mDragStartListener = mActivityConfig.dragAndDropEnabled()
                 ? DragStartListener.create(
                         mIconHelper,
-                        getContext(),
+                        mActivity,
                         mModel,
                         mSelectionMgr,
                         mClipper,
-                        getDisplayState(),
+                        mState,
                         this::getModelId,
                         mRecView::findChildViewUnder,
                         getContext().getDrawable(com.android.internal.R.drawable.ic_doc_generic))
                 : DragStartListener.DUMMY;
 
-        EventHandler<InputEvent> gestureHandler = state.allowMultiple
+        EventHandler<InputEvent> gestureHandler = mState.allowMultiple
                 ? gestureSel::start
                 : EventHandler.createStub(false);
         mInputHandler = new UserInputHandler<>(
@@ -355,16 +360,16 @@
                 mInputHandler,
                 mBandController);
 
-        mMenuManager = activity.getMenuManager();
+        mMenuManager = mActivity.getMenuManager();
 
-        mActionModeController = activity.getActionModeController(
+        mActionModeController = mActivity.getActionModeController(
                 mSelectionMetadata,
                 this::handleMenuItemClick,
                 mRecView);
 
         mSelectionMgr.addCallback(mActionModeController);
 
-        final ActivityManager am = (ActivityManager) context.getSystemService(
+        final ActivityManager am = (ActivityManager) mActivity.getSystemService(
                 Context.ACTIVITY_SERVICE);
         boolean svelte = am.isLowRamDevice() && (mLocalState.mType == TYPE_RECENT_OPEN);
         mIconHelper.setThumbnailsEnabled(!svelte);
@@ -375,13 +380,13 @@
                         ? (mLocalState.mDocument.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0
                         : true;
         // Call this before adding the listener to avoid restarting the loader one more time
-        state.sortModel.setDefaultDimension(
+        mState.sortModel.setDefaultDimension(
                 prefersLastModified
                         ? SortModel.SORT_DIMENSION_ID_DATE
                         : SortModel.SORT_DIMENSION_ID_TITLE);
 
         // Kick off loader at least once
-        getLoaderManager().restartLoader(LOADER_ID, null, this);
+        getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
     }
 
     @Override
@@ -389,20 +394,19 @@
         super.onStart();
 
         // Add listener to update contents on sort model change
-        getDisplayState().sortModel.addListener(mSortListener);
+        mState.sortModel.addListener(mSortListener);
     }
 
     @Override
     public void onStop() {
         super.onStop();
 
-        getDisplayState().sortModel.removeListener(mSortListener);
+        mState.sortModel.removeListener(mSortListener);
 
         // Remember last scroll location
         final SparseArray<Parcelable> container = new SparseArray<>();
         getView().saveHierarchyState(container);
-        final State state = getDisplayState();
-        state.dirConfigs.put(mLocalState.getConfigKey(), container);
+        mState.dirConfigs.put(mLocalState.getConfigKey(), container);
     }
 
     public void retainState(RetainedState state) {
@@ -417,17 +421,6 @@
     }
 
     @Override
-    public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
-        switch (requestCode) {
-            case REQUEST_COPY_DESTINATION:
-                handleCopyResult(resultCode, data);
-                break;
-            default:
-                throw new UnsupportedOperationException("Unknown request code: " + requestCode);
-        }
-    }
-
-    @Override
     public void onCreateContextMenu(ContextMenu menu,
             View v,
             ContextMenu.ContextMenuInfo menuInfo) {
@@ -463,7 +456,7 @@
 
         operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
 
-        FileOperations.start(getBaseActivity(), operation, mDialogs::showFileOperationFailures);
+        FileOperations.start(mActivity, operation, mDialogs::showFileOperationFailures);
     }
 
     protected boolean onRightClick(InputEvent e) {
@@ -494,8 +487,7 @@
     }
 
     private void updateDisplayState() {
-        State state = getDisplayState();
-        updateLayout(state.derivedMode);
+        updateLayout(mState.derivedMode);
         mRecView.setAdapter(mAdapter);
     }
 
@@ -547,17 +539,6 @@
         }
     }
 
-    @Override
-    public int getColumnCount() {
-        return mColumnCount;
-    }
-
-    // Support method to replace getOwner().foo() with something
-    // slightly less clumsy like: getOwner().foo().
-    private BaseActivity getBaseActivity() {
-        return (BaseActivity) getActivity();
-    }
-
     private boolean handleMenuItemClick(MenuItem item) {
         Selection selection = mSelectionMgr.getSelection(new Selection());
 
@@ -576,7 +557,7 @@
                 return true;
 
             case R.id.menu_share:
-                shareDocuments(selection);
+                mActions.shareSelectedDocuments();
                 // TODO: Only finish selection if share action is completed.
                 mActionModeController.finishActionMode();
                 return true;
@@ -629,7 +610,7 @@
 
             default:
                 // See if BaseActivity can handle this particular MenuItem
-                if (!getBaseActivity().onOptionsItemSelected(item)) {
+                if (!mActivity.onOptionsItemSelected(item)) {
                     if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
                     return false;
                 }
@@ -659,11 +640,10 @@
 
         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
         List<DocumentInfo> docs = mModel.getDocuments(selected);
-        BaseActivity activity = getBaseActivity();
         if (docs.size() > 1) {
-            activity.onDocumentsPicked(docs);
+            mActivity.onDocumentsPicked(docs);
         } else {
-            activity.onDocumentPicked(docs.get(0));
+            mActivity.onDocumentPicked(docs.get(0));
         }
     }
 
@@ -676,54 +656,6 @@
         mActions.showChooserForDoc(doc);
     }
 
-    private void shareDocuments(final Selection selected) {
-        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
-
-        // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
-        List<DocumentInfo> docs = mModel.getDocuments(selected);
-
-        Intent intent;
-
-        // Filter out directories and virtual files - those can't be shared.
-        List<DocumentInfo> docsForSend = new ArrayList<>();
-        for (DocumentInfo doc: docs) {
-            if (!doc.isDirectory() && !doc.isVirtualDocument()) {
-                docsForSend.add(doc);
-            }
-        }
-
-        if (docsForSend.size() == 1) {
-            final DocumentInfo doc = docsForSend.get(0);
-
-            intent = new Intent(Intent.ACTION_SEND);
-            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-            intent.addCategory(Intent.CATEGORY_DEFAULT);
-            intent.setType(doc.mimeType);
-            intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
-
-        } else if (docsForSend.size() > 1) {
-            intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
-            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-            intent.addCategory(Intent.CATEGORY_DEFAULT);
-
-            final ArrayList<String> mimeTypes = new ArrayList<>();
-            final ArrayList<Uri> uris = new ArrayList<>();
-            for (DocumentInfo doc : docsForSend) {
-                mimeTypes.add(doc.mimeType);
-                uris.add(doc.derivedUri);
-            }
-
-            intent.setType(findCommonMimeType(mimeTypes));
-            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
-
-        } else {
-            return;
-        }
-
-        intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
-        startActivity(intent);
-    }
-
     private void transferDocuments(final Selection selected, final @OpType int mode) {
         if (mode == FileOperationService.OPERATION_COPY) {
             Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
@@ -747,7 +679,7 @@
             throw new RuntimeException("Failed to create uri supplier.", e);
         }
 
-        Uri srcParent = getDisplayState().stack.peek().derivedUri;
+        Uri srcParent = mState.stack.peek().derivedUri;
         mLocalState.mPendingOperation = new FileOperation.Builder()
                 .withOpType(mode)
                 .withSrcParent(srcParent)
@@ -784,6 +716,17 @@
         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
     }
 
+    @Override
+    public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_COPY_DESTINATION:
+                handleCopyResult(resultCode, data);
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown request code: " + requestCode);
+        }
+    }
+
     private static boolean hasDirectory(List<DocumentInfo> docs) {
         for (DocumentInfo info : docs) {
             if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
@@ -805,30 +748,8 @@
         RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
     }
 
-    @Override
-    public void initDocumentHolder(DocumentHolder holder) {
-        holder.addKeyEventListener(mInputHandler);
-        holder.itemView.setOnFocusChangeListener(mFocusManager);
-    }
-
-    @Override
-    public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
-        setupDragAndDropOnDocumentView(holder.itemView, cursor);
-    }
-
-    @Override
-    public State getDisplayState() {
-        return getBaseActivity().getDisplayState();
-    }
-
-    @Override
-    public Model getModel() {
-        return mModel;
-    }
-
-    @Override
-    public boolean isDocumentEnabled(String docMimeType, int docFlags) {
-        return mActivityConfig.isDocumentEnabled(docMimeType, docFlags, getDisplayState());
+    private boolean isDocumentEnabled(String mimeType, int flags) {
+        return mActivityConfig.isDocumentEnabled(mimeType, flags, mState);
     }
 
     private void showEmptyDirectory() {
@@ -866,30 +787,6 @@
         mRecView.requestFocus();
     }
 
-    private String findCommonMimeType(List<String> mimeTypes) {
-        String[] commonType = mimeTypes.get(0).split("/");
-        if (commonType.length != 2) {
-            return "*/*";
-        }
-
-        for (int i = 1; i < mimeTypes.size(); i++) {
-            String[] type = mimeTypes.get(i).split("/");
-            if (type.length != 2) continue;
-
-            if (!commonType[1].equals(type[1])) {
-                commonType[1] = "*";
-            }
-
-            if (!commonType[0].equals(type[0])) {
-                commonType[0] = "*";
-                commonType[1] = "*";
-                break;
-            }
-        }
-
-        return commonType[0] + "/" + commonType[1];
-    }
-
     public void copySelectedToClipboard() {
         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
 
@@ -913,7 +810,7 @@
         }
         mSelectionMgr.clearSelection();
 
-        mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
+        mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
 
         Snackbars.showDocumentsClipped(getActivity(), selection.size());
     }
@@ -924,7 +821,7 @@
         BaseActivity activity = (BaseActivity) getActivity();
         DocumentInfo destination = activity.getCurrentDirectory();
         mClipper.copyFromClipboard(
-                destination, activity.getDisplayState().stack, mDialogs::showFileOperationFailures);
+                destination, mState.stack, mDialogs::showFileOperationFailures);
         getActivity().invalidateOptionsMenu();
     }
 
@@ -937,10 +834,10 @@
             Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
             return;
         }
-        BaseActivity activity = getBaseActivity();
+        BaseActivity activity = mActivity;
         DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
         mClipper.copyFromClipboard(
-                destination, activity.getDisplayState().stack, mDialogs::showFileOperationFailures);
+                destination, mState.stack, mDialogs::showFileOperationFailures);
         getActivity().invalidateOptionsMenu();
     }
 
@@ -950,7 +847,7 @@
         // Exclude disabled files
         List<String> enabled = new ArrayList<>();
         for (String id : mAdapter.getModelIds()) {
-            Cursor cursor = getModel().getItem(id);
+            Cursor cursor = mModel.getItem(id);
             if (cursor == null) {
                 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
                 continue;
@@ -1003,7 +900,7 @@
      */
     @Override
     public void onDragEntered(View view) {
-        getBaseActivity().setRootsDrawerOpen(false);
+        mActivity.setRootsDrawerOpen(false);
     }
 
     /**
@@ -1013,7 +910,7 @@
      */
     @Override
     public void onViewHovered(View view) {
-        BaseActivity activity = getBaseActivity();
+        BaseActivity activity = mActivity;
         if (getModelId(view) != null) {
            activity.springOpenDirectory(getDestination(view));
         }
@@ -1044,7 +941,7 @@
 
         DocumentInfo dst = getDestination(v);
         mClipper.copyFromClipData(
-                dst, getDisplayState().stack, clipData, mDialogs::showFileOperationFailures);
+                dst, mState.stack, clipData, mDialogs::showFileOperationFailures);
         return true;
     }
 
@@ -1077,7 +974,7 @@
         }
 
         if (v == mRecView || v == mEmptyView) {
-            return getDisplayState().stack.peek();
+            return mState.stack.peek();
         }
 
         return null;
@@ -1112,48 +1009,6 @@
         return null;
     }
 
-    @Override
-    public boolean isSelected(String modelId) {
-        return mSelectionMgr.getSelection().contains(modelId);
-    }
-
-    private final class ModelUpdateListener implements EventListener<Model.Update> {
-
-        @Override
-        public void accept(Model.Update update) {
-            if (update.hasError()) {
-                showQueryError();
-                return;
-            }
-
-            if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
-
-            if (mModel.info != null || mModel.error != null) {
-                mMessageBar.setInfo(mModel.info);
-                mMessageBar.setError(mModel.error);
-                mMessageBar.show();
-            }
-
-            mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
-
-            if (mModel.isEmpty()) {
-                if (mLocalState.mSearchMode) {
-                    showNoResults(getDisplayState().stack.root);
-                } else {
-                    showEmptyDirectory();
-                }
-            } else {
-                showDirectory();
-                mAdapter.notifyDataSetChanged();
-            }
-
-            if (!mModel.isLoading()) {
-                getBaseActivity().notifyDirectoryLoaded(
-                        mModel.doc != null ? mModel.doc.derivedUri : null);
-            }
-        }
-    }
-
     // TODO: Move to activities when Model becomes activity level object.
     private boolean canSelect(DocumentDetails doc) {
         return canSetSelectionState(doc.getModelId(), true);
@@ -1171,7 +1026,7 @@
 
             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-            return mActivityConfig.canSelectType(docMimeType, docFlags, getDisplayState());
+            return mActivityConfig.canSelectType(docMimeType, docFlags, mState);
         } else {
             // Right now all selected items can be deselected.
             return true;
@@ -1193,7 +1048,7 @@
         DirectoryFragment df = get(fm);
 
         df.mLocalState.update(root, doc, query);
-        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+        df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
     }
 
     public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
@@ -1202,7 +1057,7 @@
         DirectoryFragment df = get(fm);
 
         df.mLocalState.update(type, root, doc, query);
-        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+        df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
     }
 
     public static void create(
@@ -1263,96 +1118,193 @@
         }
 
         // Trigger loading
-        getLoaderManager().restartLoader(LOADER_ID, null, this);
+        getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
     }
 
-    @Override
-    public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
-        Context context = getActivity();
-        State state = getDisplayState();
+    private final class ModelUpdateListener implements EventListener<Model.Update> {
 
-        Uri contentsUri;
-        switch (mLocalState.mType) {
-            case TYPE_NORMAL:
-                contentsUri = mLocalState.mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
-                        mLocalState.mRoot.authority, mLocalState.mRoot.rootId, mLocalState.mQuery)
-                        : DocumentsContract.buildChildDocumentsUri(
-                                mLocalState.mDocument.authority, mLocalState.mDocument.documentId);
-                if (mActivityConfig.managedModeEnabled(state.stack)) {
-                    contentsUri = DocumentsContract.setManageMode(contentsUri);
+        @Override
+        public void accept(Model.Update update) {
+            if (update.hasError()) {
+                showQueryError();
+                return;
+            }
+
+            if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
+
+            if (mModel.info != null || mModel.error != null) {
+                mMessageBar.setInfo(mModel.info);
+                mMessageBar.setError(mModel.error);
+                mMessageBar.show();
+            }
+
+            mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
+
+            if (mModel.isEmpty()) {
+                if (mLocalState.mSearchMode) {
+                    showNoResults(mState.stack.root);
+                } else {
+                    showEmptyDirectory();
                 }
-                if (DEBUG) Log.d(TAG, "Creating new directory loader for: "
-                        + DocumentInfo.debugString(mLocalState.mDocument));
-                return new DirectoryLoader(
-                        context, mLocalState.mRoot, mLocalState.mDocument, contentsUri, state.sortModel,
-                        mLocalState.mSearchMode);
-            case TYPE_RECENT_OPEN:
-                if (DEBUG) Log.d(TAG, "Creating new loader recents.");
-                final RootsAccess roots = DocumentsApplication.getRootsCache(context);
-                return new RecentsLoader(context, roots, state);
+            } else {
+                showDirectory();
+                mAdapter.notifyDataSetChanged();
+            }
 
-            default:
-                throw new IllegalStateException("Unknown type " + mLocalState.mType);
+            if (!mModel.isLoading()) {
+                mActivity.notifyDirectoryLoaded(
+                        mModel.doc != null ? mModel.doc.derivedUri : null);
+            }
         }
     }
 
-    @Override
-    public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
-        if (DEBUG) Log.d(TAG, "Loader has finished for: "
-                + DocumentInfo.debugString(mLocalState.mDocument));
-        assert(result != null);
+    private final class AdapterEnvironment implements DocumentsAdapter.Environment {
 
-        if (!isAdded()) return;
-
-        if (mLocalState.mSearchMode) {
-            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
+        @Override
+        public Context getContext() {
+            return mActivity;
         }
 
-        State state = getDisplayState();
-
-        mAdapter.notifyDataSetChanged();
-        mModel.update(result);
-
-        updateLayout(state.derivedMode);
-
-        if (mRestoredSelection != null) {
-            mSelectionMgr.restoreSelection(mRestoredSelection);
-            // Note, we'll take care of cleaning up retained selection
-            // in the selection handler where we already have some
-            // specialized code to handle when selection was restored.
+        @Override
+        public State getDisplayState() {
+            return mState;
         }
 
-        // Restore any previous instance state
-        final SparseArray<Parcelable> container = state.dirConfigs.remove(mLocalState.getConfigKey());
-        final int curSortedDimensionId = state.sortModel.getSortedDimensionId();
-
-        final SortDimension curSortedDimension =
-                state.sortModel.getDimensionById(curSortedDimensionId);
-        if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
-            getView().restoreHierarchyState(container);
-        } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
-                || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
-                || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
-            // Scroll to the top if the sort order actually changed.
-            mRecView.smoothScrollToPosition(0);
+        @Override
+        public Model getModel() {
+            return mModel;
         }
 
-        mLocalState.mLastSortDimensionId = curSortedDimension.getId();
-        mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
+        @Override
+        public int getColumnCount() {
+            return mColumnCount;
+        }
 
-        if (mRefreshLayout.isRefreshing()) {
-            new Handler().postDelayed(
-                    () -> mRefreshLayout.setRefreshing(false),
-                    REFRESH_SPINNER_DISMISS_DELAY);
+        @Override
+        public boolean isSelected(String id) {
+            return mSelectionMgr.getSelection().contains(id);
+        }
+
+        @Override
+        public boolean isDocumentEnabled(String mimeType, int flags) {
+            return mActivityConfig.isDocumentEnabled(mimeType, flags, mState);
+        }
+
+        @Override
+        public void initDocumentHolder(DocumentHolder holder) {
+            holder.addKeyEventListener(mInputHandler);
+            holder.itemView.setOnFocusChangeListener(mFocusManager);
+        }
+
+        @Override
+        public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
+            setupDragAndDropOnDocumentView(holder.itemView, cursor);
         }
     }
 
-    @Override
-    public void onLoaderReset(Loader<DirectoryResult> loader) {
-        if (DEBUG) Log.d(TAG, "Resetting loader for: "
-                + DocumentInfo.debugString(mLocalState.mDocument));
-        mModel.onLoaderReset();
+    private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
 
-        mRefreshLayout.setRefreshing(false);
+        @Override
+        public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+            Context context = getActivity();
+
+            Uri contentsUri;
+            switch (mLocalState.mType) {
+                case TYPE_NORMAL:
+                    contentsUri = mLocalState.mSearchMode
+                            ? DocumentsContract.buildSearchDocumentsUri(
+                                    mLocalState.mRoot.authority,
+                                    mLocalState.mRoot.rootId,
+                                    mLocalState.mQuery)
+                            : DocumentsContract.buildChildDocumentsUri(
+                                    mLocalState.mDocument.authority,
+                                    mLocalState.mDocument.documentId);
+
+                    if (mActivityConfig.managedModeEnabled(mState.stack)) {
+                        contentsUri = DocumentsContract.setManageMode(contentsUri);
+                    }
+
+                    if (DEBUG) Log.d(TAG,
+                            "Creating new directory loader for: "
+                            + DocumentInfo.debugString(mLocalState.mDocument));
+
+                    return new DirectoryLoader(
+                            context,
+                            mLocalState.mRoot,
+                            mLocalState.mDocument,
+                            contentsUri,
+                            mState.sortModel,
+                            mReloadLock,
+                            mLocalState.mSearchMode);
+
+                case TYPE_RECENT_OPEN:
+                    if (DEBUG) Log.d(TAG, "Creating new loader recents.");
+                    final RootsAccess roots = DocumentsApplication.getRootsCache(context);
+                    return new RecentsLoader(context, roots, mState);
+
+                default:
+                    throw new IllegalStateException("Unknown type " + mLocalState.mType);
+            }
+        }
+
+        @Override
+        public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+            if (DEBUG) Log.d(TAG, "Loader has finished for: "
+                    + DocumentInfo.debugString(mLocalState.mDocument));
+            assert(result != null);
+
+            if (!isAdded()) return;
+
+            if (mLocalState.mSearchMode) {
+                Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
+            }
+
+            mAdapter.notifyDataSetChanged();
+            mModel.update(result);
+
+            updateLayout(mState.derivedMode);
+
+            if (mRestoredSelection != null) {
+                mSelectionMgr.restoreSelection(mRestoredSelection);
+                // Note, we'll take care of cleaning up retained selection
+                // in the selection handler where we already have some
+                // specialized code to handle when selection was restored.
+            }
+
+            // Restore any previous instance state
+            final SparseArray<Parcelable> container =
+                    mState.dirConfigs.remove(mLocalState.getConfigKey());
+            final int curSortedDimensionId = mState.sortModel.getSortedDimensionId();
+
+            final SortDimension curSortedDimension =
+                    mState.sortModel.getDimensionById(curSortedDimensionId);
+            if (container != null
+                    && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
+                getView().restoreHierarchyState(container);
+            } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
+                    || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
+                    || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
+                // Scroll to the top if the sort order actually changed.
+                mRecView.smoothScrollToPosition(0);
+            }
+
+            mLocalState.mLastSortDimensionId = curSortedDimension.getId();
+            mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
+
+            if (mRefreshLayout.isRefreshing()) {
+                new Handler().postDelayed(
+                        () -> mRefreshLayout.setRefreshing(false),
+                        REFRESH_SPINNER_DISMISS_DELAY);
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<DirectoryResult> loader) {
+            if (DEBUG) Log.d(TAG, "Resetting loader for: "
+                    + DocumentInfo.debugString(mLocalState.mDocument));
+            mModel.onLoaderReset();
+
+            mRefreshLayout.setRefreshing(false);
+        }
     }
 }
diff --git a/src/com/android/documentsui/dirlist/IconHelper.java b/src/com/android/documentsui/dirlist/IconHelper.java
index 14c569d..6f6327b 100644
--- a/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/src/com/android/documentsui/dirlist/IconHelper.java
@@ -45,7 +45,7 @@
 import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.ThumbnailCache.Result;
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.State.ViewMode;
 
@@ -233,7 +233,7 @@
 
         final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
         final boolean allowThumbnail = (mMode == MODE_GRID)
-                || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mimeType);
+                || MimeTypes.mimeMatches(MimeTypes.VISUAL_MIMES, mimeType);
         final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
         if (showThumbnail) {
             loadedThumbnail =
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 016346a..8bdf079 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -42,12 +42,25 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 
 /**
  * The data model for the current loaded directory.
  */
 @VisibleForTesting
 public class Model {
+
+    /**
+     * Filter that passes (returns true) all non-virtual, non-partial files.
+     */
+    public static final Predicate<Cursor> CONCRETE_FILE_FILTER = (Cursor c) -> {
+        int flags = DocumentInfo.getCursorInt(c, Document.COLUMN_FLAGS);
+        return (flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0
+                && (flags & Document.FLAG_PARTIAL) == 0;
+    };
+
+    private static final Predicate<Cursor> ANY_FILE_FILTER = (Cursor c) -> true;
+
     private static final String TAG = "Model";
 
     private boolean mIsLoading;
@@ -193,19 +206,7 @@
     }
 
     public List<DocumentInfo> getDocuments(Selection selection) {
-        final int size = (selection != null) ? selection.size() : 0;
-
-        final List<DocumentInfo> docs =  new ArrayList<>(size);
-        // NOTE: That as this now iterates over only final (non-provisional) selection.
-        for (String modelId: selection) {
-            DocumentInfo doc = getDocument(modelId);
-            if (doc == null) {
-                Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
-                continue;
-            }
-            docs.add(doc);
-        }
-        return docs;
+        return loadDocuments(selection, ANY_FILE_FILTER);
     }
 
     public @Nullable DocumentInfo getDocument(String modelId) {
@@ -215,6 +216,35 @@
                 : DocumentInfo.fromDirectoryCursor(cursor);
     }
 
+    public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
+        final int size = (selection != null) ? selection.size() : 0;
+
+        final List<DocumentInfo> docs =  new ArrayList<>(size);
+        for (String modelId: selection) {
+            loadDocument(docs, modelId, filter);
+        }
+        return docs;
+    }
+
+    /**
+     * @param docs
+     * @return DocumentInfo, or null. If filter returns false, null will be returned.
+     */
+    private void loadDocument(
+            List<DocumentInfo> docs, String modelId, Predicate<Cursor> filter) {
+        final Cursor cursor = getItem(modelId);
+
+        if (cursor == null) {
+            Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
+        }
+
+        if (filter.test(cursor)) {
+            docs.add(DocumentInfo.fromDirectoryCursor(cursor));
+        } else {
+            if (DEBUG) Log.v(TAG, "Filtered document from results: " + modelId);
+        }
+    }
+
     public Uri getItemUri(String modelId) {
         final Cursor cursor = getItem(modelId);
         return DocumentInfo.getUri(cursor);
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 993af36..d59e2a7 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -32,12 +32,15 @@
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Metrics;
+import com.android.documentsui.R;
+import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.ConfirmationCallback;
 import com.android.documentsui.base.ConfirmationCallback.Result;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Lookup;
+import com.android.documentsui.base.MimeTypes;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.clipping.ClipStore;
@@ -58,6 +61,7 @@
 import com.android.documentsui.ui.DialogController;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -83,6 +87,7 @@
             RootsAccess roots,
             DocumentsAccess docs,
             SelectionManager selectionMgr,
+            SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
             ActionModeAddons actionModeAddons,
             DialogController dialogs,
@@ -90,7 +95,7 @@
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, docs, selectionMgr, executors);
+        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
 
         mActionModeAddons = actionModeAddons;
         mDialogs = dialogs;
@@ -231,6 +236,54 @@
     }
 
     @Override
+    public void shareSelectedDocuments() {
+        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SHARE);
+
+        Selection selection = getStableSelection();
+
+        assert(!selection.isEmpty());
+
+        // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
+        List<DocumentInfo> docs =
+                mScope.model.loadDocuments(selection, Model.CONCRETE_FILE_FILTER);
+
+        Intent intent;
+
+        if (docs.size() == 1) {
+            final DocumentInfo doc = docs.get(0);
+
+            intent = new Intent(Intent.ACTION_SEND);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.setType(doc.mimeType);
+            intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
+
+        } else if (docs.size() > 1) {
+            intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+
+            final ArrayList<String> mimeTypes = new ArrayList<>();
+            final ArrayList<Uri> uris = new ArrayList<>();
+            for (DocumentInfo doc : docs) {
+                mimeTypes.add(doc.mimeType);
+                uris.add(doc.derivedUri);
+            }
+
+            intent.setType(MimeTypes.findCommonMimeType(mimeTypes));
+            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+
+        } else {
+            return;
+        }
+
+        Intent chooserIntent = Intent.createChooser(
+                intent, mActivity.getResources().getText(R.string.share_via));
+
+        mActivity.startActivity(chooserIntent);
+    }
+
+    @Override
     public void initLocation(Intent intent) {
         assert(intent != null);
 
@@ -329,7 +382,7 @@
 
     public void onDocumentPicked(DocumentInfo doc) {
         if (doc.isContainer()) {
-            mActivity.openContainerDocument(doc);
+            openContainerDocument(doc);
             return;
         }
 
@@ -351,7 +404,7 @@
         }
 
         if (doc.isContainer()) {
-            mActivity.openContainerDocument(doc);
+            openContainerDocument(doc);
             return true;
         }
 
@@ -440,10 +493,10 @@
             return false;
         }
 
-        // managament is only supported in downloads.
+        // management is only supported in downloads.
         if (mActivity.getCurrentRoot().isDownloads()) {
             // and only and only on APKs or partial files.
-            return "application/vnd.android.package-archive".equals(doc.mimeType)
+            return MimeTypes.isApkType(doc.mimeType)
                     || doc.isPartial();
         }
 
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 500bcf7..c02fced 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -35,7 +35,6 @@
 import com.android.documentsui.ActionModeController;
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
-import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.MenuManager.DirectoryDetails;
 import com.android.documentsui.MenuManager.SelectionDetails;
@@ -67,17 +66,17 @@
 /**
  * Standalone file management activity.
  */
-public class FilesActivity extends BaseActivity implements ActionHandler.Addons {
+public class FilesActivity
+        extends BaseActivity<ActionHandler<FilesActivity>> implements ActionHandler.Addons {
 
     public static final String TAG = "FilesActivity";
 
     private final Config mConfig = new Config();
     private SelectionManager mSelectionMgr;
     private MenuManager mMenuManager;
-    private ActionHandler<FilesActivity> mActions;
     private DialogController mDialogs;
     private DocumentClipper mClipper;
-    protected ActionModeController mActionModeController;
+    private ActionModeController mActionModeController;
 
     public FilesActivity() {
         super(R.layout.files_activity, TAG);
@@ -111,8 +110,9 @@
                 this,
                 mState,
                 mRoots,
-                DocumentsAccess.create(this),
+                mDocs,
                 mSelectionMgr,
+                mSearchManager,
                 ProviderExecutor::forAuthority,
                 mActionModeController,
                 mDialogs,
@@ -281,16 +281,7 @@
     public void springOpenDirectory(DocumentInfo doc) {
         assert(doc.isContainer());
         assert(!doc.isArchive());
-        openContainerDocument(doc);
-    }
-
-    /**
-     * @deprecated use {@link ActionHandler#showChooserForDoc(DocumentInfo)}
-     * @param doc
-     */
-    @Deprecated
-    public void showChooserForDoc(DocumentInfo doc) {
-        mActions.showChooserForDoc(doc);
+        mActions.openContainerDocument(doc);
     }
 
     @Override
@@ -357,6 +348,7 @@
         return mConfig;
     }
 
+    @Override
     public SelectionManager getSelectionManager(
             DocumentsAdapter adapter, SelectionPredicate canSetState) {
         return mSelectionMgr.reset(adapter, canSetState);
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 50f45d1..03c8a90 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -31,11 +31,12 @@
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.Metrics;
+import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Lookup;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
@@ -66,10 +67,11 @@
             RootsAccess roots,
             DocumentsAccess docs,
             SelectionManager selectionMgr,
+            SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
             ActivityConfig activityConfig) {
 
-        super(activity, state, roots, docs, selectionMgr, executors);
+        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
 
         mConfig = activityConfig;
         mScope = new ContentScope(this::onModelLoaded);
@@ -152,7 +154,7 @@
     private void onModelLoaded(Model.Update update) {
         boolean showDrawer = false;
 
-        if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
+        if (MimeTypes.mimeMatches(MimeTypes.VISUAL_MIMES, mState.acceptMimes)) {
             showDrawer = false;
         }
         if (mState.external && mState.action == ACTION_GET_CONTENT) {
diff --git a/src/com/android/documentsui/picker/Config.java b/src/com/android/documentsui/picker/Config.java
index 2d83ba1..959da9f 100644
--- a/src/com/android/documentsui/picker/Config.java
+++ b/src/com/android/documentsui/picker/Config.java
@@ -24,9 +24,9 @@
 
 import android.provider.DocumentsContract.Document;
 
-import com.android.documentsui.base.MimePredicate;
-import com.android.documentsui.base.State;
 import com.android.documentsui.ActivityConfig;
+import com.android.documentsui.base.MimeTypes;
+import com.android.documentsui.base.State;
 
 /**
  * Provides support for Platform specific specializations of DirectoryFragment.
@@ -39,7 +39,7 @@
             return false;
         }
 
-        if (MimePredicate.isDirectoryType(docMimeType)) {
+        if (MimeTypes.isDirectoryType(docMimeType)) {
             return false;
         }
 
@@ -56,7 +56,7 @@
     @Override
     public boolean isDocumentEnabled(String mimeType, int docFlags, State state) {
         // Directories are always enabled.
-        if (MimePredicate.isDirectoryType(mimeType)) {
+        if (MimeTypes.isDirectoryType(mimeType)) {
             return true;
         }
 
@@ -74,6 +74,6 @@
                 }
         }
 
-        return MimePredicate.mimeMatches(state.acceptMimes, mimeType);
+        return MimeTypes.mimeMatches(state.acceptMimes, mimeType);
     }
 }
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 42d32ae..17e6bb1 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -46,7 +46,6 @@
 import com.android.documentsui.ActionModeController;
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
-import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.MenuManager.DirectoryDetails;
 import com.android.documentsui.MenuManager.SelectionDetails;
@@ -54,7 +53,7 @@
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.EventHandler;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
 import com.android.documentsui.base.PairedTask;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
@@ -73,7 +72,8 @@
 import java.util.Arrays;
 import java.util.List;
 
-public class PickActivity extends BaseActivity implements ActionHandler.Addons {
+public class PickActivity
+        extends BaseActivity<ActionHandler<PickActivity>> implements ActionHandler.Addons {
 
     private static final int CODE_FORWARD = 42;
     private static final String TAG = "PickActivity";
@@ -81,7 +81,6 @@
 
     private SelectionManager mSelectionMgr;
     private MenuManager mMenuManager;
-    private ActionHandler<PickActivity> mActionHandler;
     private ActionModeController mActionModeController;
 
     public PickActivity() {
@@ -97,12 +96,13 @@
                         ? SelectionManager.MODE_MULTIPLE
                         : SelectionManager.MODE_SINGLE);
         mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
-        mActionHandler = new ActionHandler<>(
+        mActions = new ActionHandler<>(
                 this,
                 mState,
-                DocumentsApplication.getRootsCache(this),
-                DocumentsAccess.create(this),
+                mRoots,
+                mDocs,
                 mSelectionMgr,
+                mSearchManager,
                 ProviderExecutor::forAuthority,
                 mConfig);
 
@@ -115,7 +115,7 @@
         Intent intent = getIntent();
 
         setupLayout(intent);
-        mActionHandler.initLocation(intent);
+        mActions.initLocation(intent);
     }
 
     private void setupLayout(Intent intent) {
@@ -262,7 +262,7 @@
             // No directory means recents
             if (mState.action == ACTION_CREATE ||
                 mState.action == ACTION_PICK_COPY_DESTINATION) {
-                mActionHandler.loadRoot(Shared.getDefaultRootUri(this));
+                mActions.loadRoot(Shared.getDefaultRootUri(this));
             } else {
                 DirectoryFragment.showRecentsOpen(fm, anim);
 
@@ -270,8 +270,8 @@
                 // picking GRID for visual types. We intentionally don't
                 // consult a user's saved preferences here since they are
                 // set per root (not per root and per mimetype).
-                boolean visualMimes = MimePredicate.mimeMatches(
-                        MimePredicate.VISUAL_MIMES, mState.acceptMimes);
+                boolean visualMimes = MimeTypes.mimeMatches(
+                        MimeTypes.VISUAL_MIMES, mState.acceptMimes);
                 mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
             }
         } else {
@@ -312,7 +312,7 @@
     @Override
     protected void onDirectoryCreated(DocumentInfo doc) {
         assert(doc.isDirectory());
-        openContainerDocument(doc);
+        mActions.openContainerDocument(doc);
     }
 
     void onSaveRequested(String mimeType, String displayName) {
@@ -324,7 +324,7 @@
     public void onDocumentPicked(DocumentInfo doc) {
         final FragmentManager fm = getFragmentManager();
         if (doc.isContainer()) {
-            openContainerDocument(doc);
+            mActions.openContainerDocument(doc);
         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             // Explicit file picked, return
             new ExistingFinishTask(this, doc.derivedUri)
@@ -436,10 +436,10 @@
 
         // provide our friend, RootsFragment, early access to this special feature!
         if (model == null) {
-            return mActionHandler;
+            return mActions;
         }
 
-        return mActionHandler.reset(model, searchMode);
+        return mActions.reset(model, searchMode);
     }
 
     @Override
diff --git a/src/com/android/documentsui/roots/RootsAccess.java b/src/com/android/documentsui/roots/RootsAccess.java
index 6323840..0d0a975 100644
--- a/src/com/android/documentsui/roots/RootsAccess.java
+++ b/src/com/android/documentsui/roots/RootsAccess.java
@@ -18,9 +18,10 @@
 
 import static com.android.documentsui.base.Shared.DEBUG;
 
+import android.net.Uri;
 import android.util.Log;
 
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 
@@ -33,6 +34,8 @@
  */
 public interface RootsAccess {
 
+    Uri NOTIFICATION_URI = Uri.parse("content://com.android.documentsui.roots/");
+
     /**
      * Return the requested {@link RootInfo}, but only loading the roots for the
      * requested authority. This is useful when we want to load fast without
@@ -104,8 +107,8 @@
             }
 
             final boolean overlap =
-                    MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
-                    MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
+                    MimeTypes.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
+                    MimeTypes.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
             if (!overlap) {
                 if (DEBUG) Log.v(
                         tag, "Excluding root because: unsupported content types > "
diff --git a/src/com/android/documentsui/roots/RootsCache.java b/src/com/android/documentsui/roots/RootsCache.java
index ea08b40..4111a95 100644
--- a/src/com/android/documentsui/roots/RootsCache.java
+++ b/src/com/android/documentsui/roots/RootsCache.java
@@ -44,11 +44,11 @@
 import com.android.documentsui.base.State;
 import com.android.internal.annotations.GuardedBy;
 
+import libcore.io.IoUtils;
+
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
 
-import libcore.io.IoUtils;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -62,9 +62,6 @@
  * Cache of known storage backends and their roots.
  */
 public class RootsCache implements RootsAccess {
-    public static final Uri sNotificationUri = Uri.parse(
-            "content://com.android.documentsui.roots/");
-
     private static final String TAG = "RootsCache";
 
     private final Context mContext;
@@ -393,7 +390,7 @@
                 mStoppedAuthorities = mTaskStoppedAuthorities;
             }
             mFirstLoad.countDown();
-            resolver.notifyChange(sNotificationUri, null, false);
+            resolver.notifyChange(NOTIFICATION_URI, null, false);
             return null;
         }
 
diff --git a/src/com/android/documentsui/roots/RootsLoader.java b/src/com/android/documentsui/roots/RootsLoader.java
index 7e55be4..cae2639 100644
--- a/src/com/android/documentsui/roots/RootsLoader.java
+++ b/src/com/android/documentsui/roots/RootsLoader.java
@@ -38,7 +38,7 @@
         mState = state;
 
         context.getContentResolver().registerContentObserver(
-                RootsCache.sNotificationUri, false, mObserver);
+                RootsAccess.NOTIFICATION_URI, false, mObserver);
     }
 
     @Override
diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java
index a255e52..b6bca11 100644
--- a/src/com/android/documentsui/selection/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -33,6 +33,7 @@
 import android.util.SparseIntArray;
 import android.view.View;
 
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.R;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.dirlist.DocumentsAdapter;
@@ -61,6 +62,7 @@
     private final SelectionEnvironment mEnvironment;
     private final DocumentsAdapter mAdapter;
     private final SelectionManager mSelectionManager;
+    private final DirectoryReloadLock mLock;
     private final Runnable mViewScroller;
     private final GridModel.OnSelectionChangedListener mGridListener;
 
@@ -75,16 +77,19 @@
             final RecyclerView view,
             DocumentsAdapter adapter,
             SelectionManager selectionManager,
+            DirectoryReloadLock lock,
             IntPredicate gridItemTester) {
-        this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, gridItemTester);
+        this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, lock, gridItemTester);
     }
 
     private BandController(
             SelectionEnvironment env,
             DocumentsAdapter adapter,
             SelectionManager selectionManager,
+            DirectoryReloadLock lock,
             IntPredicate gridItemTester) {
 
+        mLock = lock;
         selectionManager.bindContoller(this);
 
         mEnvironment = env;
@@ -265,6 +270,7 @@
     private void startBandSelect(Point origin) {
         if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
 
+        mLock.block();
         mOrigin = origin;
         mModelBuilder.run();  // Creates a new selection model.
         mModel.startSelection(mOrigin);
@@ -314,6 +320,7 @@
 
         mModel = null;
         mOrigin = null;
+        mLock.unblock();
     }
 
     private void onSelectionChanged(Set<String> updatedSelection) {
diff --git a/src/com/android/documentsui/selection/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java
index 08afac5..959080c 100644
--- a/src/com/android/documentsui/selection/GestureSelector.java
+++ b/src/com/android/documentsui/selection/GestureSelector.java
@@ -21,6 +21,7 @@
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.ui.ViewAutoScroller;
 import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
@@ -40,6 +41,7 @@
     private final Runnable mDragScroller;
     private final IntSupplier mHeight;
     private final ViewFinder mViewFinder;
+    private final DirectoryReloadLock mLock;
     private int mLastStartedItemPos = -1;
     private boolean mStarted = false;
     private Point mLastInterceptedPoint;
@@ -48,10 +50,12 @@
             SelectionManager selectionMgr,
             IntSupplier heightSupplier,
             ViewFinder viewFinder,
-            ScrollActionDelegate actionDelegate) {
+            ScrollActionDelegate actionDelegate,
+            DirectoryReloadLock lock) {
         mSelectionMgr = selectionMgr;
         mHeight = heightSupplier;
         mViewFinder = viewFinder;
+        mLock = lock;
 
         ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
             @Override
@@ -75,7 +79,8 @@
 
     public static GestureSelector create(
             SelectionManager selectionMgr,
-            RecyclerView scrollView) {
+            RecyclerView scrollView,
+            DirectoryReloadLock lock) {
         ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
             @Override
             public void scrollBy(int dy) {
@@ -97,7 +102,8 @@
                         selectionMgr,
                         scrollView::getHeight,
                         scrollView::findChildViewUnder,
-                        actionDelegate);
+                        actionDelegate,
+                        lock);
 
         return helper;
     }
@@ -162,6 +168,8 @@
         mLastInterceptedPoint = e.getOrigin();
         if (mStarted) {
             mSelectionMgr.startRangeSelection(mLastStartedItemPos);
+            // Gesture Selection about to start
+            mLock.block();
             return true;
         }
         return false;
@@ -173,6 +181,7 @@
         mLastStartedItemPos = -1;
         mStarted = false;
         mSelectionMgr.getSelection().applyProvisionalSelection();
+        mLock.unblock();
         return false;
     }
 
diff --git a/src/com/android/documentsui/selection/SelectionMetadata.java b/src/com/android/documentsui/selection/SelectionMetadata.java
index e5f64d9..1da5748 100644
--- a/src/com/android/documentsui/selection/SelectionMetadata.java
+++ b/src/com/android/documentsui/selection/SelectionMetadata.java
@@ -24,7 +24,7 @@
 import android.util.Log;
 
 import com.android.documentsui.MenuManager;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
 
 import java.util.function.Function;
 
@@ -63,7 +63,7 @@
         final int delta = selected ? 1 : -1;
 
         final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        if (MimePredicate.isDirectoryType(mimeType)) {
+        if (MimeTypes.isDirectoryType(mimeType)) {
             mDirectoryCount += delta;
         } else {
             mFileCount += delta;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index f6a4db3..ecdc053 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -361,8 +361,8 @@
         mList.requestFocus();
     }
 
-    private BaseActivity getBaseActivity() {
-        return (BaseActivity) getActivity();
+    private BaseActivity<?> getBaseActivity() {
+        return (BaseActivity<?>) getActivity();
     }
 
     @Override
@@ -409,7 +409,7 @@
         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
         final Item item = mAdapter.getItem(adapterMenuInfo.position);
 
-        BaseActivity activity = getBaseActivity();
+        BaseActivity<?> activity = getBaseActivity();
         item.createContextMenu(menu, activity.getMenuInflater(), activity.getMenuManager());
     }
 
diff --git a/tests/common/com/android/documentsui/TestActionModeAddons.java b/tests/common/com/android/documentsui/TestActionModeAddons.java
index 5e7b7d7..02984fa 100644
--- a/tests/common/com/android/documentsui/TestActionModeAddons.java
+++ b/tests/common/com/android/documentsui/TestActionModeAddons.java
@@ -17,12 +17,9 @@
 
 import com.android.documentsui.testing.TestConfirmationCallback;
 
-/**
- *
- */
 public class TestActionModeAddons implements ActionModeAddons {
 
-    public final TestConfirmationCallback finishOnConfimed = new TestConfirmationCallback();
+    public final TestConfirmationCallback finishOnConfirmed = new TestConfirmationCallback();
 
     @Override
     public void finishActionMode() {
@@ -31,6 +28,6 @@
 
     @Override
     public void finishOnConfirmed(int code) {
-        finishOnConfimed.accept(code);
+        finishOnConfirmed.accept(code);
     }
 }
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 9fc4fca..37bd60b 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -48,9 +48,9 @@
     public TestEventListener<Intent> startActivity;
     public TestEventListener<Intent> startService;
     public TestEventListener<RootInfo> rootPicked;
-    public TestEventListener<DocumentInfo> openContainer;
     public TestEventListener<Integer> refreshCurrentRootAndDirectory;
     public TestEventListener<Boolean> setRootsDrawerOpen;
+    public TestEventListener<Uri> notifyDirectoryNavigated;
 
     public static TestActivity create() {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
@@ -66,9 +66,9 @@
        startActivity = new TestEventListener<>();
        startService = new TestEventListener<>();
        rootPicked = new TestEventListener<>();
-       openContainer = new TestEventListener<>();
        refreshCurrentRootAndDirectory =  new TestEventListener<>();
        setRootsDrawerOpen = new TestEventListener<>();
+       notifyDirectoryNavigated = new TestEventListener<>();
    }
 
     @Override
@@ -121,8 +121,8 @@
     }
 
     @Override
-    public final void openContainerDocument(DocumentInfo doc) {
-        openContainer.accept(doc);
+    public final void notifyDirectoryNavigated(Uri uri) {
+        notifyDirectoryNavigated.accept(uri);
     }
 
     @Override
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java
index 82849a3..abe4d5f 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/dirlist/TestModel.java
@@ -22,13 +22,10 @@
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.EventListener;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 import libcore.net.MimeUtils;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Random;
 
 public class TestModel extends Model {
@@ -46,7 +43,6 @@
     private int mLastId = 0;
     private Random mRand = new Random();
     private MatrixCursor mCursor;
-    private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
 
     public TestModel(String authority) {
         super();
diff --git a/tests/common/com/android/documentsui/testing/IntentAsserts.java b/tests/common/com/android/documentsui/testing/IntentAsserts.java
new file mode 100644
index 0000000..6fc1ae0
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/IntentAsserts.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.documentsui.testing;
+
+import static android.content.Intent.EXTRA_INTENT;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Parcelable;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handy-dandy Junit asserts covering Intents.
+ */
+public final class IntentAsserts {
+
+    private IntentAsserts() {}
+
+    public static void assertHasAction(Intent intent, String expected) {
+        assertEquals(expected, intent.getAction());
+    }
+
+    public static Intent assertHasExtraIntent(Intent intent) {
+        Intent extra = (Intent) intent.getExtra(EXTRA_INTENT);
+        assertNotNull(extra);
+        return extra;
+    }
+
+    public static Uri assertHasExtraUri(Intent intent, String key) {
+        Object value = intent.getExtra(key);
+        assertNotNull(value);
+        assertTrue(value instanceof Uri);
+        return (Uri) value;
+    }
+
+    public static List<Parcelable> assertHasExtraList(Intent intent, String key) {
+        ArrayList<Parcelable> list = intent.getParcelableArrayListExtra(key);
+        assertNotNull(list);
+        return list;
+    }
+
+    public static List<Parcelable> assertHasExtraList(Intent intent, String key, int size) {
+        List<Parcelable> list = assertHasExtraList(intent, key);
+        Assert.assertEquals(size, list.size());
+        return list;
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 381b738..fd199ce 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -40,6 +40,7 @@
                 env.roots,
                 env.docs,
                 env.selectionMgr,
+                env.searchViewManager,
                 (String authority) -> null);
     }
 
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 2f8587b..566f3eb 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -19,6 +19,7 @@
 import android.os.Looper;
 import android.provider.DocumentsContract.Document;
 
+import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.TestModel;
@@ -26,6 +27,8 @@
 
 import junit.framework.Assert;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
@@ -50,11 +53,13 @@
     public final TestDocumentsAccess docs = new TestDocumentsAccess();
     public final TestModel model;
     public final SelectionManager selectionMgr;
+    public final SearchViewManager searchViewManager;
 
     private TestEnv(String authority) {
         mExecutor = new TestScheduledExecutorService();
         model = new TestModel(authority);
         selectionMgr = SelectionManagers.createTestInstance();
+        searchViewManager = new TestSearchViewManager();
     }
 
     public static TestEnv create() {
@@ -127,4 +132,10 @@
     public Executor lookupExecutor(String authority) {
         return mExecutor;
     }
+
+    public void selectDocument(DocumentInfo info) {
+        List<String> ids = new ArrayList<>(1);
+        ids.add(info.documentId);
+        selectionMgr.setItemsSelected(ids, true);
+    }
 }
diff --git a/tests/common/com/android/documentsui/testing/TestResources.java b/tests/common/com/android/documentsui/testing/TestResources.java
index 206d46b..a014af3 100644
--- a/tests/common/com/android/documentsui/testing/TestResources.java
+++ b/tests/common/com/android/documentsui/testing/TestResources.java
@@ -82,4 +82,8 @@
             @StringRes int id, Object... formatArgs) throws NotFoundException {
         return getString(id);
     }
+
+    public final CharSequence getText(@StringRes int resId) {
+        return getString(resId);
+    }
 }
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 254fdbf..1577ae2 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -56,6 +56,7 @@
                 mEnv.roots,
                 mEnv.docs,
                 mEnv.selectionMgr,
+                mEnv.searchViewManager,
                 mEnv::lookupExecutor) {
 
             @Override
diff --git a/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
new file mode 100644
index 0000000..dbbc9c6
--- /dev/null
+++ b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DirectoryReloadLockTest {
+
+    private DirectoryReloadLock lock = new DirectoryReloadLock();
+    private boolean called;
+    private Runnable callback = () -> {
+        called = true;
+    };
+
+    @Before
+    public void setUp() {
+        called = false;
+    }
+
+    @Test
+    public void testNotBlocking_callbackNotBlocked() {
+        lock.tryUpdate(callback);
+        assertTrue(called);
+    }
+
+    @Test
+    public void testToggleBlocking_callbackNotBlocked() {
+        lock.block();
+        lock.unblock();
+        lock.tryUpdate(callback);
+        assertTrue(called);
+    }
+
+    @Test
+    public void testBlocking_callbackBlocked() {
+        lock.block();
+        lock.tryUpdate(callback);
+        assertFalse(called);
+    }
+
+    @Test
+    public void testBlockthenUnblock_callbackNotBlocked() {
+        lock.block();
+        lock.tryUpdate(callback);
+        lock.unblock();
+        assertTrue(called);
+    }
+}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 2620f14..7de09b1 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -16,6 +16,10 @@
 
 package com.android.documentsui.files;
 
+import static com.android.documentsui.testing.IntentAsserts.assertHasAction;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraIntent;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraList;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraUri;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -68,6 +72,7 @@
                 mEnv.roots,
                 mEnv.docs,
                 mEnv.selectionMgr,
+                mEnv.searchViewManager,
                 mEnv::lookupExecutor,
                 mActionModeAddons,
                 mDialogs,
@@ -78,7 +83,7 @@
 
         mDialogs.confirmNext();
 
-        mEnv.selectionMgr.toggleSelection("1");
+        mEnv.selectDocument(TestEnv.FILE_GIF);
 
         mHandler.reset(mEnv.model, false);
     }
@@ -107,24 +112,76 @@
     }
 
     @Test
-    public void testDeleteDocuments() {
+    public void testDeleteSelectedDocuments() {
         mEnv.populateStack();
 
         mHandler.deleteSelectedDocuments();
         mDialogs.assertNoFileFailures();
         mActivity.startService.assertCalled();
-        mActionModeAddons.finishOnConfimed.assertConfirmed();
+        mActionModeAddons.finishOnConfirmed.assertConfirmed();
     }
 
     @Test
-    public void testDeleteDocuments_Cancelable() {
+    public void testDeleteSelectedDocuments_Cancelable() {
         mEnv.populateStack();
 
         mDialogs.rejectNext();
         mHandler.deleteSelectedDocuments();
         mDialogs.assertNoFileFailures();
         mActivity.startService.assertNotCalled();
-        mActionModeAddons.finishOnConfimed.assertRejected();
+        mActionModeAddons.finishOnConfirmed.assertRejected();
+    }
+
+    @Test
+    public void testShareSelectedDocuments_ShowsChooser() {
+        mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+        mHandler.shareSelectedDocuments();
+
+        mActivity.assertActivityStarted(Intent.ACTION_CHOOSER);
+    }
+
+    @Test
+    public void testShareSelectedDocuments_Single() {
+        mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+        mHandler.shareSelectedDocuments();
+
+        Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+        assertHasAction(intent, Intent.ACTION_SEND);
+        assertHasExtraUri(intent, Intent.EXTRA_STREAM);
+    }
+
+    @Test
+    public void testShareSelectedDocuments_Multiple() {
+        mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+        mEnv.selectDocument(TestEnv.FILE_PDF);
+        mHandler.shareSelectedDocuments();
+
+        Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+        assertHasAction(intent, Intent.ACTION_SEND_MULTIPLE);
+        assertHasExtraList(intent, Intent.EXTRA_STREAM, 2);
+    }
+
+    @Test
+    public void testShareSelectedDocuments_OmitsVirtualFiles() {
+        mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+        mEnv.selectDocument(TestEnv.FILE_VIRTUAL);
+        mHandler.shareSelectedDocuments();
+
+        Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+        assertHasAction(intent, Intent.ACTION_SEND);
+        assertHasExtraUri(intent, Intent.EXTRA_STREAM);
+    }
+
+    @Test
+    public void testShareSelectedDocuments_OmitsPartialFiles() {
+        mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+        mEnv.selectDocument(TestEnv.FILE_PARTIAL);
+        mEnv.selectDocument(TestEnv.FILE_PNG);
+        mHandler.shareSelectedDocuments();
+
+        Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+        assertHasAction(intent, Intent.ACTION_SEND_MULTIPLE);
+        assertHasExtraList(intent, Intent.EXTRA_STREAM, 2);
     }
 
     @Test
@@ -165,7 +222,7 @@
         mActivity.currentRoot = TestRootsAccess.HOME;
 
         mHandler.onDocumentPicked(TestEnv.FILE_ARCHIVE);
-        mActivity.openContainer.assertLastArgument(TestEnv.FILE_ARCHIVE);
+        assertEquals(TestEnv.FILE_ARCHIVE, mEnv.state.stack.peek());
     }
 
     @Test
@@ -173,7 +230,7 @@
         mActivity.currentRoot = TestRootsAccess.HOME;
 
         mHandler.onDocumentPicked(TestEnv.FOLDER_1);
-        mActivity.openContainer.assertLastArgument(TestEnv.FOLDER_1);
+        assertEquals(TestEnv.FOLDER_1, mEnv.state.stack.peek());
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 35199c3..8311443 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -59,6 +60,7 @@
                 mEnv.roots,
                 mEnv.docs,
                 mEnv.selectionMgr,
+                mEnv.searchViewManager,
                 mEnv::lookupExecutor,
                 null  // tuner, not currently used.
                 );
@@ -119,6 +121,15 @@
         assertRootPicked(TestRootsAccess.HOME.getUri());
     }
 
+    @Test
+    public void testOpenContainerDocument() {
+        mHandler.openContainerDocument(TestEnv.FOLDER_0);
+
+        assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.peek());
+
+        mActivity.refreshCurrentRootAndDirectory.assertCalled();
+    }
+
     private void assertRootPicked(Uri expectedUri) throws Exception {
         mEnv.beforeAsserts();