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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onDestroyView()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
onActivityCreated(Bundle savedInstanceState)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
onStart()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
onStop()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 
retainState(RetainedState state)429     public void retainState(RetainedState state) {
430         state.selection = mSelectionMgr.getSelection(new Selection());
431     }
432 
433     @Override
onSaveInstanceState(Bundle outState)434     public void onSaveInstanceState(Bundle outState) {
435         super.onSaveInstanceState(outState);
436 
437         mLocalState.save(outState);
438     }
439 
440     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)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
onContextItemSelected(MenuItem item)459     public boolean onContextItemSelected(MenuItem item) {
460         return handleMenuItemClick(item);
461     }
462 
handleCopyResult(int resultCode, Intent data)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 
onContextMenuClick(InputEvent e)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 
onViewModeChanged()501     public void onViewModeChanged() {
502         // Mode change is just visual change; no need to kick loader.
503         onDisplayStateChanged();
504     }
505 
onDisplayStateChanged()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      */
updateLayout(@iewMode int mode)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      */
scaleLayout(float scale)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 
calculateColumnCount(@iewMode int mode)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      */
getFraction(@ractionRes int id)585     private float getFraction(@FractionRes int id) {
586         return getResources().getFraction(id, 1, 0);
587     }
588 
getScaledSize(@imenRes int id)589     private int getScaledSize(@DimenRes int id) {
590         return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
591     }
592 
getDirectoryPadding(@iewMode int mode)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 
handleMenuItemClick(MenuItem item)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 
onAccessibilityClick(View child)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 
cancelThumbnailTask(View view)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.
openDocuments(final Selection selected)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 
showChooserForDoc(final Selection selected)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 
transferDocuments(final Selection selected, @Nullable DocumentStack destination, final @OpType int mode)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
onActivityResult(@equestCode int requestCode, int resultCode, Intent data)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 
hasDirectory(List<DocumentInfo> docs)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 
renameDocuments(Selection selected)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 
getModel()857     Model getModel(){
858         return mModel;
859     }
860 
isDocumentEnabled(String mimeType, int flags)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      */
pasteFromClipboard()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 
pasteIntoFolder()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 
setupDragAndDropOnDocumentView(View view, Cursor cursor)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 
dragStopped(boolean result)904     void dragStopped(boolean result) {
905         if (result) {
906             mSelectionMgr.clearSelection();
907         }
908     }
909 
910     @Override
runOnUiThread(Runnable runnable)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
onDragEntered(View v, Object localState)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
onDragExited(View v, Object localState)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
onViewHovered(View view)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 
handleDropEvent(View v, DragEvent event)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 
getDestination(View v)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
setDropTargetHighlight(View v, Object localState, boolean highlight)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      */
getModelId(View view)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 
getDocumentHolder(View v)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.
canSelect(DocumentDetails doc)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.
canSetSelectionState(String modelId, boolean nextState)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 
showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim)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 
showRecentsOpen(FragmentManager fm, int anim)1080     public static void showRecentsOpen(FragmentManager fm, int anim) {
1081         create(fm, null, null, anim);
1082     }
1083 
create( FragmentManager fm, RootInfo root, @Nullable DocumentInfo doc, @AnimationType int anim)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 
get(FragmentManager fm)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 
getFragmentId()1121     private static int getFragmentId() {
1122         return R.id.container_directory;
1123     }
1124 
1125     @Override
onRefresh()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
accept(Model.Update update)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
getFeatures()1203         public Features getFeatures() {
1204             return mInjector.features;
1205         }
1206 
1207         @Override
getContext()1208         public Context getContext() {
1209             return mActivity;
1210         }
1211 
1212         @Override
getDisplayState()1213         public State getDisplayState() {
1214             return mState;
1215         }
1216 
1217         @Override
isInSearchMode()1218         public boolean isInSearchMode() {
1219             return mInjector.searchManager.isSearching();
1220         }
1221 
1222         @Override
getModel()1223         public Model getModel() {
1224             return mModel;
1225         }
1226 
1227         @Override
getColumnCount()1228         public int getColumnCount() {
1229             return mColumnCount;
1230         }
1231 
1232         @Override
isSelected(String id)1233         public boolean isSelected(String id) {
1234             return mSelectionMgr.getSelection().contains(id);
1235         }
1236 
1237         @Override
isDocumentEnabled(String mimeType, int flags)1238         public boolean isDocumentEnabled(String mimeType, int flags) {
1239             return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
1240         }
1241 
1242         @Override
initDocumentHolder(DocumentHolder holder)1243         public void initDocumentHolder(DocumentHolder holder) {
1244             holder.addKeyEventListener(mInputHandler);
1245             holder.itemView.setOnFocusChangeListener(mFocusManager);
1246         }
1247 
1248         @Override
onBindDocumentHolder(DocumentHolder holder, Cursor cursor)1249         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
1250             setupDragAndDropOnDocumentView(holder.itemView, cursor);
1251         }
1252 
1253         @Override
getActionHandler()1254         public ActionHandler getActionHandler() {
1255             return mActions;
1256         }
1257     }
1258 }
1259