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