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