Home | History | Annotate | Download | only in dirlist
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.documentsui.dirlist;
     18 
     19 import static com.android.documentsui.base.DocumentInfo.getCursorInt;
     20 import static com.android.documentsui.base.DocumentInfo.getCursorString;
     21 import static com.android.documentsui.base.Shared.DEBUG;
     22 import static com.android.documentsui.base.Shared.VERBOSE;
     23 import static com.android.documentsui.base.State.MODE_GRID;
     24 import static com.android.documentsui.base.State.MODE_LIST;
     25 
     26 import android.annotation.DimenRes;
     27 import android.annotation.FractionRes;
     28 import android.annotation.IntDef;
     29 import android.app.Activity;
     30 import android.app.ActivityManager;
     31 import android.app.Fragment;
     32 import android.app.FragmentManager;
     33 import android.app.FragmentTransaction;
     34 import android.content.ClipData;
     35 import android.content.Context;
     36 import android.content.Intent;
     37 import android.content.res.Resources;
     38 import android.database.Cursor;
     39 import android.graphics.drawable.StateListDrawable;
     40 import android.net.Uri;
     41 import android.os.Build;
     42 import android.os.Bundle;
     43 import android.os.Handler;
     44 import android.os.Parcelable;
     45 import android.provider.DocumentsContract;
     46 import android.provider.DocumentsContract.Document;
     47 import android.support.v4.widget.SwipeRefreshLayout;
     48 import android.support.v7.widget.GridLayoutManager;
     49 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
     50 import android.support.v7.widget.RecyclerView;
     51 import android.support.v7.widget.RecyclerView.RecyclerListener;
     52 import android.support.v7.widget.RecyclerView.ViewHolder;
     53 import android.util.Log;
     54 import android.util.SparseArray;
     55 import android.view.ContextMenu;
     56 import android.view.DragEvent;
     57 import android.view.HapticFeedbackConstants;
     58 import android.view.LayoutInflater;
     59 import android.view.MenuInflater;
     60 import android.view.MenuItem;
     61 import android.view.MotionEvent;
     62 import android.view.View;
     63 import android.view.ViewGroup;
     64 import android.widget.ImageView;
     65 
     66 import com.android.documentsui.ActionHandler;
     67 import com.android.documentsui.ActionModeController;
     68 import com.android.documentsui.BaseActivity;
     69 import com.android.documentsui.BaseActivity.RetainedState;
     70 import com.android.documentsui.DirectoryReloadLock;
     71 import com.android.documentsui.DocumentsApplication;
     72 import com.android.documentsui.DragAndDropHelper;
     73 import com.android.documentsui.FocusManager;
     74 import com.android.documentsui.Injector;
     75 import com.android.documentsui.Injector.ContentScoped;
     76 import com.android.documentsui.Injector.Injected;
     77 import com.android.documentsui.ItemDragListener;
     78 import com.android.documentsui.Metrics;
     79 import com.android.documentsui.Model;
     80 import com.android.documentsui.R;
     81 import com.android.documentsui.ThumbnailCache;
     82 import com.android.documentsui.base.DocumentInfo;
     83 import com.android.documentsui.base.DocumentStack;
     84 import com.android.documentsui.base.EventHandler;
     85 import com.android.documentsui.base.EventListener;
     86 import com.android.documentsui.base.Events.InputEvent;
     87 import com.android.documentsui.base.Events.MotionInputEvent;
     88 import com.android.documentsui.base.Features;
     89 import com.android.documentsui.base.RootInfo;
     90 import com.android.documentsui.base.Shared;
     91 import com.android.documentsui.base.State;
     92 import com.android.documentsui.base.State.ViewMode;
     93 import com.android.documentsui.clipping.ClipStore;
     94 import com.android.documentsui.clipping.DocumentClipper;
     95 import com.android.documentsui.clipping.UrisSupplier;
     96 import com.android.documentsui.dirlist.AnimationView.AnimationType;
     97 import com.android.documentsui.picker.PickActivity;
     98 import com.android.documentsui.selection.BandController;
     99 import com.android.documentsui.selection.GestureSelector;
    100 import com.android.documentsui.selection.Selection;
    101 import com.android.documentsui.selection.SelectionManager;
    102 import com.android.documentsui.selection.SelectionMetadata;
    103 import com.android.documentsui.services.FileOperation;
    104 import com.android.documentsui.services.FileOperationService;
    105 import com.android.documentsui.services.FileOperationService.OpType;
    106 import com.android.documentsui.services.FileOperations;
    107 import com.android.documentsui.sorting.SortDimension;
    108 import com.android.documentsui.sorting.SortModel;
    109 
    110 import java.io.IOException;
    111 import java.lang.annotation.Retention;
    112 import java.lang.annotation.RetentionPolicy;
    113 import java.util.List;
    114 
    115 import javax.annotation.Nullable;
    116 
    117 /**
    118  * Display the documents inside a single directory.
    119  */
    120 public class DirectoryFragment extends Fragment
    121         implements ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
    122 
    123     static final int TYPE_NORMAL = 1;
    124     static final int TYPE_RECENT_OPEN = 2;
    125 
    126     @IntDef(flag = true, value = {
    127             REQUEST_COPY_DESTINATION
    128     })
    129     @Retention(RetentionPolicy.SOURCE)
    130     public @interface RequestCode {}
    131     public static final int REQUEST_COPY_DESTINATION = 1;
    132 
    133     private static final String TAG = "DirectoryFragment";
    134     private static final int LOADER_ID = 42;
    135 
    136     private static final int CACHE_EVICT_LIMIT = 100;
    137     private static final int REFRESH_SPINNER_TIMEOUT = 500;
    138 
    139     private BaseActivity mActivity;
    140 
    141     private State mState;
    142     private Model mModel;
    143     private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
    144     private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment();
    145 
    146     @Injected
    147     @ContentScoped
    148     private Injector<?> mInjector;
    149 
    150     @Injected
    151     @ContentScoped
    152     private SelectionManager mSelectionMgr;
    153 
    154     @Injected
    155     @ContentScoped
    156     private FocusManager mFocusManager;
    157 
    158     @Injected
    159     @ContentScoped
    160     private ActionHandler mActions;
    161 
    162     @Injected
    163     @ContentScoped
    164     private ActionModeController mActionModeController;
    165 
    166     private SelectionMetadata mSelectionMetadata;
    167     private UserInputHandler<InputEvent> mInputHandler;
    168     private @Nullable BandController mBandController;
    169     private @Nullable DragHoverListener mDragHoverListener;
    170     private IconHelper mIconHelper;
    171     private SwipeRefreshLayout mRefreshLayout;
    172     private RecyclerView mRecView;
    173     private View mFileList;
    174 
    175     private DocumentsAdapter mAdapter;
    176     private DocumentClipper mClipper;
    177     private GridLayoutManager mLayout;
    178     private int mColumnCount = 1;  // This will get updated when layout changes.
    179 
    180     private float mLiveScale = 1.0f;
    181     private @ViewMode int mMode;
    182 
    183     private View mProgressBar;
    184 
    185     private DirectoryState mLocalState;
    186     private DirectoryReloadLock mReloadLock = new DirectoryReloadLock();
    187 
    188     // Note, we use !null to indicate that selection was restored (from rotation).
    189     // So don't fiddle with this field unless you've got the bigger picture in mind.
    190     private @Nullable Selection mRestoredSelection = null;
    191 
    192     private SortModel.UpdateListener mSortListener = (model, updateType) -> {
    193         // Only when sort order has changed do we need to trigger another loading.
    194         if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
    195             mActions.loadDocumentsForCurrentStack();
    196         }
    197     };
    198 
    199     private final Runnable mOnDisplayStateChanged = this::onDisplayStateChanged;
    200 
    201     @Override
    202     public View onCreateView(
    203             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    204 
    205         BaseActivity activity = (BaseActivity) getActivity();
    206         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
    207 
    208         mProgressBar = view.findViewById(R.id.progressbar);
    209         assert(mProgressBar != null);
    210 
    211         mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
    212         mRecView.setRecyclerListener(
    213                 new RecyclerListener() {
    214                     @Override
    215                     public void onViewRecycled(ViewHolder holder) {
    216                         cancelThumbnailTask(holder.itemView);
    217                     }
    218                 });
    219 
    220         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
    221         mRefreshLayout.setOnRefreshListener(this);
    222 
    223         Resources resources = getContext().getResources();
    224         new FastScroller(mRecView,
    225                 (StateListDrawable) resources.getDrawable(R.drawable.fast_scroll_thumb_drawable),
    226                 resources.getDrawable(R.drawable.fast_scroll_track_drawable),
    227                 (StateListDrawable) resources.getDrawable(R.drawable.fast_scroll_thumb_drawable),
    228                 resources.getDrawable(R.drawable.fast_scroll_track_drawable),
    229                 resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
    230                 resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
    231                 resources.getDimensionPixelOffset(R.dimen.fastscroll_margin)
    232                 );
    233         mRecView.setItemAnimator(new DirectoryItemAnimator(activity));
    234         mFileList = view.findViewById(R.id.file_list);
    235 
    236         mInjector = activity.getInjector();
    237         mModel = mInjector.getModel();
    238         mModel.reset();
    239 
    240         mInjector.actions.registerDisplayStateChangedListener(mOnDisplayStateChanged);
    241 
    242         mDragHoverListener = mInjector.config.dragAndDropEnabled()
    243                 ? DragHoverListener.create(new DirectoryDragListener(this), mRecView)
    244                 : null;
    245 
    246         // Make the recycler and the empty views responsive to drop events when allowed.
    247         mRecView.setOnDragListener(mDragHoverListener);
    248 
    249         return view;
    250     }
    251 
    252     @Override
    253     public void onDestroyView() {
    254         mSelectionMgr.clearSelection();
    255         mInjector.actions.unregisterDisplayStateChangedListener(mOnDisplayStateChanged);
    256 
    257         // Cancel any outstanding thumbnail requests
    258         final int count = mRecView.getChildCount();
    259         for (int i = 0; i < count; i++) {
    260             final View view = mRecView.getChildAt(i);
    261             cancelThumbnailTask(view);
    262         }
    263 
    264         mModel.removeUpdateListener(mModelUpdateListener);
    265         mModel.removeUpdateListener(mAdapter.getModelUpdateListener());
    266 
    267         super.onDestroyView();
    268     }
    269 
    270     @Override
    271     public void onActivityCreated(Bundle savedInstanceState) {
    272         super.onActivityCreated(savedInstanceState);
    273 
    274         mActivity = (BaseActivity) getActivity();
    275         mState = mActivity.getDisplayState();
    276 
    277         // Read arguments when object created for the first time.
    278         // Restore state if fragment recreated.
    279         Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
    280 
    281         mLocalState = new DirectoryState();
    282         mLocalState.restore(args);
    283 
    284         // Restore any selection we may have squirreled away in retained state.
    285         @Nullable RetainedState retained = mActivity.getRetainedState();
    286         if (retained != null && retained.hasSelection()) {
    287             // We claim the selection for ourselves and null it out once used
    288             // so we don't have a rando selection hanging around in RetainedState.
    289             mRestoredSelection = retained.selection;
    290             retained.selection = null;
    291         }
    292 
    293         mIconHelper = new IconHelper(mActivity, MODE_GRID);
    294         mClipper = DocumentsApplication.getDocumentClipper(getContext());
    295 
    296         mAdapter = new DirectoryAddonsAdapter(
    297                 mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper));
    298 
    299         mRecView.setAdapter(mAdapter);
    300 
    301         mLayout = new GridLayoutManager(getContext(), mColumnCount) {
    302             @Override
    303             public void onLayoutCompleted(RecyclerView.State state) {
    304                 super.onLayoutCompleted(state);
    305                 mFocusManager.onLayoutCompleted();
    306             }
    307         };
    308 
    309         SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
    310         if (lookup != null) {
    311             mLayout.setSpanSizeLookup(lookup);
    312         }
    313         mRecView.setLayoutManager(mLayout);
    314 
    315         mModel.addUpdateListener(mAdapter.getModelUpdateListener());
    316         mModel.addUpdateListener(mModelUpdateListener);
    317 
    318         mSelectionMgr = mInjector.getSelectionManager(mAdapter, this::canSetSelectionState);
    319         mFocusManager = mInjector.getFocusManager(mRecView, mModel);
    320         mActions = mInjector.getActionHandler(mReloadLock);
    321 
    322         mRecView.setAccessibilityDelegateCompat(
    323                 new AccessibilityEventRouter(mRecView,
    324                         (View child) -> onAccessibilityClick(child)));
    325         mSelectionMetadata = new SelectionMetadata(mModel::getItem);
    326         mSelectionMgr.addItemCallback(mSelectionMetadata);
    327 
    328         GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView, mReloadLock);
    329 
    330         if (mState.allowMultiple) {
    331             mBandController = new BandController(
    332                     mRecView,
    333                     mAdapter,
    334                     mSelectionMgr,
    335                     mReloadLock,
    336                     (int pos) -> {
    337                         // The band selection model only operates on documents and directories.
    338                         // Exclude other types of adapter items like whitespace and dividers.
    339                         RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos);
    340                         return ModelBackedDocumentsAdapter.isContentType(vh.getItemViewType());
    341                     });
    342         }
    343 
    344         DragStartListener mDragStartListener = mInjector.config.dragAndDropEnabled()
    345                 ? DragStartListener.create(
    346                         mIconHelper,
    347                         mActivity,
    348                         mModel,
    349                         mSelectionMgr,
    350                         mClipper,
    351                         mState,
    352                         this::getModelId,
    353                         mRecView::findChildViewUnder,
    354                         getContext().getDrawable(R.drawable.ic_doc_generic),
    355                         mActivity.getShadowBuilder())
    356                 : DragStartListener.DUMMY;
    357 
    358         EventHandler<InputEvent> gestureHandler = mState.allowMultiple
    359                 ? gestureSel::start
    360                 : EventHandler.createStub(false);
    361 
    362         mInputHandler = new UserInputHandler<>(
    363                 mActions,
    364                 mFocusManager,
    365                 mSelectionMgr,
    366                 (MotionEvent t) -> MotionInputEvent.obtain(t, mRecView),
    367                 this::canSelect,
    368                 this::onContextMenuClick,
    369                 mDragStartListener::onTouchDragEvent,
    370                 gestureHandler,
    371                 () -> mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS));
    372 
    373         new ListeningGestureDetector(
    374                 mInjector.features,
    375                 this.getContext(),
    376                 mRecView,
    377                 mDragStartListener::onMouseDragEvent,
    378                 mRefreshLayout::setEnabled,
    379                 gestureSel,
    380                 mInputHandler,
    381                 mBandController,
    382                 this::scaleLayout);
    383 
    384         mActionModeController = mInjector.getActionModeController(
    385                 mSelectionMetadata,
    386                 this::handleMenuItemClick);
    387 
    388         mSelectionMgr.addCallback(mActionModeController);
    389 
    390         final ActivityManager am = (ActivityManager) mActivity.getSystemService(
    391                 Context.ACTIVITY_SERVICE);
    392         boolean svelte = am.isLowRamDevice() && (mState.stack.isRecents());
    393         mIconHelper.setThumbnailsEnabled(!svelte);
    394 
    395         // If mDocument is null, we sort it by last modified by default because it's in Recents.
    396         final boolean prefersLastModified =
    397                 (mLocalState.mDocument == null)
    398                 || mLocalState.mDocument.prefersSortByLastModified();
    399         // Call this before adding the listener to avoid restarting the loader one more time
    400         mState.sortModel.setDefaultDimension(
    401                 prefersLastModified
    402                         ? SortModel.SORT_DIMENSION_ID_DATE
    403                         : SortModel.SORT_DIMENSION_ID_TITLE);
    404 
    405         // Kick off loader at least once
    406         mActions.loadDocumentsForCurrentStack();
    407     }
    408 
    409     @Override
    410     public void onStart() {
    411         super.onStart();
    412 
    413         // Add listener to update contents on sort model change
    414         mState.sortModel.addListener(mSortListener);
    415     }
    416 
    417     @Override
    418     public void onStop() {
    419         super.onStop();
    420 
    421         mState.sortModel.removeListener(mSortListener);
    422 
    423         // Remember last scroll location
    424         final SparseArray<Parcelable> container = new SparseArray<>();
    425         getView().saveHierarchyState(container);
    426         mState.dirConfigs.put(mLocalState.getConfigKey(), container);
    427     }
    428 
    429     public void retainState(RetainedState state) {
    430         state.selection = mSelectionMgr.getSelection(new Selection());
    431     }
    432 
    433     @Override
    434     public void onSaveInstanceState(Bundle outState) {
    435         super.onSaveInstanceState(outState);
    436 
    437         mLocalState.save(outState);
    438     }
    439 
    440     @Override
    441     public void onCreateContextMenu(ContextMenu menu,
    442             View v,
    443             ContextMenu.ContextMenuInfo menuInfo) {
    444         super.onCreateContextMenu(menu, v, menuInfo);
    445         final MenuInflater inflater = getActivity().getMenuInflater();
    446 
    447         final String modelId = getModelId(v);
    448         if (modelId == null) {
    449             // TODO: inject DirectoryDetails into MenuManager constructor
    450             // Since both classes are supplied by Activity and created
    451             // at the same time.
    452             mInjector.menuManager.inflateContextMenuForContainer(menu, inflater);
    453         } else {
    454             mInjector.menuManager.inflateContextMenuForDocs(menu, inflater, mSelectionMetadata);
    455         }
    456     }
    457 
    458     @Override
    459     public boolean onContextItemSelected(MenuItem item) {
    460         return handleMenuItemClick(item);
    461     }
    462 
    463     private void handleCopyResult(int resultCode, Intent data) {
    464 
    465         FileOperation operation = mLocalState.claimPendingOperation();
    466 
    467         if (resultCode == Activity.RESULT_CANCELED || data == null) {
    468             // User pressed the back button or otherwise cancelled the destination pick. Don't
    469             // proceed with the copy.
    470             operation.dispose();
    471             return;
    472         }
    473 
    474         operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
    475         FileOperations.start(
    476                 mActivity,
    477                 operation,
    478                 mInjector.dialogs::showFileOperationStatus);
    479     }
    480 
    481     protected boolean onContextMenuClick(InputEvent e) {
    482         final View v;
    483         final float x, y;
    484         if (e.isOverModelItem()) {
    485             DocumentHolder doc = (DocumentHolder) e.getDocumentDetails();
    486 
    487             v = doc.itemView;
    488             x = e.getX() - v.getLeft();
    489             y = e.getY() - v.getTop();
    490         } else {
    491             v = mRecView;
    492             x = e.getX();
    493             y = e.getY();
    494         }
    495 
    496         mInjector.menuManager.showContextMenu(this, v, x, y);
    497 
    498         return true;
    499     }
    500 
    501     public void onViewModeChanged() {
    502         // Mode change is just visual change; no need to kick loader.
    503         onDisplayStateChanged();
    504     }
    505 
    506     private void onDisplayStateChanged() {
    507         updateLayout(mState.derivedMode);
    508         mRecView.setAdapter(mAdapter);
    509     }
    510 
    511     /**
    512      * Updates the layout after the view mode switches.
    513      * @param mode The new view mode.
    514      */
    515     private void updateLayout(@ViewMode int mode) {
    516         mMode = mode;
    517         mColumnCount = calculateColumnCount(mode);
    518         if (mLayout != null) {
    519             mLayout.setSpanCount(mColumnCount);
    520         }
    521 
    522         int pad = getDirectoryPadding(mode);
    523         mRecView.setPadding(pad, pad, pad, pad);
    524         mRecView.requestLayout();
    525         if (mBandController != null) {
    526             mBandController.handleLayoutChanged();
    527         }
    528         mIconHelper.setViewMode(mode);
    529     }
    530 
    531     /**
    532      * Updates the layout after the view mode switches.
    533      * @param mode The new view mode.
    534      */
    535     private void scaleLayout(float scale) {
    536         assert(Build.IS_DEBUGGABLE);
    537         if (VERBOSE) Log.v(
    538                 TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale);
    539 
    540         if (mMode == MODE_GRID) {
    541             float minScale = getFraction(R.fraction.grid_scale_min);
    542             float maxScale = getFraction(R.fraction.grid_scale_max);
    543             float nextScale = mLiveScale * scale;
    544 
    545             if (VERBOSE) Log.v(TAG,
    546                     "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale);
    547 
    548             if (nextScale > minScale && nextScale < maxScale) {
    549                 if (DEBUG) Log.d(TAG, "Updating grid scale: " + scale);
    550                 mLiveScale = nextScale;
    551                 updateLayout(mMode);
    552             }
    553 
    554         } else {
    555             if (DEBUG) Log.d(TAG, "List mode, ignoring scale: " + scale);
    556             mLiveScale = 1.0f;
    557         }
    558     }
    559 
    560     private int calculateColumnCount(@ViewMode int mode) {
    561         if (mode == MODE_LIST) {
    562             // List mode is a "grid" with 1 column.
    563             return 1;
    564         }
    565 
    566         int cellWidth = getScaledSize(R.dimen.grid_width);
    567         int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin);
    568         int viewPadding =
    569                 (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale);
    570 
    571         // RecyclerView sometimes gets a width of 0 (see b/27150284).
    572         // Clamp so that we always lay out the grid with at least 2 columns by default.
    573         int columnCount = Math.max(2,
    574                 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
    575 
    576         // Finally with our grid count logic firmly in place, we apply any live scaling
    577         // captured by the scale gesture detector.
    578         return Math.max(1, Math.round(columnCount / mLiveScale));
    579     }
    580 
    581 
    582     /**
    583      * Moderately abuse the "fraction" resource type for our purposes.
    584      */
    585     private float getFraction(@FractionRes int id) {
    586         return getResources().getFraction(id, 1, 0);
    587     }
    588 
    589     private int getScaledSize(@DimenRes int id) {
    590         return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
    591     }
    592 
    593     private int getDirectoryPadding(@ViewMode int mode) {
    594         switch (mode) {
    595             case MODE_GRID:
    596                 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
    597             case MODE_LIST:
    598                 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
    599             default:
    600                 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
    601         }
    602     }
    603 
    604     private boolean handleMenuItemClick(MenuItem item) {
    605         Selection selection = mSelectionMgr.getSelection(new Selection());
    606 
    607         switch (item.getItemId()) {
    608             case R.id.menu_open:
    609                 openDocuments(selection);
    610                 mActionModeController.finishActionMode();
    611                 return true;
    612 
    613             case R.id.menu_open_with:
    614                 showChooserForDoc(selection);
    615                 return true;
    616 
    617             case R.id.menu_open_in_new_window:
    618                 mActions.openSelectedInNewWindow();
    619                 return true;
    620 
    621             case R.id.menu_share:
    622                 mActions.shareSelectedDocuments();
    623                 return true;
    624 
    625             case R.id.menu_delete:
    626                 // deleteDocuments will end action mode if the documents are deleted.
    627                 // It won't end action mode if user cancels the delete.
    628                 mActions.deleteSelectedDocuments();
    629                 return true;
    630 
    631             case R.id.menu_copy_to:
    632                 transferDocuments(selection, null, FileOperationService.OPERATION_COPY);
    633                 // TODO: Only finish selection mode if copy-to is not canceled.
    634                 // Need to plum down into handling the way we do with deleteDocuments.
    635                 mActionModeController.finishActionMode();
    636                 return true;
    637 
    638             case R.id.menu_compress:
    639                 transferDocuments(selection, mState.stack,
    640                         FileOperationService.OPERATION_COMPRESS);
    641                 // TODO: Only finish selection mode if compress is not canceled.
    642                 // Need to plum down into handling the way we do with deleteDocuments.
    643                 mActionModeController.finishActionMode();
    644                 return true;
    645 
    646             // TODO: Implement extract (to the current directory).
    647             case R.id.menu_extract_to:
    648                 transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT);
    649                 // TODO: Only finish selection mode if compress-to is not canceled.
    650                 // Need to plum down into handling the way we do with deleteDocuments.
    651                 mActionModeController.finishActionMode();
    652                 return true;
    653 
    654             case R.id.menu_move_to:
    655                 // Exit selection mode first, so we avoid deselecting deleted documents.
    656                 mActionModeController.finishActionMode();
    657                 transferDocuments(selection, null, FileOperationService.OPERATION_MOVE);
    658                 return true;
    659 
    660             case R.id.menu_cut_to_clipboard:
    661                 mActions.cutToClipboard();
    662                 return true;
    663 
    664             case R.id.menu_copy_to_clipboard:
    665                 mActions.copyToClipboard();
    666                 return true;
    667 
    668             case R.id.menu_paste_from_clipboard:
    669                 pasteFromClipboard();
    670                 return true;
    671 
    672             case R.id.menu_paste_into_folder:
    673                 pasteIntoFolder();
    674                 return true;
    675 
    676             case R.id.menu_select_all:
    677                 mActions.selectAllFiles();
    678                 return true;
    679 
    680             case R.id.menu_rename:
    681                 // Exit selection mode first, so we avoid deselecting deleted
    682                 // (renamed) documents.
    683                 mActionModeController.finishActionMode();
    684                 renameDocuments(selection);
    685                 return true;
    686 
    687             case R.id.menu_view_in_owner:
    688                 mActions.viewInOwner();
    689                 return true;
    690 
    691             default:
    692                 // See if BaseActivity can handle this particular MenuItem
    693                 if (!mActivity.onOptionsItemSelected(item)) {
    694                     if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
    695                     return false;
    696                 }
    697                 return true;
    698         }
    699     }
    700 
    701     private boolean onAccessibilityClick(View child) {
    702         DocumentDetails doc = getDocumentHolder(child);
    703         mActions.openDocument(doc, ActionHandler.VIEW_TYPE_PREVIEW,
    704                 ActionHandler.VIEW_TYPE_REGULAR);
    705         return true;
    706     }
    707 
    708     private void cancelThumbnailTask(View view) {
    709         final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
    710         if (iconThumb != null) {
    711             mIconHelper.stopLoading(iconThumb);
    712         }
    713     }
    714 
    715     // Support for opening multiple documents is currently exclusive to DocumentsActivity.
    716     private void openDocuments(final Selection selected) {
    717         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
    718 
    719         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    720         List<DocumentInfo> docs = mModel.getDocuments(selected);
    721         if (docs.size() > 1) {
    722             mActivity.onDocumentsPicked(docs);
    723         } else {
    724             mActivity.onDocumentPicked(docs.get(0));
    725         }
    726     }
    727 
    728     private void showChooserForDoc(final Selection selected) {
    729         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
    730 
    731         assert(selected.size() == 1);
    732         DocumentInfo doc =
    733                 DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
    734         mActions.showChooserForDoc(doc);
    735     }
    736 
    737     private void transferDocuments(final Selection selected, @Nullable DocumentStack destination,
    738             final @OpType int mode) {
    739         switch (mode) {
    740             case FileOperationService.OPERATION_COPY:
    741                 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
    742                 break;
    743             case FileOperationService.OPERATION_COMPRESS:
    744                 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COMPRESS);
    745                 break;
    746             case FileOperationService.OPERATION_EXTRACT:
    747                 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_EXTRACT_TO);
    748                 break;
    749             case FileOperationService.OPERATION_MOVE:
    750                 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_MOVE_TO);
    751                 break;
    752         }
    753 
    754         UrisSupplier srcs;
    755         try {
    756             ClipStore clipStorage = DocumentsApplication.getClipStore(getContext());
    757             srcs = UrisSupplier.create(selected, mModel::getItemUri, clipStorage);
    758         } catch (IOException e) {
    759             throw new RuntimeException("Failed to create uri supplier.", e);
    760         }
    761 
    762         final DocumentInfo parent = mState.stack.peek();
    763         final FileOperation operation = new FileOperation.Builder()
    764                 .withOpType(mode)
    765                 .withSrcParent(parent == null ? null : parent.derivedUri)
    766                 .withSrcs(srcs)
    767                 .build();
    768 
    769         if (destination != null) {
    770             operation.setDestination(destination);
    771             FileOperations.start(
    772                     mActivity,
    773                     operation,
    774                     mInjector.dialogs::showFileOperationStatus);
    775             return;
    776         }
    777 
    778         // Pop up a dialog to pick a destination.  This is inadequate but works for now.
    779         // TODO: Implement a picker that is to spec.
    780         mLocalState.mPendingOperation = operation;
    781         final Intent intent = new Intent(
    782                 Shared.ACTION_PICK_COPY_DESTINATION,
    783                 Uri.EMPTY,
    784                 getActivity(),
    785                 PickActivity.class);
    786 
    787         // Set an appropriate title on the drawer when it is shown in the picker.
    788         // Coupled with the fact that we auto-open the drawer for copy/move operations
    789         // it should basically be the thing people see first.
    790         int drawerTitleId;
    791         switch (mode) {
    792             case FileOperationService.OPERATION_COPY:
    793                 drawerTitleId = R.string.menu_copy;
    794                 break;
    795             case FileOperationService.OPERATION_COMPRESS:
    796                 drawerTitleId = R.string.menu_compress;
    797                 break;
    798             case FileOperationService.OPERATION_EXTRACT:
    799                 drawerTitleId = R.string.menu_extract;
    800                 break;
    801             case FileOperationService.OPERATION_MOVE:
    802                 drawerTitleId = R.string.menu_move;
    803                 break;
    804             default:
    805                 throw new UnsupportedOperationException("Unknown mode: " + mode);
    806         }
    807 
    808         intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
    809 
    810         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    811         List<DocumentInfo> docs = mModel.getDocuments(selected);
    812 
    813         // Determine if there is a directory in the set of documents
    814         // to be copied? Why? Directory creation isn't supported by some roots
    815         // (like Downloads). This informs DocumentsActivity (the "picker")
    816         // to restrict available roots to just those with support.
    817         intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
    818         intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode);
    819 
    820         // This just identifies the type of request...we'll check it
    821         // when we reveive a response.
    822         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
    823     }
    824 
    825     @Override
    826     public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
    827         switch (requestCode) {
    828             case REQUEST_COPY_DESTINATION:
    829                 handleCopyResult(resultCode, data);
    830                 break;
    831             default:
    832                 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
    833         }
    834     }
    835 
    836     private static boolean hasDirectory(List<DocumentInfo> docs) {
    837         for (DocumentInfo info : docs) {
    838             if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
    839                 return true;
    840             }
    841         }
    842         return false;
    843     }
    844 
    845     private void renameDocuments(Selection selected) {
    846         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_RENAME);
    847 
    848         // Batch renaming not supported
    849         // Rename option is only available in menu when 1 document selected
    850         assert(selected.size() == 1);
    851 
    852         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    853         List<DocumentInfo> docs = mModel.getDocuments(selected);
    854         RenameDocumentFragment.show(getChildFragmentManager(), docs.get(0));
    855     }
    856 
    857     Model getModel(){
    858         return mModel;
    859     }
    860 
    861     private boolean isDocumentEnabled(String mimeType, int flags) {
    862         return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
    863     }
    864 
    865     /**
    866      * Paste selection files from the primary clip into the current window.
    867      */
    868     public void pasteFromClipboard() {
    869         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
    870         // Since we are pasting into the current window, we already have the destination in the
    871         // stack. No need for a destination DocumentInfo.
    872         mClipper.copyFromClipboard(
    873                 mState.stack,
    874                 mInjector.dialogs::showFileOperationStatus);
    875         getActivity().invalidateOptionsMenu();
    876     }
    877 
    878     public void pasteIntoFolder() {
    879         assert (mSelectionMgr.getSelection().size() == 1);
    880 
    881         String modelId = mSelectionMgr.getSelection().iterator().next();
    882         Cursor dstCursor = mModel.getItem(modelId);
    883         if (dstCursor == null) {
    884             Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
    885             return;
    886         }
    887         DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
    888         mClipper.copyFromClipboard(
    889                 destination,
    890                 mState.stack,
    891                 mInjector.dialogs::showFileOperationStatus);
    892         getActivity().invalidateOptionsMenu();
    893     }
    894 
    895     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
    896         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    897         if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
    898             // Make a directory item a drop target. Drop on non-directories and empty space
    899             // is handled at the list/grid view level.
    900             view.setOnDragListener(mDragHoverListener);
    901         }
    902     }
    903 
    904     void dragStopped(boolean result) {
    905         if (result) {
    906             mSelectionMgr.clearSelection();
    907         }
    908     }
    909 
    910     @Override
    911     public void runOnUiThread(Runnable runnable) {
    912         getActivity().runOnUiThread(runnable);
    913     }
    914 
    915     // In DirectoryFragment, we close the roots drawer right away.
    916     // We also want to update the Drag Shadow to indicate whether the
    917     // item is droppable or not.
    918     @Override
    919     public void onDragEntered(View v, Object localState) {
    920         mActivity.setRootsDrawerOpen(false);
    921         mActivity.getShadowBuilder()
    922                 .setAppearDroppable(DragAndDropHelper.canCopyTo(localState, getDestination(v)));
    923         v.updateDragShadow(mActivity.getShadowBuilder());
    924     }
    925 
    926     // In DirectoryFragment, we always reset the background of the Drag Shadow once it
    927     // exits.
    928     @Override
    929     public void onDragExited(View v, Object localState) {
    930         mActivity.getShadowBuilder().resetBackground();
    931         v.updateDragShadow(mActivity.getShadowBuilder());
    932         if (v.getParent() == mRecView) {
    933             DocumentHolder holder = getDocumentHolder(v);
    934             if (holder != null) {
    935                 holder.resetDropHighlight();
    936             }
    937         }
    938     }
    939 
    940     // In DirectoryFragment, we spring loads the hovered folder.
    941     @Override
    942     public void onViewHovered(View view) {
    943         BaseActivity activity = mActivity;
    944         if (getModelId(view) != null) {
    945             mActions.springOpenDirectory(getDestination(view));
    946         }
    947         activity.setRootsDrawerOpen(false);
    948     }
    949 
    950     boolean handleDropEvent(View v, DragEvent event) {
    951         BaseActivity activity = (BaseActivity) getActivity();
    952         activity.setRootsDrawerOpen(false);
    953 
    954         ClipData clipData = event.getClipData();
    955         assert (clipData != null);
    956 
    957         assert(mClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
    958 
    959         if (!DragAndDropHelper.canCopyTo(event.getLocalState(), getDestination(v))) {
    960             return false;
    961         }
    962 
    963         // Recognize multi-window drag and drop based on the fact that localState is not
    964         // carried between processes. It will stop working when the localsState behavior
    965         // is changed. The info about window should be passed in the localState then.
    966         // The localState could also be null for copying from Recents in single window
    967         // mode, but Recents doesn't offer this functionality (no directories).
    968         Metrics.logUserAction(getContext(),
    969                 event.getLocalState() == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
    970                         : Metrics.USER_ACTION_DRAG_N_DROP);
    971 
    972         DocumentInfo dst = getDestination(v);
    973         // If destination is already at top of stack, no need to pass it in
    974         if (dst.equals(mState.stack.peek())) {
    975             mClipper.copyFromClipData(
    976                     mState.stack,
    977                     clipData,
    978                     mInjector.dialogs::showFileOperationStatus);
    979         } else {
    980             mClipper.copyFromClipData(
    981                     dst,
    982                     mState.stack,
    983                     clipData,
    984                     mInjector.dialogs::showFileOperationStatus);
    985         }
    986         return true;
    987     }
    988 
    989     DocumentInfo getDestination(View v) {
    990         String id = getModelId(v);
    991         if (id != null) {
    992             Cursor dstCursor = mModel.getItem(id);
    993             if (dstCursor == null) {
    994                 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
    995                 return null;
    996             }
    997             return DocumentInfo.fromDirectoryCursor(dstCursor);
    998         }
    999 
   1000         if (v == mRecView) {
   1001             return mState.stack.peek();
   1002         }
   1003 
   1004         return null;
   1005     }
   1006 
   1007     @Override
   1008     public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
   1009         // Note: use exact comparison - this code is searching for views which are children of
   1010         // the RecyclerView instance in the UI.
   1011         if (v.getParent() == mRecView) {
   1012             DocumentHolder holder = getDocumentHolder(v);
   1013             if (holder != null) {
   1014                 if (!highlight) {
   1015                     holder.resetDropHighlight();
   1016                 } else {
   1017                     holder.setDroppableHighlight(
   1018                             DragAndDropHelper.canCopyTo(localState, getDestination(v)));
   1019                 }
   1020             }
   1021         }
   1022     }
   1023 
   1024     /**
   1025      * Gets the model ID for a given RecyclerView item.
   1026      * @param view A View that is a document item view, or a child of a document item view.
   1027      * @return The Model ID for the given document, or null if the given view is not associated with
   1028      *     a document item view.
   1029      */
   1030     protected @Nullable String getModelId(View view) {
   1031         View itemView = mRecView.findContainingItemView(view);
   1032         if (itemView != null) {
   1033             RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
   1034             if (vh instanceof DocumentHolder) {
   1035                 return ((DocumentHolder) vh).getModelId();
   1036             }
   1037         }
   1038         return null;
   1039     }
   1040 
   1041     private @Nullable DocumentHolder getDocumentHolder(View v) {
   1042         RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
   1043         if (vh instanceof DocumentHolder) {
   1044             return (DocumentHolder) vh;
   1045         }
   1046         return null;
   1047     }
   1048 
   1049     // TODO: Move to activities when Model becomes activity level object.
   1050     private boolean canSelect(DocumentDetails doc) {
   1051         return canSetSelectionState(doc.getModelId(), true);
   1052     }
   1053 
   1054     // TODO: Move to activities when Model becomes activity level object.
   1055     private boolean canSetSelectionState(String modelId, boolean nextState) {
   1056         if (nextState) {
   1057             // Check if an item can be selected
   1058             final Cursor cursor = mModel.getItem(modelId);
   1059             if (cursor == null) {
   1060                 Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId);
   1061                 return false;
   1062             }
   1063 
   1064             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
   1065             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
   1066             return mInjector.config.canSelectType(docMimeType, docFlags, mState);
   1067         } else {
   1068         final DocumentInfo parent = mState.stack.peek();
   1069             // Right now all selected items can be deselected.
   1070             return true;
   1071         }
   1072     }
   1073 
   1074     public static void showDirectory(
   1075             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
   1076         if (DEBUG) Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc));
   1077         create(fm, root, doc, anim);
   1078     }
   1079 
   1080     public static void showRecentsOpen(FragmentManager fm, int anim) {
   1081         create(fm, null, null, anim);
   1082     }
   1083 
   1084     public static void create(
   1085             FragmentManager fm,
   1086             RootInfo root,
   1087             @Nullable DocumentInfo doc,
   1088             @AnimationType int anim) {
   1089 
   1090         if (DEBUG) {
   1091             if (doc == null) {
   1092                 Log.d(TAG, "Creating new fragment null directory");
   1093             } else {
   1094                 Log.d(TAG, "Creating new fragment for directory: " + DocumentInfo.debugString(doc));
   1095             }
   1096         }
   1097 
   1098         final Bundle args = new Bundle();
   1099         args.putParcelable(Shared.EXTRA_ROOT, root);
   1100         args.putParcelable(Shared.EXTRA_DOC, doc);
   1101         args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
   1102 
   1103         final FragmentTransaction ft = fm.beginTransaction();
   1104         AnimationView.setupAnimations(ft, anim, args);
   1105 
   1106         final DirectoryFragment fragment = new DirectoryFragment();
   1107         fragment.setArguments(args);
   1108 
   1109         ft.replace(getFragmentId(), fragment);
   1110         ft.commitAllowingStateLoss();
   1111     }
   1112 
   1113     public static @Nullable DirectoryFragment get(FragmentManager fm) {
   1114         // TODO: deal with multiple directories shown at once
   1115         Fragment fragment = fm.findFragmentById(getFragmentId());
   1116         return fragment instanceof DirectoryFragment
   1117                 ? (DirectoryFragment) fragment
   1118                 : null;
   1119     }
   1120 
   1121     private static int getFragmentId() {
   1122         return R.id.container_directory;
   1123     }
   1124 
   1125     @Override
   1126     public void onRefresh() {
   1127         // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it
   1128         // should be covered by last modified value we store in thumbnail cache, but rather to give
   1129         // the user a greater sense that contents are being reloaded.
   1130         ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext());
   1131         String[] ids = mModel.getModelIds();
   1132         int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT);
   1133         for (int i = 0; i < numOfEvicts; ++i) {
   1134             cache.removeUri(mModel.getItemUri(ids[i]));
   1135         }
   1136 
   1137         final DocumentInfo doc = mState.stack.peek();
   1138         mActions.refreshDocument(doc, (boolean refreshSupported) -> {
   1139             if (refreshSupported) {
   1140                 mRefreshLayout.setRefreshing(false);
   1141             } else {
   1142                 // If Refresh API isn't available, we will explicitly reload the loader
   1143                 mActions.loadDocumentsForCurrentStack();
   1144             }
   1145         });
   1146     }
   1147 
   1148     private final class ModelUpdateListener implements EventListener<Model.Update> {
   1149 
   1150         @Override
   1151         public void accept(Model.Update update) {
   1152             if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
   1153 
   1154             mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
   1155 
   1156             updateLayout(mState.derivedMode);
   1157 
   1158             mAdapter.notifyDataSetChanged();
   1159 
   1160             if (mRestoredSelection != null) {
   1161                 mSelectionMgr.restoreSelection(mRestoredSelection);
   1162                 // Note, we'll take care of cleaning up retained selection
   1163                 // in the selection handler where we already have some
   1164                 // specialized code to handle when selection was restored.
   1165             }
   1166 
   1167             // Restore any previous instance state
   1168             final SparseArray<Parcelable> container =
   1169                     mState.dirConfigs.remove(mLocalState.getConfigKey());
   1170             final int curSortedDimensionId = mState.sortModel.getSortedDimensionId();
   1171 
   1172             final SortDimension curSortedDimension =
   1173                     mState.sortModel.getDimensionById(curSortedDimensionId);
   1174             if (container != null
   1175                     && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
   1176                 getView().restoreHierarchyState(container);
   1177             } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
   1178                     || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
   1179                     || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
   1180                 // Scroll to the top if the sort order actually changed.
   1181                 mRecView.smoothScrollToPosition(0);
   1182             }
   1183 
   1184             mLocalState.mLastSortDimensionId = curSortedDimension.getId();
   1185             mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
   1186 
   1187             if (mRefreshLayout.isRefreshing()) {
   1188                 new Handler().postDelayed(
   1189                         () -> mRefreshLayout.setRefreshing(false),
   1190                         REFRESH_SPINNER_TIMEOUT);
   1191             }
   1192 
   1193             if (!mModel.isLoading()) {
   1194                 mActivity.notifyDirectoryLoaded(
   1195                         mModel.doc != null ? mModel.doc.derivedUri : null);
   1196             }
   1197         }
   1198     }
   1199 
   1200     private final class AdapterEnvironment implements DocumentsAdapter.Environment {
   1201 
   1202         @Override
   1203         public Features getFeatures() {
   1204             return mInjector.features;
   1205         }
   1206 
   1207         @Override
   1208         public Context getContext() {
   1209             return mActivity;
   1210         }
   1211 
   1212         @Override
   1213         public State getDisplayState() {
   1214             return mState;
   1215         }
   1216 
   1217         @Override
   1218         public boolean isInSearchMode() {
   1219             return mInjector.searchManager.isSearching();
   1220         }
   1221 
   1222         @Override
   1223         public Model getModel() {
   1224             return mModel;
   1225         }
   1226 
   1227         @Override
   1228         public int getColumnCount() {
   1229             return mColumnCount;
   1230         }
   1231 
   1232         @Override
   1233         public boolean isSelected(String id) {
   1234             return mSelectionMgr.getSelection().contains(id);
   1235         }
   1236 
   1237         @Override
   1238         public boolean isDocumentEnabled(String mimeType, int flags) {
   1239             return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
   1240         }
   1241 
   1242         @Override
   1243         public void initDocumentHolder(DocumentHolder holder) {
   1244             holder.addKeyEventListener(mInputHandler);
   1245             holder.itemView.setOnFocusChangeListener(mFocusManager);
   1246         }
   1247 
   1248         @Override
   1249         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
   1250             setupDragAndDropOnDocumentView(holder.itemView, cursor);
   1251         }
   1252 
   1253         @Override
   1254         public ActionHandler getActionHandler() {
   1255             return mActions;
   1256         }
   1257     }
   1258 }
   1259