• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                             .build();
362             mInjector.updateSharedSelectionTracker(localTracker);
363         }
364 
365         mSelectionMgr.addObserver(mSelectionMetadata);
366 
367         // Construction of the input handlers is non trivial, so to keep logic clear,
368         // and code flexible, and DirectoryFragment small, the construction has been
369         // moved off into a separate class.
370         InputHandlers handlers = new InputHandlers(
371                 mActions,
372                 mSelectionMgr,
373                 selectionPredicate,
374                 mFocusManager,
375                 mRecView);
376 
377         // This little guy gets added to each Holder, so that we can be notified of key events
378         // on RecyclerView items.
379         mKeyListener = handlers.createKeyHandler();
380 
381         if (DEBUG) {
382             new ScaleHelper(this.getContext(), mInjector.features, this::scaleLayout)
383                     .attach(mRecView);
384         }
385 
386         new RefreshHelper(mRefreshLayout::setEnabled)
387                 .attach(mRecView);
388 
389         mActionModeController = mInjector.getActionModeController(
390                 mSelectionMetadata,
391                 this::handleMenuItemClick);
392 
393         mSelectionMgr.addObserver(mActionModeController);
394 
395         final ActivityManager am = (ActivityManager) mActivity.getSystemService(
396                 Context.ACTIVITY_SERVICE);
397         boolean svelte = am.isLowRamDevice() && (mState.stack.isRecents());
398         mIconHelper.setThumbnailsEnabled(!svelte);
399 
400         // If mDocument is null, we sort it by last modified by default because it's in Recents.
401         final boolean prefersLastModified =
402                 (mLocalState.mDocument == null)
403                 || mLocalState.mDocument.prefersSortByLastModified();
404         // Call this before adding the listener to avoid restarting the loader one more time
405         mState.sortModel.setDefaultDimension(
406                 prefersLastModified
407                         ? SortModel.SORT_DIMENSION_ID_DATE
408                         : SortModel.SORT_DIMENSION_ID_TITLE);
409 
410         // Kick off loader at least once
411         mActions.loadDocumentsForCurrentStack();
412     }
413 
414     @Override
onStart()415     public void onStart() {
416         super.onStart();
417 
418         // Add listener to update contents on sort model change
419         mState.sortModel.addListener(mSortListener);
420     }
421 
422     @Override
onStop()423     public void onStop() {
424         super.onStop();
425 
426         mState.sortModel.removeListener(mSortListener);
427 
428         // Remember last scroll location
429         final SparseArray<Parcelable> container = new SparseArray<>();
430         getView().saveHierarchyState(container);
431         mState.dirConfigs.put(mLocalState.getConfigKey(), container);
432     }
433 
434     @Override
onSaveInstanceState(Bundle outState)435     public void onSaveInstanceState(Bundle outState) {
436         super.onSaveInstanceState(outState);
437 
438         mLocalState.save(outState);
439         mSelectionMgr.onSaveInstanceState(outState);
440     }
441 
442     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)443     public void onCreateContextMenu(ContextMenu menu,
444             View v,
445             ContextMenu.ContextMenuInfo menuInfo) {
446         super.onCreateContextMenu(menu, v, menuInfo);
447         final MenuInflater inflater = getActivity().getMenuInflater();
448 
449         final String modelId = getModelId(v);
450         if (modelId == null) {
451             // TODO: inject DirectoryDetails into MenuManager constructor
452             // Since both classes are supplied by Activity and created
453             // at the same time.
454             mInjector.menuManager.inflateContextMenuForContainer(menu, inflater);
455         } else {
456             mInjector.menuManager.inflateContextMenuForDocs(
457                     menu, inflater, mSelectionMetadata);
458         }
459     }
460 
461     @Override
onContextItemSelected(MenuItem item)462     public boolean onContextItemSelected(MenuItem item) {
463         return handleMenuItemClick(item);
464     }
465 
onCopyDestinationPicked(int resultCode, Intent data)466     private void onCopyDestinationPicked(int resultCode, Intent data) {
467 
468         FileOperation operation = mLocalState.claimPendingOperation();
469 
470         if (resultCode == FragmentActivity.RESULT_CANCELED || data == null) {
471             // User pressed the back button or otherwise cancelled the destination pick. Don't
472             // proceed with the copy.
473             operation.dispose();
474             return;
475         }
476 
477         operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
478         final String jobId = FileOperations.createJobId();
479         mInjector.dialogs.showProgressDialog(jobId, operation);
480         FileOperations.start(
481                 mActivity,
482                 operation,
483                 mInjector.dialogs::showFileOperationStatus,
484                 jobId);
485     }
486 
487     // TODO: Move to UserInputHander.
onContextMenuClick(MotionEvent e)488     protected boolean onContextMenuClick(MotionEvent e) {
489 
490         if (mDetailsLookup.overItemWithSelectionKey(e)) {
491             View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
492             ViewHolder holder = mRecView.getChildViewHolder(childView);
493 
494             View view = holder.itemView;
495             float x = e.getX() - view.getLeft();
496             float y = e.getY() - view.getTop();
497             mInjector.menuManager.showContextMenu(this, view, x, y);
498             return true;
499         }
500 
501         mInjector.menuManager.showContextMenu(this, mRecView, e.getX(), e.getY());
502         return true;
503     }
504 
onItemActivated(ItemDetails<String> item, MotionEvent e)505     private boolean onItemActivated(ItemDetails<String> item, MotionEvent e) {
506         if (((DocumentItemDetails) item).inPreviewIconHotspot(e)) {
507             return mActions.previewItem(item);
508         }
509 
510         return mActions.openItem(
511                 item,
512                 ActionHandler.VIEW_TYPE_PREVIEW,
513                 ActionHandler.VIEW_TYPE_REGULAR);
514     }
515 
onViewModeChanged()516     public void onViewModeChanged() {
517         // Mode change is just visual change; no need to kick loader.
518         onDisplayStateChanged();
519     }
520 
onDisplayStateChanged()521     private void onDisplayStateChanged() {
522         updateLayout(mState.derivedMode);
523         mRecView.setAdapter(mAdapter);
524     }
525 
526     /**
527      * Updates the layout after the view mode switches.
528      * @param mode The new view mode.
529      */
updateLayout(@iewMode int mode)530     private void updateLayout(@ViewMode int mode) {
531         mMode = mode;
532         mColumnCount = calculateColumnCount(mode);
533         if (mLayout != null) {
534             mLayout.setSpanCount(mColumnCount);
535         }
536 
537         int pad = getDirectoryPadding(mode);
538         mAppBarHeight = getAppBarLayoutHeight();
539         mRecView.setPadding(pad, mAppBarHeight, pad, getSaveLayoutHeight());
540         mRecView.requestLayout();
541         mIconHelper.setViewMode(mode);
542 
543         int range = getResources().getDimensionPixelOffset(R.dimen.refresh_icon_range);
544         mRefreshLayout.setProgressViewOffset(true, mAppBarHeight, mAppBarHeight + range);
545     }
546 
getAppBarLayoutHeight()547     private int getAppBarLayoutHeight() {
548         View appBarLayout = getActivity().findViewById(R.id.app_bar);
549         View collapsingBar = getActivity().findViewById(R.id.collapsing_toolbar);
550         return collapsingBar == null ? 0 : appBarLayout.getHeight();
551     }
552 
getSaveLayoutHeight()553     private int getSaveLayoutHeight() {
554         View containerSave = getActivity().findViewById(R.id.container_save);
555         return containerSave == null ? 0 : containerSave.getHeight();
556     }
557 
558     /**
559      * Updates the layout after the view mode switches.
560      * @param mode The new view mode.
561      */
scaleLayout(float scale)562     private void scaleLayout(float scale) {
563         assert DEBUG;
564 
565         if (VERBOSE) Log.v(
566                 TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale);
567 
568         if (mMode == MODE_GRID) {
569             float minScale = getFraction(R.fraction.grid_scale_min);
570             float maxScale = getFraction(R.fraction.grid_scale_max);
571             float nextScale = mLiveScale * scale;
572 
573             if (VERBOSE) Log.v(TAG,
574                     "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale);
575 
576             if (nextScale > minScale && nextScale < maxScale) {
577                 if (DEBUG) {
578                     Log.d(TAG, "Updating grid scale: " + scale);
579                 }
580                 mLiveScale = nextScale;
581                 updateLayout(mMode);
582             }
583 
584         } else {
585             if (DEBUG) {
586                 Log.d(TAG, "List mode, ignoring scale: " + scale);
587             }
588             mLiveScale = 1.0f;
589         }
590     }
591 
calculateColumnCount(@iewMode int mode)592     private int calculateColumnCount(@ViewMode int mode) {
593         if (mode == MODE_LIST) {
594             // List mode is a "grid" with 1 column.
595             return 1;
596         }
597 
598         int cellWidth = getScaledSize(R.dimen.grid_width);
599         int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin);
600         int viewPadding =
601                 (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale);
602 
603         // RecyclerView sometimes gets a width of 0 (see b/27150284).
604         // Clamp so that we always lay out the grid with at least 2 columns by default.
605         // If on photo picking state, the UI should show 3 images a row or 2 folders a row,
606         // so use 6 columns by default and set folder size to 3 and document size is to 2.
607         mColumnUnit = mState.isPhotoPicking() ? 3 : 1;
608         int columnCount = mColumnUnit * Math.max(2,
609                 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
610 
611         // Finally with our grid count logic firmly in place, we apply any live scaling
612         // captured by the scale gesture detector.
613         return Math.max(1, Math.round(columnCount / mLiveScale));
614     }
615 
616 
617     /**
618      * Moderately abuse the "fraction" resource type for our purposes.
619      */
getFraction(@ractionRes int id)620     private float getFraction(@FractionRes int id) {
621         return getResources().getFraction(id, 1, 0);
622     }
623 
getScaledSize(@imenRes int id)624     private int getScaledSize(@DimenRes int id) {
625         return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
626     }
627 
getDirectoryPadding(@iewMode int mode)628     private int getDirectoryPadding(@ViewMode int mode) {
629         switch (mode) {
630             case MODE_GRID:
631                 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
632             case MODE_LIST:
633                 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
634             default:
635                 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
636         }
637     }
638 
handleMenuItemClick(MenuItem item)639     private boolean handleMenuItemClick(MenuItem item) {
640         if (mInjector.pickResult != null) {
641             mInjector.pickResult.increaseActionCount();
642         }
643         MutableSelection<String> selection = new MutableSelection<>();
644         mSelectionMgr.copySelection(selection);
645 
646         switch (item.getItemId()) {
647             case R.id.action_menu_select:
648             case R.id.dir_menu_open:
649                 openDocuments(selection);
650                 mActionModeController.finishActionMode();
651                 return true;
652 
653             case R.id.action_menu_open_with:
654             case R.id.dir_menu_open_with:
655                 showChooserForDoc(selection);
656                 return true;
657 
658             case R.id.dir_menu_open_in_new_window:
659                 mActions.openSelectedInNewWindow();
660                 return true;
661 
662             case R.id.action_menu_share:
663             case R.id.dir_menu_share:
664                 mActions.shareSelectedDocuments();
665                 return true;
666 
667             case R.id.action_menu_delete:
668             case R.id.dir_menu_delete:
669                 // deleteDocuments will end action mode if the documents are deleted.
670                 // It won't end action mode if user cancels the delete.
671                 mActions.deleteSelectedDocuments();
672                 return true;
673 
674             case R.id.action_menu_copy_to:
675                 transferDocuments(selection, null, FileOperationService.OPERATION_COPY);
676                 // TODO: Only finish selection mode if copy-to is not canceled.
677                 // Need to plum down into handling the way we do with deleteDocuments.
678                 mActionModeController.finishActionMode();
679                 return true;
680 
681             case R.id.action_menu_compress:
682                 transferDocuments(selection, mState.stack,
683                         FileOperationService.OPERATION_COMPRESS);
684                 // TODO: Only finish selection mode if compress is not canceled.
685                 // Need to plum down into handling the way we do with deleteDocuments.
686                 mActionModeController.finishActionMode();
687                 return true;
688 
689             // TODO: Implement extract (to the current directory).
690             case R.id.action_menu_extract_to:
691                 transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT);
692                 // TODO: Only finish selection mode if compress-to is not canceled.
693                 // Need to plum down into handling the way we do with deleteDocuments.
694                 mActionModeController.finishActionMode();
695                 return true;
696 
697             case R.id.action_menu_move_to:
698                 if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
699                     mInjector.dialogs.showOperationUnsupported();
700                     return true;
701                 }
702                 // Exit selection mode first, so we avoid deselecting deleted documents.
703                 mActionModeController.finishActionMode();
704                 transferDocuments(selection, null, FileOperationService.OPERATION_MOVE);
705                 return true;
706 
707             case R.id.action_menu_inspect:
708             case R.id.dir_menu_inspect:
709                 mActionModeController.finishActionMode();
710                 assert selection.size() <= 1;
711                 DocumentInfo doc = selection.isEmpty()
712                         ? mActivity.getCurrentDirectory()
713                         : mModel.getDocuments(selection).get(0);
714 
715                         mActions.showInspector(doc);
716                 return true;
717 
718             case R.id.dir_menu_cut_to_clipboard:
719                 mActions.cutToClipboard();
720                 return true;
721 
722             case R.id.dir_menu_copy_to_clipboard:
723                 mActions.copyToClipboard();
724                 return true;
725 
726             case R.id.dir_menu_paste_from_clipboard:
727                 pasteFromClipboard();
728                 return true;
729 
730             case R.id.dir_menu_paste_into_folder:
731                 pasteIntoFolder();
732                 return true;
733 
734             case R.id.action_menu_select_all:
735             case R.id.dir_menu_select_all:
736                 mActions.selectAllFiles();
737                 return true;
738 
739             case R.id.action_menu_rename:
740             case R.id.dir_menu_rename:
741                 // Exit selection mode first, so we avoid deselecting deleted
742                 // (renamed) documents.
743                 mActionModeController.finishActionMode();
744                 renameDocuments(selection);
745                 return true;
746 
747             case R.id.dir_menu_create_dir:
748                 mActions.showCreateDirectoryDialog();
749                 return true;
750 
751             case R.id.dir_menu_view_in_owner:
752                 mActions.viewInOwner();
753                 return true;
754 
755             case R.id.action_menu_sort:
756                 mActions.showSortDialog();
757                 return true;
758 
759             default:
760                 if (DEBUG) {
761                     Log.d(TAG, "Unhandled menu item selected: " + item);
762                 }
763                 return false;
764         }
765     }
766 
onAccessibilityClick(View child)767     private boolean onAccessibilityClick(View child) {
768         if (mSelectionMgr.hasSelection()) {
769             selectItem(child);
770         } else {
771             DocumentHolder holder = getDocumentHolder(child);
772             mActions.openItem(holder.getItemDetails(), ActionHandler.VIEW_TYPE_PREVIEW,
773                 ActionHandler.VIEW_TYPE_REGULAR);
774         }
775         return true;
776     }
777 
onAccessibilityLongClick(View child)778     private boolean onAccessibilityLongClick(View child) {
779         selectItem(child);
780         return true;
781     }
782 
selectItem(View child)783     private void selectItem(View child) {
784         final String id = getModelId(child);
785         if (mSelectionMgr.isSelected(id)) {
786             mSelectionMgr.deselect(id);
787         } else {
788             mSelectionMgr.select(id);
789         }
790     }
791 
cancelThumbnailTask(View view)792     private void cancelThumbnailTask(View view) {
793         final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
794         if (iconThumb != null) {
795             mIconHelper.stopLoading(iconThumb);
796         }
797     }
798 
799     // Support for opening multiple documents is currently exclusive to DocumentsActivity.
openDocuments(final Selection selected)800     private void openDocuments(final Selection selected) {
801         Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN);
802 
803         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
804         List<DocumentInfo> docs = mModel.getDocuments(selected);
805         if (docs.size() > 1) {
806             mActivity.onDocumentsPicked(docs);
807         } else {
808             mActivity.onDocumentPicked(docs.get(0));
809         }
810     }
811 
showChooserForDoc(final Selection<String> selected)812     private void showChooserForDoc(final Selection<String> selected) {
813         Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN);
814 
815         assert selected.size() == 1;
816         DocumentInfo doc =
817                 DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
818         mActions.showChooserForDoc(doc);
819     }
820 
transferDocuments( final Selection<String> selected, @Nullable DocumentStack destination, final @OpType int mode)821     private void transferDocuments(
822             final Selection<String> selected, @Nullable DocumentStack destination,
823             final @OpType int mode) {
824         switch (mode) {
825             case FileOperationService.OPERATION_COPY:
826                 Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_TO);
827                 break;
828             case FileOperationService.OPERATION_COMPRESS:
829                 Metrics.logUserAction(MetricConsts.USER_ACTION_COMPRESS);
830                 break;
831             case FileOperationService.OPERATION_EXTRACT:
832                 Metrics.logUserAction(MetricConsts.USER_ACTION_EXTRACT_TO);
833                 break;
834             case FileOperationService.OPERATION_MOVE:
835                 Metrics.logUserAction(MetricConsts.USER_ACTION_MOVE_TO);
836                 break;
837         }
838 
839         UrisSupplier srcs;
840         try {
841             ClipStore clipStorage = DocumentsApplication.getClipStore(getContext());
842             srcs = UrisSupplier.create(selected, mModel::getItemUri, clipStorage);
843         } catch (IOException e) {
844             throw new RuntimeException("Failed to create uri supplier.", e);
845         }
846 
847         final DocumentInfo parent = mActivity.getCurrentDirectory();
848         final FileOperation operation = new FileOperation.Builder()
849                 .withOpType(mode)
850                 .withSrcParent(parent == null ? null : parent.derivedUri)
851                 .withSrcs(srcs)
852                 .build();
853 
854         if (destination != null) {
855             operation.setDestination(destination);
856             final String jobId = FileOperations.createJobId();
857             mInjector.dialogs.showProgressDialog(jobId, operation);
858             FileOperations.start(
859                     mActivity,
860                     operation,
861                     mInjector.dialogs::showFileOperationStatus,
862                     jobId);
863             return;
864         }
865 
866         // Pop up a dialog to pick a destination.  This is inadequate but works for now.
867         // TODO: Implement a picker that is to spec.
868         mLocalState.mPendingOperation = operation;
869         final Intent intent = new Intent(
870                 Shared.ACTION_PICK_COPY_DESTINATION,
871                 Uri.EMPTY,
872                 getActivity(),
873                 PickActivity.class);
874 
875         // Set an appropriate title on the drawer when it is shown in the picker.
876         // Coupled with the fact that we auto-open the drawer for copy/move operations
877         // it should basically be the thing people see first.
878         int drawerTitleId;
879         switch (mode) {
880             case FileOperationService.OPERATION_COPY:
881                 drawerTitleId = R.string.menu_copy;
882                 break;
883             case FileOperationService.OPERATION_COMPRESS:
884                 drawerTitleId = R.string.menu_compress;
885                 break;
886             case FileOperationService.OPERATION_EXTRACT:
887                 drawerTitleId = R.string.menu_extract;
888                 break;
889             case FileOperationService.OPERATION_MOVE:
890                 drawerTitleId = R.string.menu_move;
891                 break;
892             default:
893                 throw new UnsupportedOperationException("Unknown mode: " + mode);
894         }
895 
896         intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
897 
898         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
899         List<DocumentInfo> docs = mModel.getDocuments(selected);
900 
901         // Determine if there is a directory in the set of documents
902         // to be copied? Why? Directory creation isn't supported by some roots
903         // (like Downloads). This informs DocumentsActivity (the "picker")
904         // to restrict available roots to just those with support.
905         intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
906         intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode);
907 
908         // This just identifies the type of request...we'll check it
909         // when we reveive a response.
910         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
911     }
912 
913     @Override
onActivityResult(@equestCode int requestCode, int resultCode, Intent data)914     public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
915         switch (requestCode) {
916             case REQUEST_COPY_DESTINATION:
917                 onCopyDestinationPicked(resultCode, data);
918                 break;
919             default:
920                 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
921         }
922     }
923 
hasDirectory(List<DocumentInfo> docs)924     private static boolean hasDirectory(List<DocumentInfo> docs) {
925         for (DocumentInfo info : docs) {
926             if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
927                 return true;
928             }
929         }
930         return false;
931     }
932 
renameDocuments(Selection selected)933     private void renameDocuments(Selection selected) {
934         Metrics.logUserAction(MetricConsts.USER_ACTION_RENAME);
935 
936         // Batch renaming not supported
937         // Rename option is only available in menu when 1 document selected
938         assert selected.size() == 1;
939 
940         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
941         List<DocumentInfo> docs = mModel.getDocuments(selected);
942         RenameDocumentFragment.show(getChildFragmentManager(), docs.get(0));
943     }
944 
getModel()945     Model getModel(){
946         return mModel;
947     }
948 
949     /**
950      * Paste selection files from the primary clip into the current window.
951      */
pasteFromClipboard()952     public void pasteFromClipboard() {
953         Metrics.logUserAction(MetricConsts.USER_ACTION_PASTE_CLIPBOARD);
954         // Since we are pasting into the current window, we already have the destination in the
955         // stack. No need for a destination DocumentInfo.
956         mClipper.copyFromClipboard(
957                 mState.stack,
958                 mInjector.dialogs::showFileOperationStatus);
959         getActivity().invalidateOptionsMenu();
960     }
961 
pasteIntoFolder()962     public void pasteIntoFolder() {
963         assert (mSelectionMgr.getSelection().size() == 1);
964 
965         String modelId = mSelectionMgr.getSelection().iterator().next();
966         Cursor dstCursor = mModel.getItem(modelId);
967         if (dstCursor == null) {
968             Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
969             return;
970         }
971         DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
972         mClipper.copyFromClipboard(
973                 destination,
974                 mState.stack,
975                 mInjector.dialogs::showFileOperationStatus);
976         getActivity().invalidateOptionsMenu();
977     }
978 
setupDragAndDropOnDocumentView(View view, Cursor cursor)979     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
980         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
981         if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
982             // Make a directory item a drop target. Drop on non-directories and empty space
983             // is handled at the list/grid view level.
984             view.setOnDragListener(mDragHoverListener);
985         }
986     }
987 
getDestination(View v)988     private DocumentInfo getDestination(View v) {
989         String id = getModelId(v);
990         if (id != null) {
991             Cursor dstCursor = mModel.getItem(id);
992             if (dstCursor == null) {
993                 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
994                 return null;
995             }
996             return DocumentInfo.fromDirectoryCursor(dstCursor);
997         }
998 
999         if (v == mRecView) {
1000             return mActivity.getCurrentDirectory();
1001         }
1002 
1003         return null;
1004     }
1005 
1006     /**
1007      * Gets the model ID for a given RecyclerView item.
1008      * @param view A View that is a document item view, or a child of a document item view.
1009      * @return The Model ID for the given document, or null if the given view is not associated with
1010      *     a document item view.
1011      */
getModelId(View view)1012     private @Nullable String getModelId(View view) {
1013         View itemView = mRecView.findContainingItemView(view);
1014         if (itemView != null) {
1015             RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1016             if (vh instanceof DocumentHolder) {
1017                 return ((DocumentHolder) vh).getModelId();
1018             }
1019         }
1020         return null;
1021     }
1022 
getDocumentHolder(View v)1023     private @Nullable DocumentHolder getDocumentHolder(View v) {
1024         RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1025         if (vh instanceof DocumentHolder) {
1026             return (DocumentHolder) vh;
1027         }
1028         return null;
1029     }
1030 
setPreDrawListener(boolean enable)1031     private void setPreDrawListener(boolean enable) {
1032         if (mActivity == null) {
1033             return;
1034         }
1035 
1036         final View bar = mActivity.findViewById(R.id.collapsing_toolbar);
1037         if (bar != null) {
1038             if (enable) {
1039                 bar.getViewTreeObserver().addOnPreDrawListener(mToolbarPreDrawListener);
1040             } else {
1041                 bar.getViewTreeObserver().removeOnPreDrawListener(mToolbarPreDrawListener);
1042             }
1043         }
1044     }
1045 
showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim)1046     public static void showDirectory(
1047             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1048         if (DEBUG) {
1049             Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc));
1050         }
1051         create(fm, root, doc, anim);
1052     }
1053 
showRecentsOpen(FragmentManager fm, int anim)1054     public static void showRecentsOpen(FragmentManager fm, int anim) {
1055         create(fm, null, null, anim);
1056     }
1057 
create( FragmentManager fm, RootInfo root, @Nullable DocumentInfo doc, @AnimationType int anim)1058     public static void create(
1059             FragmentManager fm,
1060             RootInfo root,
1061             @Nullable DocumentInfo doc,
1062             @AnimationType int anim) {
1063 
1064         if (DEBUG) {
1065             if (doc == null) {
1066                 Log.d(TAG, "Creating new fragment null directory");
1067             } else {
1068                 Log.d(TAG, "Creating new fragment for directory: " + DocumentInfo.debugString(doc));
1069             }
1070         }
1071 
1072         final Bundle args = new Bundle();
1073         args.putParcelable(Shared.EXTRA_ROOT, root);
1074         args.putParcelable(Shared.EXTRA_DOC, doc);
1075 
1076         final FragmentTransaction ft = fm.beginTransaction();
1077         AnimationView.setupAnimations(ft, anim, args);
1078 
1079         final DirectoryFragment fragment = new DirectoryFragment();
1080         fragment.setArguments(args);
1081 
1082         ft.replace(getFragmentId(), fragment);
1083         ft.commitAllowingStateLoss();
1084     }
1085 
get(FragmentManager fm)1086     public static @Nullable DirectoryFragment get(FragmentManager fm) {
1087         // TODO: deal with multiple directories shown at once
1088         Fragment fragment = fm.findFragmentById(getFragmentId());
1089         return fragment instanceof DirectoryFragment
1090                 ? (DirectoryFragment) fragment
1091                 : null;
1092     }
1093 
getFragmentId()1094     private static int getFragmentId() {
1095         return R.id.container_directory;
1096     }
1097 
1098     /**
1099      *  Scroll to top of recyclerView in fragment
1100      */
scrollToTop()1101     public void scrollToTop() {
1102         if (mRecView != null) {
1103             mRecView.scrollToPosition(0);
1104         }
1105     }
1106 
1107     /**
1108      *  Stop the scroll of recyclerView in fragment
1109      */
stopScroll()1110     public void stopScroll() {
1111         if (mRecView != null) {
1112             mRecView.stopScroll();
1113         }
1114     }
1115 
1116     @Override
onRefresh()1117     public void onRefresh() {
1118         // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it
1119         // should be covered by last modified value we store in thumbnail cache, but rather to give
1120         // the user a greater sense that contents are being reloaded.
1121         ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext());
1122         String[] ids = mModel.getModelIds();
1123         int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT);
1124         for (int i = 0; i < numOfEvicts; ++i) {
1125             cache.removeUri(mModel.getItemUri(ids[i]));
1126         }
1127 
1128         final DocumentInfo doc = mActivity.getCurrentDirectory();
1129         mActions.refreshDocument(doc, (boolean refreshSupported) -> {
1130             if (refreshSupported) {
1131                 mRefreshLayout.setRefreshing(false);
1132             } else {
1133                 // If Refresh API isn't available, we will explicitly reload the loader
1134                 mActions.loadDocumentsForCurrentStack();
1135             }
1136         });
1137     }
1138 
1139     private final class ModelUpdateListener implements EventListener<Model.Update> {
1140 
1141         @Override
accept(Model.Update update)1142         public void accept(Model.Update update) {
1143             if (DEBUG) {
1144                 Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
1145             }
1146 
1147             mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
1148 
1149             updateLayout(mState.derivedMode);
1150 
1151             // Update the selection to remove any disappeared IDs.
1152             Iterator<String> selectionIter = mSelectionMgr.getSelection().iterator();
1153             while (selectionIter.hasNext()) {
1154                 if (!mAdapter.getStableIds().contains(selectionIter.next())) {
1155                     selectionIter.remove();
1156                 }
1157             }
1158 
1159             mAdapter.notifyDataSetChanged();
1160 
1161             if (mRestoredState != null) {
1162                 mSelectionMgr.onRestoreInstanceState(mRestoredState);
1163                 mRestoredState = null;
1164             }
1165 
1166             // Restore any previous instance state
1167             final SparseArray<Parcelable> container =
1168                     mState.dirConfigs.remove(mLocalState.getConfigKey());
1169             final int curSortedDimensionId = mState.sortModel.getSortedDimensionId();
1170 
1171             final SortDimension curSortedDimension =
1172                     mState.sortModel.getDimensionById(curSortedDimensionId);
1173 
1174             // Default not restore to avoid app bar layout expand to confuse users.
1175             if (container != null
1176                     && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, true)) {
1177                 getView().restoreHierarchyState(container);
1178             } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
1179                     || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
1180                     || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
1181                 // Scroll to the top if the sort order actually changed.
1182                 mRecView.smoothScrollToPosition(0);
1183             }
1184 
1185             mLocalState.mLastSortDimensionId = curSortedDimension.getId();
1186             mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
1187 
1188             if (mRefreshLayout.isRefreshing()) {
1189                 new Handler().postDelayed(
1190                         () -> mRefreshLayout.setRefreshing(false),
1191                         REFRESH_SPINNER_TIMEOUT);
1192             }
1193 
1194             if (!mModel.isLoading()) {
1195                 mActivity.notifyDirectoryLoaded(
1196                         mModel.doc != null ? mModel.doc.derivedUri : null);
1197                 // For orientation changed case, sometimes the docs loading comes after the menu
1198                 // update. We need to update the menu here to ensure the status is correct.
1199                 mInjector.menuManager.updateModel(mModel);
1200                 mInjector.menuManager.updateOptionMenu();
1201 
1202                 mActivity.updateHeaderTitle();
1203 
1204                 setPreDrawListener(true);
1205             }
1206         }
1207     }
1208 
1209     private final class AdapterEnvironment implements DocumentsAdapter.Environment {
1210 
1211         @Override
getFeatures()1212         public Features getFeatures() {
1213             return mInjector.features;
1214         }
1215 
1216         @Override
getContext()1217         public Context getContext() {
1218             return mActivity;
1219         }
1220 
1221         @Override
getDisplayState()1222         public State getDisplayState() {
1223             return mState;
1224         }
1225 
1226         @Override
isInSearchMode()1227         public boolean isInSearchMode() {
1228             return mInjector.searchManager.isSearching();
1229         }
1230 
1231         @Override
getModel()1232         public Model getModel() {
1233             return mModel;
1234         }
1235 
1236         @Override
getColumnCount()1237         public int getColumnCount() {
1238             return mColumnCount;
1239         }
1240 
1241         @Override
isSelected(String id)1242         public boolean isSelected(String id) {
1243             return mSelectionMgr.isSelected(id);
1244         }
1245 
1246         @Override
isDocumentEnabled(String mimeType, int flags)1247         public boolean isDocumentEnabled(String mimeType, int flags) {
1248             return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
1249         }
1250 
1251         @Override
initDocumentHolder(DocumentHolder holder)1252         public void initDocumentHolder(DocumentHolder holder) {
1253             holder.addKeyEventListener(mKeyListener);
1254             holder.itemView.setOnFocusChangeListener(mFocusManager);
1255         }
1256 
1257         @Override
onBindDocumentHolder(DocumentHolder holder, Cursor cursor)1258         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
1259             setupDragAndDropOnDocumentView(holder.itemView, cursor);
1260         }
1261 
1262         @Override
getActionHandler()1263         public ActionHandler getActionHandler() {
1264             return mActions;
1265         }
1266     }
1267 }
1268