Lifetime of FragmentTuners scoped to activity.

Add support for resetting with new state from directory fragment.
Update Model to use an EventListener.
Eliminate documentPick handling from base activity and directory fragment...
    Plum document picking directly from UserInputHandler to FragmentTuner instances.
Add new EventHandler internface (returns void, more semantic meaning that Consumer<T>).
Replace ModelUpdateListener interface with EventHandler.
Make DocumentAdapters return EventHandler<Model.Update> instead of
    implementing ModelUpdateListener.
Move Activity specific FragmentTuner impls along side respective activities.

Change-Id: Ia6a5ab00ede685f7418773ed865d8c51e4125330
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 0e62464..d41c521 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -67,6 +67,7 @@
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.FragmentTuner;
 import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.dirlist.MultiSelectManager;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.roots.LoadRootTask;
 import com.android.documentsui.roots.RootsCache;
@@ -138,12 +139,27 @@
     private boolean mNavDrawerHasFocus;
     private long mStartTime;
 
+    /**
+     * Provides Activity a means of injection into and specialization of
+     * DirectoryFragment.
+     */
+    public abstract FragmentTuner getFragmentTuner(
+            Model model, MultiSelectManager selectionMgr, boolean mSearchMode);
+
+    /**
+     * Provides Activity a means of injection into and specialization of
+     * DirectoryFragment hosted menus.
+     */
+    public abstract MenuManager getMenuManager();
+
+    /**
+     * Provides Activity a means of injection into and specialization of
+     * DirectoryFragment.
+     */
+    public abstract DirectoryDetails getDirectoryDetails();
 
     public abstract void onDocumentPicked(DocumentInfo doc, Model model);
     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
-    public abstract FragmentTuner createFragmentTuner();
-    public abstract MenuManager getMenuManager();
-    public abstract DirectoryDetails getDirectoryDetails();
 
     protected abstract void onTaskFinished(Uri... uris);
     protected abstract void refreshDirectory(int anim);
diff --git a/src/com/android/documentsui/base/EventHandler.java b/src/com/android/documentsui/base/EventHandler.java
index 0e289c9..c0667b6 100644
--- a/src/com/android/documentsui/base/EventHandler.java
+++ b/src/com/android/documentsui/base/EventHandler.java
@@ -22,5 +22,5 @@
  */
 @FunctionalInterface
 public interface EventHandler<T> {
-    boolean apply(T event);
+    boolean accept(T event);
 }
\ No newline at end of file
diff --git a/src/com/android/documentsui/base/EventListener.java b/src/com/android/documentsui/base/EventListener.java
new file mode 100644
index 0000000..8f7cd2b
--- /dev/null
+++ b/src/com/android/documentsui/base/EventListener.java
@@ -0,0 +1,25 @@
+/*
+ * 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.base;
+
+/**
+ * A functional interface that listens to an event without returning any information.
+ */
+@FunctionalInterface
+public interface EventListener<T> {
+    void accept(T event);
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/ActionModeController.java b/src/com/android/documentsui/dirlist/ActionModeController.java
index 70c5a64..4756e3c 100644
--- a/src/com/android/documentsui/dirlist/ActionModeController.java
+++ b/src/com/android/documentsui/dirlist/ActionModeController.java
@@ -195,7 +195,7 @@
 
     @Override
     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-        return mMenuItemClicker.apply(item);
+        return mMenuItemClicker.accept(item);
     }
 
     static ActionModeController create(
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index a66a3db..bcd1f30 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -77,6 +77,7 @@
 import com.android.documentsui.Snackbars;
 import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.base.Events.MotionInputEvent;
 import com.android.documentsui.base.RootInfo;
@@ -134,7 +135,7 @@
     private static final int REFRESH_SPINNER_DISMISS_DELAY = 500;
 
     private final Model mModel = new Model();
-    private final Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
+    private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
     private MultiSelectManager mSelectionMgr;
     private ActionModeController mActionModeController;
     private SelectionMetadata mSelectionMetadata;
@@ -298,7 +299,7 @@
         mSelectionMetadata = new SelectionMetadata(mModel::getItem);
         mSelectionMgr.addItemCallback(mSelectionMetadata);
 
-        mModel.addUpdateListener(mAdapter);
+        mModel.addUpdateListener(mAdapter.getModelUpdateListener());
         mModel.addUpdateListener(mModelUpdateListener);
 
         // Make sure this is done after the RecyclerView and the Model are set up.
@@ -310,8 +311,9 @@
                 mSelectionMgr,
                 mRecView);
 
-        mTuner = getBaseActivity().createFragmentTuner();
-        mMenuManager = getBaseActivity().getMenuManager();
+        final BaseActivity activity = getBaseActivity();
+        mTuner = activity.getFragmentTuner(mModel, mSelectionMgr, mSearchMode);
+        mMenuManager = activity.getMenuManager();
 
         if (state.allowMultiple) {
             mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
@@ -336,7 +338,11 @@
                 (MotionEvent t) -> MotionInputEvent.obtain(t, mRecView),
                 this::canSelect,
                 this::onRightClick,
-                (DocumentDetails doc) -> handleViewItem(doc.getModelId()), // activate handler
+                // TODO: consider injecting the tuner directly into the handler for
+                // less middle-man action.
+                (DocumentDetails details) -> mTuner.onDocumentPicked(details.getModelId()),
+                // TODO: replace this with a previewHandler
+                (DocumentDetails details) -> mTuner.onDocumentPicked(details.getModelId()),
                 (DocumentDetails ignored) -> onDeleteSelectedDocuments(), // delete handler
                 mDragStartListener::onTouchDragEvent,
                 gestureSel::start);
@@ -350,8 +356,6 @@
                 mInputHandler,
                 mBandController);
 
-        final BaseActivity activity = getBaseActivity();
-        mTuner = activity.createFragmentTuner();
         mMenuManager = activity.getMenuManager();
 
         mActionModeController = ActionModeController.create(
@@ -441,6 +445,9 @@
 
         final String modelId = getModelId(v);
         if (modelId == null) {
+            // TODO: inject DirectoryDetails into MenuManager constructor
+            // Since both classes are supplied by Activity and created
+            // at the same time.
             mMenuManager.inflateContextMenuForContainer(
                     menu, inflater, getBaseActivity().getDirectoryDetails());
         } else {
@@ -493,25 +500,6 @@
         return true;
     }
 
-    private boolean handleViewItem(String id) {
-        final Cursor cursor = mModel.getItem(id);
-
-        if (cursor == null) {
-            Log.w(TAG, "Can't view item. Can't obtain cursor for modeId" + id);
-            return false;
-        }
-
-        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-        if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
-            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
-            getBaseActivity().onDocumentPicked(doc, mModel);
-            mSelectionMgr.clearSelection();
-            return true;
-        }
-        return false;
-    }
-
     public void onViewModeChanged() {
         // Mode change is just visual change; no need to kick loader.
         updateDisplayState();
@@ -1254,20 +1242,26 @@
         return mSelectionMgr.getSelection().contains(modelId);
     }
 
-    private final class ModelUpdateListener implements Model.UpdateListener {
-        @Override
-        public void onModelUpdate(Model model) {
-            if (DEBUG) Log.d(TAG, "Received model update. Loading=" + model.isLoading());
+    private final class ModelUpdateListener implements EventListener<Model.Update> {
 
-            if (model.info != null || model.error != null) {
-                mMessageBar.setInfo(model.info);
-                mMessageBar.setError(model.error);
+        @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(model.isLoading() ? View.VISIBLE : View.GONE);
+            mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
 
-            if (model.isEmpty()) {
+            if (mModel.isEmpty()) {
                 if (mSearchMode) {
                     showNoResults(getDisplayState().stack.root);
                 } else {
@@ -1278,16 +1272,11 @@
                 mAdapter.notifyDataSetChanged();
             }
 
-            if (!model.isLoading()) {
+            if (!mModel.isLoading()) {
                 getBaseActivity().notifyDirectoryLoaded(
-                    model.doc != null ? model.doc.derivedUri : null);
+                        mModel.doc != null ? mModel.doc.derivedUri : null);
             }
         }
-
-        @Override
-        public void onModelUpdateFailed(Exception e) {
-            showQueryError();
-        }
     }
 
     private boolean canSelect(DocumentDetails doc) {
@@ -1486,8 +1475,6 @@
         mLastSortDimension = curSortedDimension;
         mLastSortDirection = curSortedDimension.getSortDirection();
 
-        mTuner.onModelLoaded(mModel, mType, mSearchMode);
-
         if (mRefreshLayout.isRefreshing()) {
             new Handler().postDelayed(
                     () -> mRefreshLayout.setRefreshing(false),
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index 00f1f11..b5e5282 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -24,6 +24,7 @@
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.State;
 
 import java.util.List;
@@ -38,8 +39,7 @@
  * @see SectionBreakDocumentsAdapterWrapper
  */
 abstract class DocumentsAdapter
-        extends RecyclerView.Adapter<DocumentHolder>
-        implements Model.UpdateListener {
+        extends RecyclerView.Adapter<DocumentHolder> {
 
     // Payloads for notifyItemChange to distinguish between selection and other events.
     static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
@@ -63,6 +63,8 @@
      */
     abstract String getModelId(int position);
 
+    abstract EventListener<Model.Update> getModelUpdateListener();
+
     /**
      * Returns a class that yields the span size for a particular element. This is
      * primarily useful in {@link SectionBreakDocumentsAdapterWrapper} where
diff --git a/src/com/android/documentsui/dirlist/FocusManager.java b/src/com/android/documentsui/dirlist/FocusManager.java
index 05ce0d1..07c992d 100644
--- a/src/com/android/documentsui/dirlist/FocusManager.java
+++ b/src/com/android/documentsui/dirlist/FocusManager.java
@@ -38,7 +38,9 @@
 import android.view.View;
 import android.widget.TextView;
 
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Events;
+import com.android.documentsui.dirlist.Model.Update;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -516,15 +518,9 @@
             mIndex = index;
         }
 
-        private Model.UpdateListener mModelListener = new Model.UpdateListener() {
+        private EventListener<Model.Update> mModelListener = new EventListener<Model.Update>() {
             @Override
-            public void onModelUpdate(Model model) {
-                // Invalidate the search index when the model updates.
-                mIndex = null;
-            }
-
-            @Override
-            public void onModelUpdateFailed(Exception e) {
+            public void accept(Update event) {
                 // Invalidate the search index when the model updates.
                 mIndex = null;
             }
diff --git a/src/com/android/documentsui/dirlist/FragmentTuner.java b/src/com/android/documentsui/dirlist/FragmentTuner.java
index 6f82db9..b9ea77a 100644
--- a/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -16,23 +16,8 @@
 
 package com.android.documentsui.dirlist;
 
-import static com.android.documentsui.base.State.ACTION_CREATE;
-import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
-import static com.android.documentsui.base.State.ACTION_OPEN;
-import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
-import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
-
-import android.content.Context;
-import android.provider.DocumentsContract.Document;
-
-import com.android.documentsui.BaseActivity;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
-import com.android.documentsui.base.MimePredicate;
-import com.android.documentsui.base.State;
-import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
-import com.android.documentsui.manager.ManageActivity;
-import com.android.documentsui.sorting.SortController;
 
 /**
  * Providers support for specializing the DirectoryFragment to the "host" Activity.
@@ -40,20 +25,6 @@
  */
 public abstract class FragmentTuner {
 
-    final Context mContext;
-    final State mState;
-    final SortController mSortController;
-
-    public FragmentTuner(Context context, State state, SortController sortController) {
-        assert(context != null);
-        assert(state != null);
-        assert(sortController != null);
-
-        mContext = context;
-        mState = state;
-        mSortController = sortController;
-    }
-
     // Subtly different from isDocumentEnabled. The reason may be illuminated as follows.
     // A folder is enabled such that it may be double clicked, even in settings
     // when the folder itself cannot be selected. This may also be true of container types.
@@ -69,161 +40,24 @@
      * When managed mode is enabled, active downloads will be visible in the UI.
      * Presumably this should only be true when in the downloads directory.
      */
-    boolean managedModeEnabled() {
+    protected boolean managedModeEnabled() {
         return false;
     }
 
     /**
      * Whether drag n' drop is allowed in this context
      */
-    boolean dragAndDropEnabled() {
+    protected boolean dragAndDropEnabled() {
         return false;
     }
 
-    abstract void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch);
-
-    void showChooserForDoc(DocumentInfo doc) {
+    protected void showChooserForDoc(DocumentInfo doc) {
         throw new UnsupportedOperationException("Show chooser not supported!");
     }
 
-    void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
+    protected void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
         throw new UnsupportedOperationException("Open in new window not supported!");
     }
 
-    /**
-     * Provides support for Platform specific specializations of DirectoryFragment.
-     */
-    public static final class DocumentsTuner extends FragmentTuner {
-
-        // We use this to keep track of whether a model has been previously loaded or not so we can
-        // open the drawer on empty directories on first launch
-        private boolean mModelPreviousLoaded;
-
-        public DocumentsTuner(Context context, State state, SortController sortController) {
-            super(context, state, sortController);
-        }
-
-        @Override
-        public boolean canSelectType(String docMimeType, int docFlags) {
-            if (!isDocumentEnabled(docMimeType, docFlags)) {
-                return false;
-            }
-
-            if (MimePredicate.isDirectoryType(docMimeType)) {
-                return false;
-            }
-
-            if (mState.action == ACTION_OPEN_TREE
-                    || mState.action == ACTION_PICK_COPY_DESTINATION) {
-                // In this case nothing *ever* is selectable...the expected user behavior is
-                // they navigate *into* a folder, then click a confirmation button indicating
-                // that the current directory is the directory they are picking.
-                return false;
-            }
-
-            return true;
-        }
-
-        @Override
-        public boolean isDocumentEnabled(String mimeType, int docFlags) {
-            // Directories are always enabled.
-            if (MimePredicate.isDirectoryType(mimeType)) {
-                return true;
-            }
-
-            switch (mState.action) {
-                case ACTION_CREATE:
-                    // Read-only files are disabled when creating.
-                    if ((docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
-                        return false;
-                    }
-                case ACTION_OPEN:
-                case ACTION_GET_CONTENT:
-                    final boolean isVirtual = (docFlags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
-                    if (isVirtual && mState.openableOnly) {
-                        return false;
-                    }
-            }
-
-            return MimePredicate.mimeMatches(mState.acceptMimes, mimeType);
-        }
-
-        @Override
-        void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
-            boolean showDrawer = false;
-
-            if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
-                showDrawer = false;
-            }
-            if (mState.external && mState.action == ACTION_GET_CONTENT) {
-                showDrawer = true;
-            }
-            if (mState.action == ACTION_PICK_COPY_DESTINATION) {
-                showDrawer = true;
-            }
-
-            // When launched into empty root, open drawer.
-            if (model.isEmpty()) {
-                showDrawer = true;
-            }
-
-            if (showDrawer && !mState.hasInitialLocationChanged() && !isSearch
-                    && !mModelPreviousLoaded) {
-                // This noops on layouts without drawer, so no need to guard.
-                ((BaseActivity) mContext).setRootsDrawerOpen(true);
-            }
-            mModelPreviousLoaded = true;
-        }
-    }
-
-    /**
-     * Provides support for Files activity specific specializations of DirectoryFragment.
-     */
-    public static final class FilesTuner extends FragmentTuner {
-
-        // We use this to keep track of whether a model has been previously loaded or not so we can
-        // open the drawer on empty directories on first launch
-        private boolean mModelPreviousLoaded;
-
-        public FilesTuner(Context context, State state, SortController sortController) {
-            super(context, state, sortController);
-        }
-
-        @Override
-        void onModelLoaded(Model model, @ResultType int resultType, boolean isSearch) {
-            // When launched into empty root, open drawer.
-            if (model.isEmpty() && !mState.hasInitialLocationChanged() && !isSearch
-                    && !mModelPreviousLoaded) {
-                // This noops on layouts without drawer, so no need to guard.
-                ((BaseActivity) mContext).setRootsDrawerOpen(true);
-            }
-            mModelPreviousLoaded = true;
-        }
-
-        @Override
-        public boolean managedModeEnabled() {
-            // When in downloads top level directory, we also show active downloads.
-            // And while we don't allow folders in Downloads, we do allow Zip files in
-            // downloads that themselves can be opened and viewed like directories.
-            // This method helps us understand when to kick in on those special behaviors.
-            return mState.stack.root != null
-                    && mState.stack.root.isDownloads()
-                    && mState.stack.size() == 1;
-        }
-
-        @Override
-        public boolean dragAndDropEnabled() {
-            return true;
-        }
-
-        @Override
-        public void showChooserForDoc(DocumentInfo doc) {
-            ((ManageActivity) mContext).showChooserForDoc(doc);
-        }
-
-        @Override
-        public void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
-            ((ManageActivity) mContext).openInNewWindow(stack, doc);
-        }
-    }
+    protected abstract boolean onDocumentPicked(String id);
 }
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 06a2e21..a810244 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -97,7 +97,7 @@
     private class MouseDelegate {
         boolean onInterceptTouchEvent(InputEvent e) {
             if (Events.isMouseDragEvent(e)) {
-                return mMouseDragListener.apply(e);
+                return mMouseDragListener.accept(e);
             } else if (mBandController != null &&
                     (mBandController.shouldStart(e) || mBandController.shouldStop(e))) {
                 return mBandController.onInterceptTouchEvent(e);
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 504fdd0..abf5d40 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -20,6 +20,7 @@
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 import static com.android.documentsui.base.Shared.DEBUG;
 
+import android.annotation.IntDef;
 import android.database.Cursor;
 import android.database.MergeCursor;
 import android.net.Uri;
@@ -32,12 +33,15 @@
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortModel;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -51,7 +55,7 @@
     private static final String TAG = "Model";
 
     private boolean mIsLoading;
-    private List<UpdateListener> mUpdateListeners = new ArrayList<>();
+    private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
     @Nullable private Cursor mCursor;
     private int mCursorCount;
     /** Maps Model ID to cursor positions, for looking up items by Model ID. */
@@ -67,15 +71,24 @@
     @Nullable String error;
     @Nullable DocumentInfo doc;
 
+    public void addUpdateListener(EventListener<Update> listener) {
+        mUpdateListeners.add(listener);
+    }
+
+    public void removeUpdateListener(EventListener<Update> listener) {
+        mUpdateListeners.remove(listener);
+    }
+
     private void notifyUpdateListeners() {
-        for (UpdateListener listener: mUpdateListeners) {
-            listener.onModelUpdate(this);
+        for (EventListener<Update> handler: mUpdateListeners) {
+            handler.accept(Update.UPDATE);
         }
     }
 
     private void notifyUpdateListeners(Exception e) {
-        for (UpdateListener listener: mUpdateListeners) {
-            listener.onModelUpdateFailed(e);
+        Update error = new Update(e);
+        for (EventListener<Update> handler: mUpdateListeners) {
+            handler.accept(error);
         }
     }
 
@@ -420,7 +433,7 @@
         return mCursor;
     }
 
-    boolean isEmpty() {
+    public boolean isEmpty() {
         return mCursorCount == 0;
     }
 
@@ -434,16 +447,23 @@
         final List<DocumentInfo> docs =  new ArrayList<>(size);
         // NOTE: That as this now iterates over only final (non-provisional) selection.
         for (String modelId: selection) {
-            final Cursor cursor = getItem(modelId);
-            if (cursor == null) {
-                Log.w(TAG, "Skipping document. Unabled to obtain cursor for modelId: " + modelId);
+            DocumentInfo doc = getDocument(modelId);
+            if (doc == null) {
+                Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
                 continue;
             }
-            docs.add(DocumentInfo.fromDirectoryCursor(cursor));
+            docs.add(doc);
         }
         return docs;
     }
 
+    public @Nullable DocumentInfo getDocument(String modelId) {
+        final Cursor cursor = getItem(modelId);
+        return (cursor == null)
+                ? null
+                : DocumentInfo.fromDirectoryCursor(cursor);
+    }
+
     public Uri getItemUri(String modelId) {
         final Cursor cursor = getItem(modelId);
         return DocumentInfo.getUri(cursor);
@@ -457,23 +477,43 @@
         return mIds;
     }
 
-    void addUpdateListener(UpdateListener listener) {
-        mUpdateListeners.add(listener);
-    }
+    public static class Update {
 
-    void removeUpdateListener(UpdateListener listener) {
-        mUpdateListeners.remove(listener);
-    }
+        public static final Update UPDATE = new Update();
 
-    interface UpdateListener {
-        /**
-         * Called when a successful update has occurred.
-         */
-        void onModelUpdate(Model model);
+        @IntDef(value = {
+                TYPE_UPDATE,
+                TYPE_UPDATE_ERROR
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface UpdateType {}
+        public static final int TYPE_UPDATE = 0;
+        public static final int TYPE_UPDATE_ERROR = 1;
 
-        /**
-         * Called when an update has been attempted but failed.
-         */
-        void onModelUpdateFailed(Exception e);
+        private final @UpdateType int mType;
+        private final @Nullable Exception mException;
+
+        private Update() {
+            mType = TYPE_UPDATE;
+            mException = null;
+        }
+
+        public Update(Exception exception) {
+            assert(exception != null);
+            mType = TYPE_UPDATE_ERROR;
+            mException = exception;
+        }
+
+        public boolean isUpdate() {
+            return mType == TYPE_UPDATE;
+        }
+
+        public boolean hasError() {
+            return mType == TYPE_UPDATE_ERROR;
+        }
+
+        public @Nullable Exception getError() {
+            return mException;
+        }
     }
 }
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index 8b11a62..c1077c5 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -27,7 +27,9 @@
 import android.util.Log;
 import android.view.ViewGroup;
 
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.Model.Update;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -53,6 +55,7 @@
      * the UI, and where.
      */
     private List<String> mModelIds = new ArrayList<>();
+    private EventListener<Model.Update> mModelUpdateListener;
 
     // List of files that have been deleted. Some transient directory updates
     // may happen while files are being deleted. During this time we don't
@@ -66,6 +69,22 @@
     public ModelBackedDocumentsAdapter(Environment env, IconHelper iconHelper) {
         mEnv = env;
         mIconHelper = iconHelper;
+
+        mModelUpdateListener = new EventListener<Model.Update>() {
+            @Override
+            public void accept(Update event) {
+                if (event.hasError()) {
+                    onModelUpdateFailed(event.getError());
+                } else {
+                    onModelUpdate(mEnv.getModel());
+                }
+            }
+        };
+    }
+
+    @Override
+    EventListener<Update> getModelUpdateListener() {
+        return mModelUpdateListener;
     }
 
     @Override
@@ -131,8 +150,7 @@
         return mModelIds.size();
     }
 
-    @Override
-    public void onModelUpdate(Model model) {
+    private void onModelUpdate(Model model) {
         if (DEBUG && mHiddenIds.size() > 0) {
             Log.d(TAG, "Updating model with hidden ids: " + mHiddenIds);
         }
@@ -152,8 +170,7 @@
         mHiddenIds.retainAll(mModelIds);
     }
 
-    @Override
-    public void onModelUpdateFailed(Exception e) {
+    private void onModelUpdateFailed(Exception e) {
         Log.w(TAG, "Model update failed.", e);
         mModelIds.clear();
     }
diff --git a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java b/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
index 4aef832..15dfe05 100644
--- a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
+++ b/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
@@ -24,7 +24,9 @@
 import android.widget.Space;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.Model.Update;
 
 import java.util.List;
 
@@ -39,6 +41,7 @@
 
     private final Environment mEnv;
     private final DocumentsAdapter mDelegate;
+    private final EventListener<Update> mModelUpdateListener;
 
     private int mBreakPosition = -1;
 
@@ -49,6 +52,26 @@
         // Relay events published by our delegate to our listeners (presumably RecyclerView)
         // with adjusted positions.
         mDelegate.registerAdapterDataObserver(new EventRelay());
+
+        mModelUpdateListener = new EventListener<Model.Update>() {
+            @Override
+            public void accept(Update event) {
+                // make sure the delegate handles the update before we do.
+                // This isn't ideal since the delegate might be listening
+                // the updates itself. But this is the safe thing to do
+                // since we read model ids from the delegate
+                // in our update handler.
+                mDelegate.getModelUpdateListener().accept(event);
+                if (!event.hasError()) {
+                    onModelUpdate(mEnv.getModel());
+                }
+            }
+        };
+    }
+
+    @Override
+    EventListener<Update> getModelUpdateListener() {
+        return mModelUpdateListener;
     }
 
     @Override
@@ -101,9 +124,7 @@
                 : mDelegate.getItemCount() + 1;
     }
 
-    @Override
-    public void onModelUpdate(Model model) {
-        mDelegate.onModelUpdate(model);
+    private void onModelUpdate(Model model) {
         mBreakPosition = -1;
 
         // Walk down the list of IDs till we encounter something that's not a directory, and
@@ -125,11 +146,6 @@
     }
 
     @Override
-    public void onModelUpdateFailed(Exception e) {
-        mDelegate.onModelUpdateFailed(e);
-    }
-
-    @Override
     public int getItemViewType(int p) {
         if (p == mBreakPosition) {
             return ITEM_TYPE_SECTION_BREAK;
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index dcc76e2..9db6599 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -48,10 +48,12 @@
     private final Function<MotionEvent, T> mEventConverter;
     private final Predicate<DocumentDetails> mSelectable;
     private final EventHandler<InputEvent> mRightClickHandler;
-    private final DocumentHandler mActivateHandler;
-    private final DocumentHandler mDeleteHandler;
+    private final EventHandler<DocumentDetails> mPickHandler;
+    private final EventHandler<DocumentDetails> mPreviewHandler;
+    private final EventHandler<DocumentDetails> mDeleteHandler;
     private final EventHandler<InputEvent> mTouchDragListener;
     private final EventHandler<InputEvent> mGestureSelectHandler;
+
     private final TouchInputDelegate mTouchDelegate;
     private final MouseInputDelegate mMouseDelegate;
     private final KeyInputHandler mKeyListener;
@@ -62,8 +64,9 @@
             Function<MotionEvent, T> eventConverter,
             Predicate<DocumentDetails> selectable,
             EventHandler<InputEvent> rightClickHandler,
-            DocumentHandler activateHandler,
-            DocumentHandler deleteHandler,
+            EventHandler<DocumentDetails> pickHandler,
+            EventHandler<DocumentDetails> previewHandler,
+            EventHandler<DocumentDetails> deleteHandler,
             EventHandler<InputEvent> touchDragListener,
             EventHandler<InputEvent> gestureSelectHandler) {
 
@@ -72,7 +75,8 @@
         mEventConverter = eventConverter;
         mSelectable = selectable;
         mRightClickHandler = rightClickHandler;
-        mActivateHandler = activateHandler;
+        mPickHandler = pickHandler;
+        mPreviewHandler = previewHandler;
         mDeleteHandler = deleteHandler;
         mTouchDragListener = touchDragListener;
         mGestureSelectHandler = gestureSelectHandler;
@@ -183,8 +187,12 @@
         return mKeyListener.onKey(doc, keyCode, event);
     }
 
-    private boolean activateDocument(DocumentDetails doc) {
-        return mActivateHandler.accept(doc);
+    private boolean pickDocument(DocumentDetails doc) {
+        return mPickHandler.accept(doc);
+    }
+
+    private boolean previewDocument(DocumentDetails doc) {
+        return mPreviewHandler.accept(doc);
     }
 
     private boolean selectDocument(DocumentDetails doc) {
@@ -245,7 +253,7 @@
             // otherwise they activate.
             return doc.isInSelectionHotspot(event)
                     ? selectDocument(doc)
-                    : activateDocument(doc);
+                    : pickDocument(doc);
         }
 
         boolean onSingleTapConfirmed(T event) {
@@ -271,11 +279,11 @@
             } else {
                 if (!mSelectionMgr.getSelection().contains(doc.getModelId())) {
                     selectDocument(doc);
-                    mGestureSelectHandler.apply(event);
+                    mGestureSelectHandler.accept(event);
                 } else {
                     // We only initiate drag and drop on long press for touch to allow regular
                     // touch-based scrolling
-                    mTouchDragListener.apply(event);
+                    mTouchDragListener.accept(event);
                 }
             }
         }
@@ -377,7 +385,7 @@
             DocumentDetails doc = event.getDocumentDetails();
             return mSelectionMgr.hasSelection()
                     ? selectDocument(doc)
-                    : activateDocument(doc);
+                    : pickDocument(doc);
         }
 
         final void onLongPress(T event) {
@@ -398,7 +406,7 @@
             // We always delegate final handling of the event,
             // since the handler might want to show a context menu
             // in an empty area or some other weirdo view.
-            return mRightClickHandler.apply(event);
+            return mRightClickHandler.accept(event);
         }
     }
 
@@ -443,7 +451,7 @@
                     // For non-shifted enter keypresses, fall through.
                 case KeyEvent.KEYCODE_DPAD_CENTER:
                 case KeyEvent.KEYCODE_BUTTON_A:
-                    return activateDocument(doc);
+                    return pickDocument(doc);
                 case KeyEvent.KEYCODE_FORWARD_DEL:
                     // This has to be handled here instead of in a keyboard shortcut, because
                     // keyboard shortcuts all have to be modified with the 'Ctrl' key.
@@ -467,9 +475,4 @@
             return mSelectable.test(doc);
         }
     }
-
-    @FunctionalInterface
-    interface DocumentHandler {
-        boolean accept(DocumentDetails doc);
-    }
 }
diff --git a/src/com/android/documentsui/manager/ManageActivity.java b/src/com/android/documentsui/manager/ManageActivity.java
index 4f5de12..0b4c982 100644
--- a/src/com/android/documentsui/manager/ManageActivity.java
+++ b/src/com/android/documentsui/manager/ManageActivity.java
@@ -54,8 +54,8 @@
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.FragmentTuner;
-import com.android.documentsui.dirlist.FragmentTuner.FilesTuner;
 import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.dirlist.MultiSelectManager;
 import com.android.documentsui.roots.RootsCache;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.sidebar.RootsFragment;
@@ -79,9 +79,10 @@
     // Track the time we opened the drawer in response to back being pressed.
     // We use the time gap to figure out whether to close app or reopen the drawer.
     private long mDrawerLastFiddled;
-    private DocumentClipper mClipper;
+    private Tuner mTuner;
     private MenuManager mMenuManager;
     private DirectoryDetails mDetails;
+    private DocumentClipper mClipper;
 
     public ManageActivity() {
         super(R.layout.files_activity, TAG);
@@ -92,13 +93,15 @@
         super.onCreate(icicle);
 
         mClipper = DocumentsApplication.getDocumentClipper(this);
-        mMenuManager = new MenuManager(mSearchManager, getDisplayState());
+        mMenuManager = new MenuManager(mSearchManager, mState);
+        mTuner = new Tuner(this, mState);
         mDetails = new DirectoryDetails(this) {
             @Override
             public boolean hasItemsToPaste() {
                 return mClipper.hasItemsToPaste();
             }
         };
+        mClipper = DocumentsApplication.getDocumentClipper(this);
 
         RootsFragment.show(getFragmentManager(), this::openRootSettings);
 
@@ -528,8 +531,11 @@
     }
 
     @Override
-    public FragmentTuner createFragmentTuner() {
-        return new FilesTuner(this, getDisplayState(), mSortController);
+    public FragmentTuner getFragmentTuner(
+            Model model,
+            MultiSelectManager selectionMgr,
+            boolean mSearchMode) {
+        return mTuner.reset(model, selectionMgr, mSearchMode);
     }
 
     @Override
diff --git a/src/com/android/documentsui/manager/Tuner.java b/src/com/android/documentsui/manager/Tuner.java
new file mode 100644
index 0000000..45d0c12
--- /dev/null
+++ b/src/com/android/documentsui/manager/Tuner.java
@@ -0,0 +1,142 @@
+/*
+ * 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.manager;
+
+import android.util.Log;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.EventListener;
+import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.FragmentTuner;
+import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.dirlist.MultiSelectManager;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides support for Files activity specific specializations of DirectoryFragment.
+ */
+public final class Tuner extends FragmentTuner {
+
+    private static final String TAG = "ManageTuner";
+
+    private final ManageActivity mActivity;
+    private final State mState;
+
+    private FragState mFragState = new FragState(this::onModelLoaded);
+
+    public Tuner(ManageActivity activity, State state) {
+
+        assert(activity != null);
+        assert(state != null);
+
+        mActivity = activity;
+        mState = state;
+    }
+
+    private void onModelLoaded(Model.Update update) {
+        mFragState.modelLoadObserved = true;
+
+        // When launched into empty root, open drawer.
+        if (mFragState.model.isEmpty()
+                && !mState.hasInitialLocationChanged()
+                && !mFragState.searchMode
+                && !mFragState.modelLoadObserved) {
+            // Opens the drawer *if* an openable drawer is present
+            // else this is a no-op.
+            mActivity.setRootsDrawerOpen(true);
+        }
+    }
+
+    @Override
+    public boolean managedModeEnabled() {
+        // When in downloads top level directory, we also show active downloads.
+        // And while we don't allow folders in Downloads, we do allow Zip files in
+        // downloads that themselves can be opened and viewed like directories.
+        // This method helps us understand when to kick in on those special behaviors.
+        return mState.stack.root != null
+                && mState.stack.root.isDownloads()
+                && mState.stack.size() == 1;
+    }
+
+    @Override
+    public boolean dragAndDropEnabled() {
+        return true;
+    }
+
+    @Override
+    public void showChooserForDoc(DocumentInfo doc) {
+        mActivity.showChooserForDoc(doc);
+    }
+
+    @Override
+    public void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
+        mActivity.openInNewWindow(stack, doc);
+    }
+
+    @Override
+    protected boolean onDocumentPicked(String id) {
+        DocumentInfo doc = mFragState.model.getDocument(id);
+        if (doc == null) {
+            Log.w(TAG, "Can't view item. No Document available for modeId: " + id);
+            return false;
+        }
+
+        if (isDocumentEnabled(doc.mimeType, doc.flags)) {
+            mActivity.onDocumentPicked(doc, mFragState.model);
+            mFragState.selectionMgr.clearSelection();
+            return true;
+        }
+        return false;
+    }
+
+    Tuner reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+        mFragState.reset(model, selectionMgr, searchMode);
+        return this;
+    }
+
+    private static final class FragState {
+
+        @Nullable Model model;
+        @Nullable MultiSelectManager selectionMgr;
+        boolean searchMode;
+
+        private final EventListener<Update> mModelUpdateListener;
+
+        public FragState(EventListener<Update> modelUpdateListener) {
+            mModelUpdateListener = modelUpdateListener;
+        }
+
+        // We use this to keep track of whether a model has been previously loaded or not so we can
+        // open the drawer on empty directories on first launch
+        private boolean modelLoadObserved;
+
+        public void reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+            this.searchMode = searchMode;
+            assert(model != null);
+            assert(selectionMgr != null);
+
+            this.model = model;
+            this.selectionMgr = selectionMgr;
+
+            model.addUpdateListener(mModelUpdateListener);
+            modelLoadObserved = false;
+        }
+    }
+}
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 9f738c8..83e446a 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -44,18 +44,18 @@
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.MenuManager.DirectoryDetails;
+import com.android.documentsui.R;
+import com.android.documentsui.Snackbars;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.MimePredicate;
 import com.android.documentsui.base.PairedTask;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
-import com.android.documentsui.R;
-import com.android.documentsui.Snackbars;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.FragmentTuner;
-import com.android.documentsui.dirlist.FragmentTuner.DocumentsTuner;
 import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.dirlist.MultiSelectManager;
 import com.android.documentsui.picker.LastAccessedProvider.Columns;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.sidebar.RootsFragment;
@@ -64,9 +64,11 @@
 import java.util.List;
 
 public class PickActivity extends BaseActivity {
+
     private static final int CODE_FORWARD = 42;
     private static final String TAG = "DocumentsActivity";
     private MenuManager mMenuManager;
+    private Tuner mTuner;
     private DirectoryDetails mDetails;
 
     public PickActivity() {
@@ -76,7 +78,8 @@
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
-        mMenuManager = new MenuManager(mSearchManager, getDisplayState());
+        mTuner = new Tuner(this, mState);
+        mMenuManager = new MenuManager(mSearchManager, mState);
         mDetails = new DirectoryDetails(this);
 
         if (mState.action == ACTION_CREATE) {
@@ -391,10 +394,11 @@
     }
 
     @Override
-    public FragmentTuner createFragmentTuner() {
-        // Currently DocumentsTuner maintains a state specific to the fragment instance. Because of
-        // that, we create a new instance everytime it is needed
-        return new DocumentsTuner(this, getDisplayState(), mSortController);
+    public FragmentTuner getFragmentTuner(
+            Model model,
+            MultiSelectManager selectionMgr,
+            boolean mSearchMode) {
+        return mTuner.reset(model, selectionMgr, mSearchMode);
     }
 
     @Override
diff --git a/src/com/android/documentsui/picker/Tuner.java b/src/com/android/documentsui/picker/Tuner.java
new file mode 100644
index 0000000..033a108
--- /dev/null
+++ b/src/com/android/documentsui/picker/Tuner.java
@@ -0,0 +1,181 @@
+/*
+ * 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.picker;
+
+import static com.android.documentsui.base.State.ACTION_CREATE;
+import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
+import static com.android.documentsui.base.State.ACTION_OPEN;
+import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
+import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
+
+import android.provider.DocumentsContract.Document;
+import android.util.Log;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.EventListener;
+import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.FragmentTuner;
+import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.dirlist.MultiSelectManager;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides support for Platform specific specializations of DirectoryFragment.
+ */
+final class Tuner extends FragmentTuner {
+
+    private static final String TAG = "PickTuner";
+
+
+    private final PickActivity mActivity;
+    private final State mState;
+
+    private final FragState mFragState = new FragState(this::onModelLoaded);
+
+    public Tuner(PickActivity activity, State state) {
+
+        assert(activity != null);
+        assert(state != null);
+
+        mActivity = activity;
+        mState = state;
+    }
+
+    @Override
+    public boolean canSelectType(String docMimeType, int docFlags) {
+        if (!isDocumentEnabled(docMimeType, docFlags)) {
+            return false;
+        }
+
+        if (MimePredicate.isDirectoryType(docMimeType)) {
+            return false;
+        }
+
+        if (mState.action == ACTION_OPEN_TREE
+                || mState.action == ACTION_PICK_COPY_DESTINATION) {
+            // In this case nothing *ever* is selectable...the expected user behavior is
+            // they navigate *into* a folder, then click a confirmation button indicating
+            // that the current directory is the directory they are picking.
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean isDocumentEnabled(String mimeType, int docFlags) {
+        // Directories are always enabled.
+        if (MimePredicate.isDirectoryType(mimeType)) {
+            return true;
+        }
+
+        switch (mState.action) {
+            case ACTION_CREATE:
+                // Read-only files are disabled when creating.
+                if ((docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
+                    return false;
+                }
+            case ACTION_OPEN:
+            case ACTION_GET_CONTENT:
+                final boolean isVirtual = (docFlags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
+                if (isVirtual && mState.openableOnly) {
+                    return false;
+                }
+        }
+
+        return MimePredicate.mimeMatches(mState.acceptMimes, mimeType);
+    }
+
+    private void onModelLoaded(Model.Update update) {
+        mFragState.modelLoadObserved = true;
+        boolean showDrawer = false;
+
+        if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
+            showDrawer = false;
+        }
+        if (mState.external && mState.action == ACTION_GET_CONTENT) {
+            showDrawer = true;
+        }
+        if (mState.action == ACTION_PICK_COPY_DESTINATION) {
+            showDrawer = true;
+        }
+
+        // When launched into empty root, open drawer.
+        if (mFragState.model.isEmpty()) {
+            showDrawer = true;
+        }
+
+        if (showDrawer && !mState.hasInitialLocationChanged() && !mFragState.searchMode
+                && !mFragState.modelLoadObserved) {
+            // This noops on layouts without drawer, so no need to guard.
+            mActivity.setRootsDrawerOpen(true);
+        }
+    }
+
+    @Override
+    protected boolean onDocumentPicked(String id) {
+        DocumentInfo doc = mFragState.model.getDocument(id);
+        if (doc == null) {
+            Log.w(TAG, "Can't view item. No Document available for modeId: " + id);
+            return false;
+        }
+
+        if (isDocumentEnabled(doc.mimeType, doc.flags)) {
+            mActivity.onDocumentPicked(doc, mFragState.model);
+            mFragState.selectionMgr.clearSelection();
+            return true;
+        }
+        return false;
+    }
+
+    Tuner reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+        mFragState.reset(model, selectionMgr, searchMode);
+        return this;
+    }
+
+    private static final class FragState {
+
+        @Nullable Model model;
+        @Nullable MultiSelectManager selectionMgr;
+        boolean searchMode;
+
+        private final EventListener<Update> mModelUpdateListener;
+
+        public FragState(EventListener<Update> modelUpdateListener) {
+            mModelUpdateListener = modelUpdateListener;
+        }
+
+        // We use this to keep track of whether a model has been previously loaded or not so we can
+        // open the drawer on empty directories on first launch
+        private boolean modelLoadObserved;
+
+        public void reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+            this.searchMode = searchMode;
+            assert(model != null);
+            assert(selectionMgr != null);
+
+            this.model = model;
+            this.selectionMgr = selectionMgr;
+
+            model.addUpdateListener(mModelUpdateListener);
+            modelLoadObserved = false;
+        }
+    }
+}
diff --git a/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java b/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
index bd87e03..d641fdb 100644
--- a/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
+++ b/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
@@ -18,6 +18,10 @@
 
 import android.view.ViewGroup;
 
+import com.android.documentsui.base.EventListener;
+import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.testing.TestEventListener;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -27,17 +31,15 @@
 public class TestDocumentsAdapter extends DocumentsAdapter {
 
     List<String> mModelIds = new ArrayList<>();
+    final TestEventListener<Update> mModelListener = new TestEventListener<>();
 
     public TestDocumentsAdapter(List<String> modelIds) {
         mModelIds = modelIds;
     }
 
     @Override
-    public void onModelUpdate(Model model) {
-    }
-
-    @Override
-    public void onModelUpdateFailed(Exception e) {
+    EventListener<Update> getModelUpdateListener() {
+        return mModelListener;
     }
 
     @Override
diff --git a/tests/common/com/android/documentsui/testing/TestEventHandler.java b/tests/common/com/android/documentsui/testing/TestEventHandler.java
new file mode 100644
index 0000000..1322b7a
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestEventHandler.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.documentsui.base.EventHandler;
+
+/**
+ * Test {@link EventHandler} that can be used to spy on,  control responses from,
+ * and make assertions against values tested.
+ */
+public class TestEventHandler<T> extends TestPredicate<T> implements EventHandler<T> {
+
+    @Override
+    public boolean accept(T event) {
+        return test(event);
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestEventListener.java b/tests/common/com/android/documentsui/testing/TestEventListener.java
new file mode 100644
index 0000000..14a131c
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestEventListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.documentsui.base.EventHandler;
+import com.android.documentsui.base.EventListener;
+
+/**
+ * Test {@link EventHandler} that can be used to spy on,  control responses from,
+ * and make assertions against values tested.
+ */
+public class TestEventListener<T> extends TestPredicate<T> implements EventListener<T> {
+
+    @Override
+    public void accept(T event) {
+        test(event);
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestPredicate.java b/tests/common/com/android/documentsui/testing/TestPredicate.java
index 738b096..a722150 100644
--- a/tests/common/com/android/documentsui/testing/TestPredicate.java
+++ b/tests/common/com/android/documentsui/testing/TestPredicate.java
@@ -24,8 +24,8 @@
 import javax.annotation.Nullable;
 
 /**
- * Test predicate that can be used to spy control responses and make
- * assertions against values tested.
+ * Test {@link Predicate} that can be used to spy on,  control responses from,
+ * and make assertions against values tested.
  */
 public class TestPredicate<T> implements Predicate<T> {
 
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 0680e33..1208f03 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -18,10 +18,8 @@
 
 import android.content.Context;
 import android.database.Cursor;
-import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.view.ViewGroup;
 
 import com.android.documentsui.base.State;
 
@@ -55,7 +53,7 @@
 
         mAdapter = new ModelBackedDocumentsAdapter(
                 env, new IconHelper(testContext, State.MODE_GRID));
-        mAdapter.onModelUpdate(mModel);
+        mAdapter.getModelUpdateListener().accept(Model.Update.UPDATE);
     }
 
     // Tests that the item count is correct.
@@ -106,17 +104,4 @@
         @Override
         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {}
     }
-
-    private static class DummyListener implements Model.UpdateListener {
-        public void onModelUpdate(Model model) {}
-        public void onModelUpdateFailed(Exception e) {}
-    }
-
-    private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
-        public int getItemCount() { return 0; }
-        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
-        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            return null;
-        }
-    }
 }
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelTest.java b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
index 75273c9..b9dab0d 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
@@ -33,6 +33,7 @@
 import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortModel;
 import com.android.documentsui.testing.SortModels;
+import com.android.documentsui.testing.TestEventListener;
 
 import java.util.ArrayList;
 import java.util.BitSet;
@@ -101,7 +102,8 @@
 
         // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
         model = new Model();
-        model.addUpdateListener(new DummyListener());
+        // not sure why we add a listener here at all.
+        model.addUpdateListener(new TestEventListener<>());
         model.update(r);
     }
 
@@ -468,11 +470,4 @@
         provider = new TestContentProvider();
         resolver.addProvider(AUTHORITY, provider);
     }
-
-    private static class DummyListener implements Model.UpdateListener {
-        @Override
-        public void onModelUpdate(Model model) {}
-        @Override
-        public void onModelUpdateFailed(Exception e) {}
-    }
 }
diff --git a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index f19b912..dec287b 100644
--- a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -61,7 +61,7 @@
             new ModelBackedDocumentsAdapter(
                 env, new IconHelper(testContext, State.MODE_GRID)));
 
-        mModel.addUpdateListener(mAdapter);
+        mModel.addUpdateListener(mAdapter.getModelUpdateListener());
     }
 
     // Tests that the item count is correct for a directory containing only subdirs.
@@ -153,11 +153,6 @@
         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {}
     }
 
-    private static class DummyListener implements Model.UpdateListener {
-        public void onModelUpdate(Model model) {}
-        public void onModelUpdateFailed(Exception e) {}
-    }
-
     private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
         public int getItemCount() { return 0; }
         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
index 8d26424..0c35542 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -27,6 +27,7 @@
 import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
+import com.android.documentsui.testing.TestEventHandler;
 import com.android.documentsui.testing.TestPredicate;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 
@@ -44,14 +45,14 @@
 
     private UserInputHandler<TestEvent> mInputHandler;
 
-    private TestDocumentsAdapter mAdapter;
     private SelectionProbe mSelection;
     private TestPredicate<DocumentDetails> mCanSelect;
-    private TestPredicate<InputEvent> mRightClickHandler;
-    private TestPredicate<DocumentDetails> mActivateHandler;
-    private TestPredicate<DocumentDetails> mDeleteHandler;
-    private TestPredicate<InputEvent> mDragAndDropHandler;
-    private TestPredicate<InputEvent> mGestureSelectHandler;
+    private TestEventHandler<InputEvent> mRightClickHandler;
+    private TestEventHandler<DocumentDetails> mPickHandler;
+    private TestEventHandler<DocumentDetails> mPreviewHandler;
+    private TestEventHandler<DocumentDetails> mDeleteHandler;
+    private TestEventHandler<InputEvent> mDragAndDropHandler;
+    private TestEventHandler<InputEvent> mGestureSelectHandler;
 
     private Builder mEvent;
 
@@ -62,11 +63,12 @@
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
-        mRightClickHandler = new TestPredicate<>();
-        mActivateHandler = new TestPredicate<>();
-        mDeleteHandler = new TestPredicate<>();
-        mDragAndDropHandler = new TestPredicate<>();
-        mGestureSelectHandler = new TestPredicate<>();
+        mRightClickHandler = new TestEventHandler<>();
+        mPickHandler = new TestEventHandler<>();
+        mPreviewHandler = new TestEventHandler<>();
+        mDeleteHandler = new TestEventHandler<>();
+        mDragAndDropHandler = new TestEventHandler<>();
+        mGestureSelectHandler = new TestEventHandler<>();
 
         mInputHandler = new UserInputHandler<>(
                 selectionMgr,
@@ -75,11 +77,12 @@
                     throw new UnsupportedOperationException("Not exercised in tests.");
                 },
                 mCanSelect,
-                mRightClickHandler::test,
-                mActivateHandler::test,
-                mDeleteHandler::test,
-                mDragAndDropHandler::test,
-                mGestureSelectHandler::test);
+                mRightClickHandler::accept,
+                mPickHandler::accept,
+                mPreviewHandler::accept,
+                mDeleteHandler::accept,
+                mDragAndDropHandler::accept,
+                mGestureSelectHandler::accept);
 
         mEvent = TestEvent.builder().mouse();
     }
@@ -160,7 +163,7 @@
     @Test
     public void testDoubleClick_Activates() {
         mInputHandler.onDoubleTap(mEvent.at(11).build());
-        mActivateHandler.assertLastArgument(mEvent.build().getDocumentDetails());
+        mPickHandler.assertLastArgument(mEvent.build().getDocumentDetails());
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
index be408ba..d98e1aa 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
@@ -24,6 +24,7 @@
 import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
+import com.android.documentsui.testing.TestEventHandler;
 import com.android.documentsui.testing.TestPredicate;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 
@@ -45,14 +46,14 @@
 
     private UserInputHandler<TestEvent> mInputHandler;
 
-    private TestDocumentsAdapter mAdapter;
     private SelectionProbe mSelection;
     private TestPredicate<DocumentDetails> mCanSelect;
-    private TestPredicate<InputEvent> mRightClickHandler;
-    private TestPredicate<DocumentDetails> mActivateHandler;
-    private TestPredicate<DocumentDetails> mDeleteHandler;
-    private TestPredicate<InputEvent> mDragAndDropHandler;
-    private TestPredicate<InputEvent> mGestureSelectHandler;
+    private TestEventHandler<InputEvent> mRightClickHandler;
+    private TestEventHandler<DocumentDetails> mPickHandler;
+    private TestEventHandler<DocumentDetails> mPreviewHandler;
+    private TestEventHandler<DocumentDetails> mDeleteHandler;
+    private TestEventHandler<InputEvent> mDragAndDropHandler;
+    private TestEventHandler<InputEvent> mGestureSelectHandler;
     private Builder mEvent;
 
     @Before
@@ -62,11 +63,12 @@
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
-        mRightClickHandler = new TestPredicate<>();
-        mActivateHandler = new TestPredicate<>();
-        mDeleteHandler = new TestPredicate<>();
-        mDragAndDropHandler = new TestPredicate<>();
-        mGestureSelectHandler = new TestPredicate<>();
+        mRightClickHandler = new TestEventHandler<>();
+        mPickHandler = new TestEventHandler<>();
+        mPreviewHandler = new TestEventHandler<>();
+        mDeleteHandler = new TestEventHandler<>();
+        mDragAndDropHandler = new TestEventHandler<>();
+        mGestureSelectHandler = new TestEventHandler<>();
 
         mInputHandler = new UserInputHandler<>(
                 selectionMgr,
@@ -75,11 +77,12 @@
                     throw new UnsupportedOperationException("Not exercised in tests.");
                 },
                 mCanSelect,
-                mRightClickHandler::test,
-                mActivateHandler::test,
-                mDeleteHandler::test,
-                mDragAndDropHandler::test,
-                mGestureSelectHandler::test);
+                mRightClickHandler::accept,
+                mPickHandler::accept,
+                mPreviewHandler::accept,
+                mDeleteHandler::accept,
+                mDragAndDropHandler::accept,
+                mGestureSelectHandler::accept);
 
         mEvent = TestEvent.builder().mouse();
     }
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
index 051c1cf..5dfa23e 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
@@ -27,6 +27,7 @@
 import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
+import com.android.documentsui.testing.TestEventHandler;
 import com.android.documentsui.testing.TestPredicate;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 
@@ -44,14 +45,14 @@
 
     private UserInputHandler<TestEvent> mInputHandler;
 
-    private TestDocumentsAdapter mAdapter;
     private SelectionProbe mSelection;
     private TestPredicate<DocumentDetails> mCanSelect;
-    private TestPredicate<InputEvent> mRightClickHandler;
-    private TestPredicate<DocumentDetails> mActivateHandler;
-    private TestPredicate<DocumentDetails> mDeleteHandler;
-    private TestPredicate<InputEvent> mDragAndDropHandler;
-    private TestPredicate<InputEvent> mGestureSelectHandler;
+    private TestEventHandler<InputEvent> mRightClickHandler;
+    private TestEventHandler<DocumentDetails> mPickHandler;
+    private TestEventHandler<DocumentDetails> mPreviewHandler;
+    private TestEventHandler<DocumentDetails> mDeleteHandler;
+    private TestEventHandler<InputEvent> mDragAndDropHandler;
+    private TestEventHandler<InputEvent> mGestureSelectHandler;
 
     private Builder mEvent;
 
@@ -61,11 +62,12 @@
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
-        mRightClickHandler = new TestPredicate<>();
-        mActivateHandler = new TestPredicate<>();
-        mDeleteHandler = new TestPredicate<>();
-        mDragAndDropHandler = new TestPredicate<>();
-        mGestureSelectHandler = new TestPredicate<>();
+        mRightClickHandler = new TestEventHandler<>();
+        mPickHandler = new TestEventHandler<>();
+        mPreviewHandler = new TestEventHandler<>();
+        mDeleteHandler = new TestEventHandler<>();
+        mDragAndDropHandler = new TestEventHandler<>();
+        mGestureSelectHandler = new TestEventHandler<>();
 
         mInputHandler = new UserInputHandler<>(
                 selectionMgr,
@@ -74,11 +76,12 @@
                     throw new UnsupportedOperationException("Not exercised in tests.");
                 },
                 mCanSelect,
-                mRightClickHandler::test,
-                mActivateHandler::test,
-                mDeleteHandler::test,
-                mDragAndDropHandler::test,
-                mGestureSelectHandler::test);
+                mRightClickHandler::accept,
+                mPickHandler::accept,
+                mPreviewHandler::accept,
+                mDeleteHandler::accept,
+                mDragAndDropHandler::accept,
+                mGestureSelectHandler::accept);
 
         mEvent = TestEvent.builder();
     }
@@ -86,7 +89,7 @@
     @Test
     public void testTap_ActivatesWhenNoExistingSelection() {
         mInputHandler.onSingleTapUp(mEvent.at(11).build());
-        mActivateHandler.assertLastArgument(mEvent.build().getDocumentDetails());
+        mPickHandler.assertLastArgument(mEvent.build().getDocumentDetails());
     }
 
     @Test