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