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.Shared.DEBUG;
     20 import static com.android.documentsui.Shared.MAX_DOCS_IN_INTENT;
     21 import static com.android.documentsui.State.MODE_GRID;
     22 import static com.android.documentsui.State.MODE_LIST;
     23 import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
     24 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
     25 import static com.android.documentsui.model.DocumentInfo.getCursorString;
     26 
     27 import android.annotation.IntDef;
     28 import android.annotation.StringRes;
     29 import android.app.Activity;
     30 import android.app.ActivityManager;
     31 import android.app.AlertDialog;
     32 import android.app.Fragment;
     33 import android.app.FragmentManager;
     34 import android.app.FragmentTransaction;
     35 import android.app.LoaderManager.LoaderCallbacks;
     36 import android.content.ClipData;
     37 import android.content.Context;
     38 import android.content.DialogInterface;
     39 import android.content.Intent;
     40 import android.content.Loader;
     41 import android.database.Cursor;
     42 import android.graphics.Canvas;
     43 import android.graphics.Point;
     44 import android.graphics.Rect;
     45 import android.graphics.drawable.Drawable;
     46 import android.net.Uri;
     47 import android.os.AsyncTask;
     48 import android.os.Bundle;
     49 import android.os.Parcel;
     50 import android.os.Parcelable;
     51 import android.provider.DocumentsContract;
     52 import android.provider.DocumentsContract.Document;
     53 import android.support.annotation.Nullable;
     54 import android.support.design.widget.Snackbar;
     55 import android.support.v13.view.DragStartHelper;
     56 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     57 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
     58 import android.support.v7.widget.GridLayoutManager;
     59 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
     60 import android.support.v7.widget.RecyclerView;
     61 import android.support.v7.widget.RecyclerView.OnItemTouchListener;
     62 import android.support.v7.widget.RecyclerView.Recycler;
     63 import android.support.v7.widget.RecyclerView.RecyclerListener;
     64 import android.support.v7.widget.RecyclerView.ViewHolder;
     65 import android.text.BidiFormatter;
     66 import android.text.TextUtils;
     67 import android.util.Log;
     68 import android.util.SparseArray;
     69 import android.view.ActionMode;
     70 import android.view.DragEvent;
     71 import android.view.GestureDetector;
     72 import android.view.HapticFeedbackConstants;
     73 import android.view.KeyEvent;
     74 import android.view.LayoutInflater;
     75 import android.view.Menu;
     76 import android.view.MenuItem;
     77 import android.view.MotionEvent;
     78 import android.view.View;
     79 import android.view.ViewGroup;
     80 import android.widget.ImageView;
     81 import android.widget.TextView;
     82 import android.widget.Toolbar;
     83 
     84 import com.android.documentsui.BaseActivity;
     85 import com.android.documentsui.DirectoryLoader;
     86 import com.android.documentsui.DirectoryResult;
     87 import com.android.documentsui.DocumentClipper;
     88 import com.android.documentsui.DocumentsActivity;
     89 import com.android.documentsui.DocumentsApplication;
     90 import com.android.documentsui.Events;
     91 import com.android.documentsui.Events.MotionInputEvent;
     92 import com.android.documentsui.Menus;
     93 import com.android.documentsui.MessageBar;
     94 import com.android.documentsui.Metrics;
     95 import com.android.documentsui.MimePredicate;
     96 import com.android.documentsui.R;
     97 import com.android.documentsui.RecentsLoader;
     98 import com.android.documentsui.RootsCache;
     99 import com.android.documentsui.Shared;
    100 import com.android.documentsui.Snackbars;
    101 import com.android.documentsui.State;
    102 import com.android.documentsui.State.ViewMode;
    103 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
    104 import com.android.documentsui.model.DocumentInfo;
    105 import com.android.documentsui.model.DocumentStack;
    106 import com.android.documentsui.model.RootInfo;
    107 import com.android.documentsui.services.FileOperationService;
    108 import com.android.documentsui.services.FileOperationService.OpType;
    109 import com.android.documentsui.services.FileOperations;
    110 
    111 import com.google.common.collect.Lists;
    112 
    113 import java.lang.annotation.Retention;
    114 import java.lang.annotation.RetentionPolicy;
    115 import java.util.ArrayList;
    116 import java.util.Collections;
    117 import java.util.HashSet;
    118 import java.util.List;
    119 import java.util.Objects;
    120 import java.util.Set;
    121 
    122 /**
    123  * Display the documents inside a single directory.
    124  */
    125 public class DirectoryFragment extends Fragment
    126         implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
    127 
    128     @IntDef(flag = true, value = {
    129             TYPE_NORMAL,
    130             TYPE_RECENT_OPEN
    131     })
    132     @Retention(RetentionPolicy.SOURCE)
    133     public @interface ResultType {}
    134     public static final int TYPE_NORMAL = 1;
    135     public static final int TYPE_RECENT_OPEN = 2;
    136 
    137     @IntDef(flag = true, value = {
    138             REQUEST_COPY_DESTINATION
    139     })
    140     @Retention(RetentionPolicy.SOURCE)
    141     public @interface RequestCode {}
    142     public static final int REQUEST_COPY_DESTINATION = 1;
    143 
    144     private static final String TAG = "DirectoryFragment";
    145     private static final int LOADER_ID = 42;
    146 
    147     private Model mModel;
    148     private MultiSelectManager mSelectionManager;
    149     private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
    150     private ItemEventListener mItemEventListener = new ItemEventListener();
    151     private FocusManager mFocusManager;
    152 
    153     private IconHelper mIconHelper;
    154 
    155     private View mEmptyView;
    156     private RecyclerView mRecView;
    157     private ListeningGestureDetector mGestureDetector;
    158 
    159     private String mStateKey;
    160 
    161     private int mLastSortOrder = SORT_ORDER_UNKNOWN;
    162     private DocumentsAdapter mAdapter;
    163     private FragmentTuner mTuner;
    164     private DocumentClipper mClipper;
    165     private GridLayoutManager mLayout;
    166     private int mColumnCount = 1;  // This will get updated when layout changes.
    167 
    168     private LayoutInflater mInflater;
    169     private MessageBar mMessageBar;
    170     private View mProgressBar;
    171 
    172     // Directory fragment state is defined by: root, document, query, type, selection
    173     private @ResultType int mType = TYPE_NORMAL;
    174     private RootInfo mRoot;
    175     private DocumentInfo mDocument;
    176     private String mQuery = null;
    177     // Save selection found during creation so it can be restored during directory loading.
    178     private Selection mSelection = null;
    179     private boolean mSearchMode = false;
    180     private @Nullable ActionMode mActionMode;
    181 
    182     @Override
    183     public View onCreateView(
    184             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    185         mInflater = inflater;
    186         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
    187 
    188         mMessageBar = MessageBar.create(getChildFragmentManager());
    189         mProgressBar = view.findViewById(R.id.progressbar);
    190         mEmptyView = view.findViewById(android.R.id.empty);
    191         mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
    192         mRecView.setRecyclerListener(
    193                 new RecyclerListener() {
    194                     @Override
    195                     public void onViewRecycled(ViewHolder holder) {
    196                         cancelThumbnailTask(holder.itemView);
    197                     }
    198                 });
    199 
    200         mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
    201 
    202         // Make the recycler and the empty views responsive to drop events.
    203         mRecView.setOnDragListener(mOnDragListener);
    204         mEmptyView.setOnDragListener(mOnDragListener);
    205 
    206         return view;
    207     }
    208 
    209     @Override
    210     public void onDestroyView() {
    211         mSelectionManager.clearSelection();
    212 
    213         // Cancel any outstanding thumbnail requests
    214         final int count = mRecView.getChildCount();
    215         for (int i = 0; i < count; i++) {
    216             final View view = mRecView.getChildAt(i);
    217             cancelThumbnailTask(view);
    218         }
    219 
    220         super.onDestroyView();
    221     }
    222 
    223     @Override
    224     public void onActivityCreated(Bundle savedInstanceState) {
    225         super.onActivityCreated(savedInstanceState);
    226 
    227         final Context context = getActivity();
    228         final State state = getDisplayState();
    229 
    230         // Read arguments when object created for the first time.
    231         // Restore state if fragment recreated.
    232         Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
    233         mRoot = args.getParcelable(Shared.EXTRA_ROOT);
    234         mDocument = args.getParcelable(Shared.EXTRA_DOC);
    235         mStateKey = buildStateKey(mRoot, mDocument);
    236         mQuery = args.getString(Shared.EXTRA_QUERY);
    237         mType = args.getInt(Shared.EXTRA_TYPE);
    238         final Selection selection = args.getParcelable(Shared.EXTRA_SELECTION);
    239         mSelection = selection != null ? selection : new Selection();
    240         mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
    241 
    242         mIconHelper = new IconHelper(context, MODE_GRID);
    243 
    244         mAdapter = new SectionBreakDocumentsAdapterWrapper(
    245                 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
    246 
    247         mRecView.setAdapter(mAdapter);
    248 
    249         // Switch Access Accessibility API needs an {@link AccessibilityDelegate} to know the proper
    250         // route when user selects an UI element. It usually guesses this if the element has an
    251         // {@link OnClickListener}, but since we do not have one for itemView, we will need to
    252         // manually route it to the right behavior. RecyclerView has its own AccessibilityDelegate,
    253         // and routes it to its LayoutManager; so we must override the LayoutManager's accessibility
    254         // methods to route clicks correctly.
    255         mLayout = new GridLayoutManager(getContext(), mColumnCount) {
    256             @Override
    257             public void onInitializeAccessibilityNodeInfoForItem(
    258                     RecyclerView.Recycler recycler, RecyclerView.State state,
    259                     View host, AccessibilityNodeInfoCompat info) {
    260                 super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
    261                 info.addAction(AccessibilityActionCompat.ACTION_CLICK);
    262             }
    263 
    264             @Override
    265             public boolean performAccessibilityActionForItem(
    266                     RecyclerView.Recycler recycler, RecyclerView.State state, View view,
    267                     int action, Bundle args) {
    268                 // We are only handling click events; route all other to default implementation
    269                 if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
    270                     RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
    271                     if (vh instanceof DocumentHolder) {
    272                         DocumentHolder dh = (DocumentHolder) vh;
    273                         if (dh.mEventListener != null) {
    274                             dh.mEventListener.onActivate(dh);
    275                             return true;
    276                         }
    277                     }
    278                 }
    279                 return super.performAccessibilityActionForItem(recycler, state, view, action,
    280                         args);
    281             }
    282         };
    283         SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
    284         if (lookup != null) {
    285             mLayout.setSpanSizeLookup(lookup);
    286         }
    287         mRecView.setLayoutManager(mLayout);
    288 
    289         mGestureDetector =
    290                 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
    291 
    292         mRecView.addOnItemTouchListener(mGestureDetector);
    293 
    294         // TODO: instead of inserting the view into the constructor, extract listener-creation code
    295         // and set the listener on the view after the fact.  Then the view doesn't need to be passed
    296         // into the selection manager.
    297         mSelectionManager = new MultiSelectManager(
    298                 mRecView,
    299                 mAdapter,
    300                 state.allowMultiple
    301                     ? MultiSelectManager.MODE_MULTIPLE
    302                     : MultiSelectManager.MODE_SINGLE,
    303                 null);
    304 
    305         mSelectionManager.addCallback(new SelectionModeListener());
    306 
    307         mModel = new Model();
    308         mModel.addUpdateListener(mAdapter);
    309         mModel.addUpdateListener(mModelUpdateListener);
    310 
    311         // Make sure this is done after the RecyclerView is set up.
    312         mFocusManager = new FocusManager(context, mRecView, mModel);
    313 
    314         mTuner = FragmentTuner.pick(getContext(), state);
    315         mClipper = new DocumentClipper(context);
    316 
    317         final ActivityManager am = (ActivityManager) context.getSystemService(
    318                 Context.ACTIVITY_SERVICE);
    319         boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
    320         mIconHelper.setThumbnailsEnabled(!svelte);
    321 
    322         // Kick off loader at least once
    323         getLoaderManager().restartLoader(LOADER_ID, null, this);
    324     }
    325 
    326     @Override
    327     public void onSaveInstanceState(Bundle outState) {
    328         super.onSaveInstanceState(outState);
    329 
    330         mSelectionManager.getSelection(mSelection);
    331 
    332         outState.putInt(Shared.EXTRA_TYPE, mType);
    333         outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
    334         outState.putParcelable(Shared.EXTRA_DOC, mDocument);
    335         outState.putString(Shared.EXTRA_QUERY, mQuery);
    336 
    337         // Workaround. To avoid crash, write only up to 512 KB of selection.
    338         // If more files are selected, then the selection will be lost.
    339         final Parcel parcel = Parcel.obtain();
    340         try {
    341             mSelection.writeToParcel(parcel, 0);
    342             if (parcel.dataSize() <= 512 * 1024) {
    343                 outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
    344             }
    345         } finally {
    346             parcel.recycle();
    347         }
    348 
    349         outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
    350     }
    351 
    352     @Override
    353     public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
    354         switch (requestCode) {
    355             case REQUEST_COPY_DESTINATION:
    356                 handleCopyResult(resultCode, data);
    357                 break;
    358             default:
    359                 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
    360         }
    361     }
    362 
    363     private void handleCopyResult(int resultCode, Intent data) {
    364         if (resultCode == Activity.RESULT_CANCELED || data == null) {
    365             // User pressed the back button or otherwise cancelled the destination pick. Don't
    366             // proceed with the copy.
    367             return;
    368         }
    369 
    370         @OpType int operationType = data.getIntExtra(
    371                 FileOperationService.EXTRA_OPERATION,
    372                 FileOperationService.OPERATION_COPY);
    373 
    374         FileOperations.start(
    375                 getActivity(),
    376                 getDisplayState().selectedDocumentsForCopy,
    377                 getDisplayState().stack.peek(),
    378                 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
    379                 operationType);
    380     }
    381 
    382     protected boolean onDoubleTap(MotionEvent e) {
    383         if (Events.isMouseEvent(e)) {
    384             String id = getModelId(e);
    385             if (id != null) {
    386                 return handleViewItem(id);
    387             }
    388         }
    389         return false;
    390     }
    391 
    392     private boolean handleViewItem(String id) {
    393         final Cursor cursor = mModel.getItem(id);
    394 
    395         if (cursor == null) {
    396             Log.w(TAG, "Can't view item. Can't obtain cursor for modeId" + id);
    397             return false;
    398         }
    399 
    400         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    401         final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    402         if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
    403             final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
    404             ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
    405             mSelectionManager.clearSelection();
    406             return true;
    407         }
    408         return false;
    409     }
    410 
    411     @Override
    412     public void onStop() {
    413         super.onStop();
    414 
    415         // Remember last scroll location
    416         final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
    417         getView().saveHierarchyState(container);
    418         final State state = getDisplayState();
    419         state.dirState.put(mStateKey, container);
    420     }
    421 
    422     public void onDisplayStateChanged() {
    423         updateDisplayState();
    424     }
    425 
    426     public void onSortOrderChanged() {
    427         // Sort order is implemented as a sorting wrapper around directory
    428         // results. So when sort order changes, we force a reload of the directory.
    429         getLoaderManager().restartLoader(LOADER_ID, null, this);
    430     }
    431 
    432     public void onViewModeChanged() {
    433         // Mode change is just visual change; no need to kick loader.
    434         updateDisplayState();
    435     }
    436 
    437     private void updateDisplayState() {
    438         State state = getDisplayState();
    439         updateLayout(state.derivedMode);
    440         mRecView.setAdapter(mAdapter);
    441     }
    442 
    443     /**
    444      * Updates the layout after the view mode switches.
    445      * @param mode The new view mode.
    446      */
    447     private void updateLayout(@ViewMode int mode) {
    448         mColumnCount = calculateColumnCount(mode);
    449         if (mLayout != null) {
    450             mLayout.setSpanCount(mColumnCount);
    451         }
    452 
    453         int pad = getDirectoryPadding(mode);
    454         mRecView.setPadding(pad, pad, pad, pad);
    455         mRecView.requestLayout();
    456         mSelectionManager.handleLayoutChanged();  // RecyclerView doesn't do this for us
    457         mIconHelper.setViewMode(mode);
    458     }
    459 
    460     private int calculateColumnCount(@ViewMode int mode) {
    461         if (mode == MODE_LIST) {
    462             // List mode is a "grid" with 1 column.
    463             return 1;
    464         }
    465 
    466         int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
    467         int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
    468         int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
    469 
    470         // RecyclerView sometimes gets a width of 0 (see b/27150284).  Clamp so that we always lay
    471         // out the grid with at least 2 columns.
    472         int columnCount = Math.max(2,
    473                 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
    474 
    475         return columnCount;
    476     }
    477 
    478     private int getDirectoryPadding(@ViewMode int mode) {
    479         switch (mode) {
    480             case MODE_GRID:
    481                 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
    482             case MODE_LIST:
    483                 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
    484             default:
    485                 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
    486         }
    487     }
    488 
    489     @Override
    490     public int getColumnCount() {
    491         return mColumnCount;
    492     }
    493 
    494     /**
    495      * Manages the integration between our ActionMode and MultiSelectManager, initiating
    496      * ActionMode when there is a selection, canceling it when there is no selection,
    497      * and clearing selection when action mode is explicitly exited by the user.
    498      */
    499     private final class SelectionModeListener implements MultiSelectManager.Callback,
    500             ActionMode.Callback, FragmentTuner.SelectionDetails {
    501 
    502         private Selection mSelected = new Selection();
    503 
    504         // Partial files are files that haven't been fully downloaded.
    505         private int mPartialCount = 0;
    506         private int mDirectoryCount = 0;
    507         private int mNoDeleteCount = 0;
    508         private int mNoRenameCount = 0;
    509 
    510         private Menu mMenu;
    511 
    512         @Override
    513         public boolean onBeforeItemStateChange(String modelId, boolean selected) {
    514             if (selected) {
    515                 final Cursor cursor = mModel.getItem(modelId);
    516                 if (cursor == null) {
    517                     Log.w(TAG, "Can't obtain cursor for modelId: " + modelId);
    518                     return false;
    519                 }
    520 
    521                 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    522                 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    523                 if (!mTuner.canSelectType(docMimeType, docFlags)) {
    524                     return false;
    525                 }
    526 
    527                 if (mSelected.size() >= MAX_DOCS_IN_INTENT) {
    528                     Snackbars.makeSnackbar(
    529                             getActivity(),
    530                             R.string.too_many_selected,
    531                             Snackbar.LENGTH_SHORT)
    532                             .show();
    533                     return false;
    534                 }
    535             }
    536             return true;
    537         }
    538 
    539         @Override
    540         public void onItemStateChanged(String modelId, boolean selected) {
    541             final Cursor cursor = mModel.getItem(modelId);
    542             if (cursor == null) {
    543                 Log.w(TAG, "Model returned null cursor for document: " + modelId
    544                         + ". Ignoring state changed event.");
    545                 return;
    546             }
    547 
    548             // TODO: Should this be happening in onSelectionChanged? Technically this callback is
    549             // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
    550             // selection changes here)
    551             final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    552             if (MimePredicate.isDirectoryType(mimeType)) {
    553                 mDirectoryCount += selected ? 1 : -1;
    554             }
    555 
    556             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    557             if ((docFlags & Document.FLAG_PARTIAL) != 0) {
    558                 mPartialCount += selected ? 1 : -1;
    559             }
    560             if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
    561                 mNoDeleteCount += selected ? 1 : -1;
    562             }
    563             if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
    564                 mNoRenameCount += selected ? 1 : -1;
    565             }
    566         }
    567 
    568         @Override
    569         public void onSelectionChanged() {
    570             mSelectionManager.getSelection(mSelected);
    571             if (mSelected.size() > 0) {
    572                 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
    573                 if (mActionMode == null) {
    574                     if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
    575                     mActionMode = getActivity().startActionMode(this);
    576                 }
    577                 updateActionMenu();
    578             } else {
    579                 if (DEBUG) Log.d(TAG, "Finishing action mode.");
    580                 if (mActionMode != null) {
    581                     mActionMode.finish();
    582                 }
    583             }
    584 
    585             if (mActionMode != null) {
    586                 assert(!mSelected.isEmpty());
    587                 final String title = Shared.getQuantityString(getActivity(),
    588                         R.plurals.elements_selected, mSelected.size());
    589                 mActionMode.setTitle(title);
    590                 mRecView.announceForAccessibility(title);
    591             }
    592         }
    593 
    594         // Called when the user exits the action mode
    595         @Override
    596         public void onDestroyActionMode(ActionMode mode) {
    597             if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
    598             mActionMode = null;
    599             // clear selection
    600             mSelectionManager.clearSelection();
    601             mSelected.clear();
    602 
    603             mDirectoryCount = 0;
    604             mPartialCount = 0;
    605             mNoDeleteCount = 0;
    606             mNoRenameCount = 0;
    607 
    608             // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
    609             final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
    610             toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
    611 
    612             // This toolbar is not present in the fixed_layout
    613             final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
    614             if (rootsToolbar != null) {
    615                 rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
    616             }
    617         }
    618 
    619         @Override
    620         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    621             mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    622 
    623             int size = mSelectionManager.getSelection().size();
    624             mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
    625             mode.setTitle(TextUtils.formatSelectedCount(size));
    626 
    627             if (size > 0) {
    628                 // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
    629                 // these controls when using linear navigation.
    630                 final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
    631                 toolbar.setImportantForAccessibility(
    632                         View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    633 
    634                 // This toolbar is not present in the fixed_layout
    635                 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
    636                         R.id.roots_toolbar);
    637                 if (rootsToolbar != null) {
    638                     rootsToolbar.setImportantForAccessibility(
    639                             View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    640                 }
    641                 return true;
    642             }
    643 
    644             return false;
    645         }
    646 
    647         @Override
    648         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    649             mMenu = menu;
    650             updateActionMenu();
    651             return true;
    652         }
    653 
    654         @Override
    655         public boolean containsDirectories() {
    656             return mDirectoryCount > 0;
    657         }
    658 
    659         @Override
    660         public boolean containsPartialFiles() {
    661             return mPartialCount > 0;
    662         }
    663 
    664         @Override
    665         public boolean canDelete() {
    666             return mNoDeleteCount == 0;
    667         }
    668 
    669         @Override
    670         public boolean canRename() {
    671             return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
    672         }
    673 
    674         private void updateActionMenu() {
    675             assert(mMenu != null);
    676             mTuner.updateActionMenu(mMenu, this);
    677             Menus.disableHiddenItems(mMenu);
    678         }
    679 
    680         @Override
    681         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    682             Selection selection = mSelectionManager.getSelection(new Selection());
    683 
    684             switch (item.getItemId()) {
    685                 case R.id.menu_open:
    686                     openDocuments(selection);
    687                     mode.finish();
    688                     return true;
    689 
    690                 case R.id.menu_share:
    691                     shareDocuments(selection);
    692                     // TODO: Only finish selection if share action is completed.
    693                     mode.finish();
    694                     return true;
    695 
    696                 case R.id.menu_delete:
    697                     // deleteDocuments will end action mode if the documents are deleted.
    698                     // It won't end action mode if user cancels the delete.
    699                     deleteDocuments(selection);
    700                     return true;
    701 
    702                 case R.id.menu_copy_to:
    703                     // TODO: Only finish selection mode if copy-to is not canceled.
    704                     // Need to plum down into handling the way we do with deleteDocuments.
    705                     mode.finish();
    706                     transferDocuments(selection, FileOperationService.OPERATION_COPY);
    707                     return true;
    708 
    709                 case R.id.menu_move_to:
    710                     // Exit selection mode first, so we avoid deselecting deleted documents.
    711                     mode.finish();
    712                     transferDocuments(selection, FileOperationService.OPERATION_MOVE);
    713                     return true;
    714 
    715                 case R.id.menu_copy_to_clipboard:
    716                     copySelectedToClipboard();
    717                     return true;
    718 
    719                 case R.id.menu_select_all:
    720                     selectAllFiles();
    721                     return true;
    722 
    723                 case R.id.menu_rename:
    724                     // Exit selection mode first, so we avoid deselecting deleted
    725                     // (renamed) documents.
    726                     mode.finish();
    727                     renameDocuments(selection);
    728                     return true;
    729 
    730                 default:
    731                     if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
    732                     return false;
    733             }
    734         }
    735     }
    736 
    737     public final boolean onBackPressed() {
    738         if (mSelectionManager.hasSelection()) {
    739             if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
    740             mSelectionManager.clearSelection();
    741             return true;
    742         }
    743         return false;
    744     }
    745 
    746     private void cancelThumbnailTask(View view) {
    747         final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
    748         if (iconThumb != null) {
    749             mIconHelper.stopLoading(iconThumb);
    750         }
    751     }
    752 
    753     private void openDocuments(final Selection selected) {
    754         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
    755 
    756         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    757         List<DocumentInfo> docs = mModel.getDocuments(selected);
    758         // TODO: Implement support in Files activity for opening multiple docs.
    759         BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
    760     }
    761 
    762     private void shareDocuments(final Selection selected) {
    763         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
    764 
    765         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    766         List<DocumentInfo> docs = mModel.getDocuments(selected);
    767         Intent intent;
    768 
    769         // Filter out directories and virtual files - those can't be shared.
    770         List<DocumentInfo> docsForSend = new ArrayList<>();
    771         for (DocumentInfo doc: docs) {
    772             if (!doc.isDirectory() && !doc.isVirtualDocument()) {
    773                 docsForSend.add(doc);
    774             }
    775         }
    776 
    777         if (docsForSend.size() == 1) {
    778             final DocumentInfo doc = docsForSend.get(0);
    779 
    780             intent = new Intent(Intent.ACTION_SEND);
    781             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    782             intent.addCategory(Intent.CATEGORY_DEFAULT);
    783             intent.setType(doc.mimeType);
    784             intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
    785 
    786         } else if (docsForSend.size() > 1) {
    787             intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
    788             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    789             intent.addCategory(Intent.CATEGORY_DEFAULT);
    790 
    791             final ArrayList<String> mimeTypes = new ArrayList<>();
    792             final ArrayList<Uri> uris = new ArrayList<>();
    793             for (DocumentInfo doc : docsForSend) {
    794                 mimeTypes.add(doc.mimeType);
    795                 uris.add(doc.derivedUri);
    796             }
    797 
    798             intent.setType(findCommonMimeType(mimeTypes));
    799             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
    800 
    801         } else {
    802             return;
    803         }
    804 
    805         intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
    806         startActivity(intent);
    807     }
    808 
    809     private String generateDeleteMessage(final List<DocumentInfo> docs) {
    810         String message;
    811         int dirsCount = 0;
    812 
    813         for (DocumentInfo doc : docs) {
    814             if (doc.isDirectory()) {
    815                 ++dirsCount;
    816             }
    817         }
    818 
    819         if (docs.size() == 1) {
    820             // Deleteing 1 file xor 1 folder in cwd
    821 
    822             // Address b/28772371, where including user strings in message can result in
    823             // broken bidirectional support.
    824             String displayName = BidiFormatter.getInstance().unicodeWrap(docs.get(0).displayName);
    825             message = dirsCount == 0
    826                     ? getActivity().getString(R.string.delete_filename_confirmation_message,
    827                             displayName)
    828                     : getActivity().getString(R.string.delete_foldername_confirmation_message,
    829                             displayName);
    830         } else if (dirsCount == 0) {
    831             // Deleting only files in cwd
    832             message = Shared.getQuantityString(getActivity(),
    833                     R.plurals.delete_files_confirmation_message, docs.size());
    834         } else if (dirsCount == docs.size()) {
    835             // Deleting only folders in cwd
    836             message = Shared.getQuantityString(getActivity(),
    837                     R.plurals.delete_folders_confirmation_message, docs.size());
    838         } else {
    839             // Deleting mixed items (files and folders) in cwd
    840             message = Shared.getQuantityString(getActivity(),
    841                     R.plurals.delete_items_confirmation_message, docs.size());
    842         }
    843         return message;
    844     }
    845 
    846     private void deleteDocuments(final Selection selected) {
    847         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
    848 
    849         assert(!selected.isEmpty());
    850 
    851         final DocumentInfo srcParent = getDisplayState().stack.peek();
    852 
    853         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    854         List<DocumentInfo> docs = mModel.getDocuments(selected);
    855 
    856         TextView message =
    857                 (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
    858         message.setText(generateDeleteMessage(docs));
    859 
    860         // This "insta-hides" files that are being deleted, because
    861         // the delete operation may be not execute immediately (it
    862         // may be queued up on the FileOperationService.)
    863         // To hide the files locally, we call the hide method on the adapter
    864         // ...which a live object...cannot be parceled.
    865         // For that reason, for now, we implement this dialog NOT
    866         // as a fragment (which can survive rotation and have its own state),
    867         // but as a simple runtime dialog. So rotating a device with an
    868         // active delete dialog...results in that dialog disappearing.
    869         // We can do better, but don't have cycles for it now.
    870         new AlertDialog.Builder(getActivity())
    871             .setView(message)
    872             .setPositiveButton(
    873                  android.R.string.yes,
    874                  new DialogInterface.OnClickListener() {
    875                     @Override
    876                     public void onClick(DialogInterface dialog, int id) {
    877                         // Finish selection mode first which clears selection so we
    878                         // don't end up trying to deselect deleted documents.
    879                         // This is done here, rather in the onActionItemClicked
    880                         // so we can avoid de-selecting items in the case where
    881                         // the user cancels the delete.
    882                         if (mActionMode != null) {
    883                             mActionMode.finish();
    884                         } else {
    885                             Log.w(TAG, "Action mode is null before deleting documents.");
    886                         }
    887                         // Hide the files in the UI...since the operation
    888                         // might be queued up on FileOperationService.
    889                         // We're walking a line here.
    890                         mAdapter.hide(selected.getAll());
    891                         FileOperations.delete(
    892                                 getActivity(), docs, srcParent, getDisplayState().stack);
    893                     }
    894                 })
    895             .setNegativeButton(android.R.string.no, null)
    896             .show();
    897     }
    898 
    899     private void transferDocuments(final Selection selected, final @OpType int mode) {
    900         if(mode == FileOperationService.OPERATION_COPY) {
    901             Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
    902         } else if (mode == FileOperationService.OPERATION_MOVE) {
    903             Metrics.logUserAction(getContext(), Metrics.USER_ACTION_MOVE_TO);
    904         }
    905 
    906         // Pop up a dialog to pick a destination.  This is inadequate but works for now.
    907         // TODO: Implement a picker that is to spec.
    908         final Intent intent = new Intent(
    909                 Shared.ACTION_PICK_COPY_DESTINATION,
    910                 Uri.EMPTY,
    911                 getActivity(),
    912                 DocumentsActivity.class);
    913 
    914 
    915         // Relay any config overrides bits present in the original intent.
    916         Intent original = getActivity().getIntent();
    917         if (original != null && original.hasExtra(Shared.EXTRA_PRODUCTIVITY_MODE)) {
    918             intent.putExtra(
    919                     Shared.EXTRA_PRODUCTIVITY_MODE,
    920                     original.getBooleanExtra(Shared.EXTRA_PRODUCTIVITY_MODE, false));
    921         }
    922 
    923         // Set an appropriate title on the drawer when it is shown in the picker.
    924         // Coupled with the fact that we auto-open the drawer for copy/move operations
    925         // it should basically be the thing people see first.
    926         int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
    927                 ? R.string.menu_move : R.string.menu_copy;
    928         intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
    929 
    930         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    931         List<DocumentInfo> docs = mModel.getDocuments(selected);
    932         // TODO: Can this move to Fragment bundle state?
    933         getDisplayState().selectedDocumentsForCopy = docs;
    934 
    935         // Determine if there is a directory in the set of documents
    936         // to be copied? Why? Directory creation isn't supported by some roots
    937         // (like Downloads). This informs DocumentsActivity (the "picker")
    938         // to restrict available roots to just those with support.
    939         intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
    940         intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
    941 
    942         // This just identifies the type of request...we'll check it
    943         // when we reveive a response.
    944         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
    945     }
    946 
    947     private static boolean hasDirectory(List<DocumentInfo> docs) {
    948         for (DocumentInfo info : docs) {
    949             if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
    950                 return true;
    951             }
    952         }
    953         return false;
    954     }
    955 
    956     private void renameDocuments(Selection selected) {
    957         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_RENAME);
    958 
    959         // Batch renaming not supported
    960         // Rename option is only available in menu when 1 document selected
    961         assert(selected.size() == 1);
    962 
    963         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
    964         List<DocumentInfo> docs = mModel.getDocuments(selected);
    965         RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
    966     }
    967 
    968     @Override
    969     public void initDocumentHolder(DocumentHolder holder) {
    970         holder.addEventListener(mItemEventListener);
    971         holder.itemView.setOnFocusChangeListener(mFocusManager);
    972     }
    973 
    974     @Override
    975     public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
    976         setupDragAndDropOnDocumentView(holder.itemView, cursor);
    977     }
    978 
    979     @Override
    980     public State getDisplayState() {
    981         return ((BaseActivity) getActivity()).getDisplayState();
    982     }
    983 
    984     @Override
    985     public Model getModel() {
    986         return mModel;
    987     }
    988 
    989     @Override
    990     public boolean isDocumentEnabled(String docMimeType, int docFlags) {
    991         return mTuner.isDocumentEnabled(docMimeType, docFlags);
    992     }
    993 
    994     private void showEmptyDirectory() {
    995         showEmptyView(R.string.empty, R.drawable.cabinet);
    996     }
    997 
    998     private void showNoResults(RootInfo root) {
    999         CharSequence msg = getContext().getResources().getText(R.string.no_results);
   1000         showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
   1001     }
   1002 
   1003     private void showQueryError() {
   1004         showEmptyView(R.string.query_error, R.drawable.hourglass);
   1005     }
   1006 
   1007     private void showEmptyView(@StringRes int id, int drawable) {
   1008         showEmptyView(getContext().getResources().getText(id), drawable);
   1009     }
   1010 
   1011     private void showEmptyView(CharSequence msg, int drawable) {
   1012         View content = mEmptyView.findViewById(R.id.content);
   1013         TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
   1014         ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
   1015         msgView.setText(msg);
   1016         imageView.setImageResource(drawable);
   1017 
   1018         mEmptyView.setVisibility(View.VISIBLE);
   1019         mEmptyView.requestFocus();
   1020         mRecView.setVisibility(View.GONE);
   1021     }
   1022 
   1023     private void showDirectory() {
   1024         mEmptyView.setVisibility(View.GONE);
   1025         mRecView.setVisibility(View.VISIBLE);
   1026         mRecView.requestFocus();
   1027     }
   1028 
   1029     private String findCommonMimeType(List<String> mimeTypes) {
   1030         String[] commonType = mimeTypes.get(0).split("/");
   1031         if (commonType.length != 2) {
   1032             return "*/*";
   1033         }
   1034 
   1035         for (int i = 1; i < mimeTypes.size(); i++) {
   1036             String[] type = mimeTypes.get(i).split("/");
   1037             if (type.length != 2) continue;
   1038 
   1039             if (!commonType[1].equals(type[1])) {
   1040                 commonType[1] = "*";
   1041             }
   1042 
   1043             if (!commonType[0].equals(type[0])) {
   1044                 commonType[0] = "*";
   1045                 commonType[1] = "*";
   1046                 break;
   1047             }
   1048         }
   1049 
   1050         return commonType[0] + "/" + commonType[1];
   1051     }
   1052 
   1053     private void copyFromClipboard() {
   1054         new AsyncTask<Void, Void, List<DocumentInfo>>() {
   1055 
   1056             @Override
   1057             protected List<DocumentInfo> doInBackground(Void... params) {
   1058                 return mClipper.getClippedDocuments();
   1059             }
   1060 
   1061             @Override
   1062             protected void onPostExecute(List<DocumentInfo> docs) {
   1063                 DocumentInfo destination =
   1064                         ((BaseActivity) getActivity()).getCurrentDirectory();
   1065                 copyDocuments(docs, destination);
   1066             }
   1067         }.execute();
   1068     }
   1069 
   1070     private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
   1071         assert(clipData != null);
   1072 
   1073         new AsyncTask<Void, Void, List<DocumentInfo>>() {
   1074 
   1075             @Override
   1076             protected List<DocumentInfo> doInBackground(Void... params) {
   1077                 return mClipper.getDocumentsFromClipData(clipData);
   1078             }
   1079 
   1080             @Override
   1081             protected void onPostExecute(List<DocumentInfo> docs) {
   1082                 copyDocuments(docs, destination);
   1083             }
   1084         }.execute();
   1085     }
   1086 
   1087     private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
   1088         BaseActivity activity = (BaseActivity) getActivity();
   1089         if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
   1090             Snackbars.makeSnackbar(
   1091                     getActivity(),
   1092                     R.string.clipboard_files_cannot_paste,
   1093                     Snackbar.LENGTH_SHORT)
   1094                     .show();
   1095             return;
   1096         }
   1097 
   1098         if (docs.isEmpty()) {
   1099             return;
   1100         }
   1101 
   1102         final DocumentStack curStack = getDisplayState().stack;
   1103         DocumentStack tmpStack = new DocumentStack();
   1104         if (destination != null) {
   1105             tmpStack.push(destination);
   1106             tmpStack.addAll(curStack);
   1107         } else {
   1108             tmpStack = curStack;
   1109         }
   1110 
   1111         FileOperations.copy(getActivity(), docs, tmpStack);
   1112     }
   1113 
   1114     public void copySelectedToClipboard() {
   1115         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
   1116 
   1117         Selection selection = mSelectionManager.getSelection(new Selection());
   1118         if (!selection.isEmpty()) {
   1119             copySelectionToClipboard(selection);
   1120             mSelectionManager.clearSelection();
   1121         }
   1122     }
   1123 
   1124     void copySelectionToClipboard(Selection selected) {
   1125         assert(!selected.isEmpty());
   1126 
   1127         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
   1128         List<DocumentInfo> docs = mModel.getDocuments(selected);
   1129         mClipper.clipDocuments(docs);
   1130         Activity activity = getActivity();
   1131         Snackbars.makeSnackbar(activity,
   1132                 activity.getResources().getQuantityString(
   1133                         R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
   1134                 Snackbar.LENGTH_SHORT).show();
   1135     }
   1136 
   1137     public void pasteFromClipboard() {
   1138         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
   1139 
   1140         copyFromClipboard();
   1141         getActivity().invalidateOptionsMenu();
   1142     }
   1143 
   1144     /**
   1145      * Returns true if the list of files can be copied to destination. Note that this
   1146      * is a policy check only. Currently the method does not attempt to verify
   1147      * available space or any other environmental aspects possibly resulting in
   1148      * failure to copy.
   1149      *
   1150      * @return true if the list of files can be copied to destination.
   1151      */
   1152     private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
   1153         if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
   1154             return false;
   1155         }
   1156 
   1157         // Can't copy folders to downloads, because we don't show folders there.
   1158         if (root.isDownloads()) {
   1159             for (DocumentInfo docs : files) {
   1160                 if (docs.isDirectory()) {
   1161                     return false;
   1162                 }
   1163             }
   1164         }
   1165 
   1166         return true;
   1167     }
   1168 
   1169     public void selectAllFiles() {
   1170         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL);
   1171 
   1172         // Exclude disabled files.
   1173         Set<String> enabled = new HashSet<String>();
   1174         List<String> modelIds = mAdapter.getModelIds();
   1175 
   1176         // Get the current selection.
   1177         String[] alreadySelected = mSelectionManager.getSelection().getAll();
   1178         for (String id : alreadySelected) {
   1179            enabled.add(id);
   1180         }
   1181 
   1182         for (String id : modelIds) {
   1183             Cursor cursor = getModel().getItem(id);
   1184             if (cursor == null) {
   1185                 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
   1186                 continue;
   1187             }
   1188             String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
   1189             int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
   1190             if (mTuner.canSelectType(docMimeType, docFlags)) {
   1191                 if (enabled.size() >= MAX_DOCS_IN_INTENT) {
   1192                     Snackbars.makeSnackbar(
   1193                         getActivity(),
   1194                         R.string.too_many_in_select_all,
   1195                         Snackbar.LENGTH_SHORT)
   1196                         .show();
   1197                     break;
   1198                 }
   1199                 enabled.add(id);
   1200             }
   1201         }
   1202 
   1203         // Only select things currently visible in the adapter.
   1204         boolean changed = mSelectionManager.setItemsSelected(enabled, true);
   1205         if (changed) {
   1206             updateDisplayState();
   1207         }
   1208     }
   1209 
   1210     /**
   1211      * Attempts to restore focus on the directory listing.
   1212      */
   1213     public void requestFocus() {
   1214         mFocusManager.restoreLastFocus();
   1215     }
   1216 
   1217     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
   1218         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
   1219         if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
   1220             // Make a directory item a drop target. Drop on non-directories and empty space
   1221             // is handled at the list/grid view level.
   1222             view.setOnDragListener(mOnDragListener);
   1223         }
   1224 
   1225         if (mTuner.dragAndDropEnabled()) {
   1226             // Make all items draggable.
   1227             view.setOnLongClickListener(onLongClickListener);
   1228         }
   1229     }
   1230 
   1231     private View.OnDragListener mOnDragListener = new View.OnDragListener() {
   1232         @Override
   1233         public boolean onDrag(View v, DragEvent event) {
   1234             switch (event.getAction()) {
   1235                 case DragEvent.ACTION_DRAG_STARTED:
   1236                     // TODO: Check if the event contains droppable data.
   1237                     return true;
   1238 
   1239                 // TODO: Expand drop target directory on hover?
   1240                 case DragEvent.ACTION_DRAG_ENTERED:
   1241                     setDropTargetHighlight(v, true);
   1242                     return true;
   1243                 case DragEvent.ACTION_DRAG_EXITED:
   1244                     setDropTargetHighlight(v, false);
   1245                     return true;
   1246 
   1247                 case DragEvent.ACTION_DRAG_LOCATION:
   1248                     return true;
   1249 
   1250                 case DragEvent.ACTION_DRAG_ENDED:
   1251                     if (event.getResult()) {
   1252                         // Exit selection mode if the drop was handled.
   1253                         mSelectionManager.clearSelection();
   1254                     }
   1255                     return true;
   1256 
   1257                 case DragEvent.ACTION_DROP:
   1258                     // After a drop event, always stop highlighting the target.
   1259                     setDropTargetHighlight(v, false);
   1260 
   1261                     ClipData clipData = event.getClipData();
   1262                     if (clipData == null) {
   1263                         Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
   1264                         return false;
   1265                     }
   1266 
   1267                     // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
   1268                     // multi-window drag, because localState isn't carried over from one process to
   1269                     // another.
   1270                     Object src = event.getLocalState();
   1271                     DocumentInfo dst = getDestination(v);
   1272                     if (Objects.equals(src, dst)) {
   1273                         if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
   1274                         return false;
   1275                     }
   1276 
   1277                     // Recognize multi-window drag and drop based on the fact that localState is not
   1278                     // carried between processes. It will stop working when the localsState behavior
   1279                     // is changed. The info about window should be passed in the localState then.
   1280                     // The localState could also be null for copying from Recents in single window
   1281                     // mode, but Recents doesn't offer this functionality (no directories).
   1282                     Metrics.logUserAction(getContext(),
   1283                             src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
   1284                                     : Metrics.USER_ACTION_DRAG_N_DROP);
   1285 
   1286                     copyFromClipData(clipData, dst);
   1287                     return true;
   1288             }
   1289             return false;
   1290         }
   1291 
   1292         private DocumentInfo getDestination(View v) {
   1293             String id = getModelId(v);
   1294             if (id != null) {
   1295                 Cursor dstCursor = mModel.getItem(id);
   1296                 if (dstCursor == null) {
   1297                     Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
   1298                     return null;
   1299                 }
   1300                 return DocumentInfo.fromDirectoryCursor(dstCursor);
   1301             }
   1302 
   1303             if (v == mRecView || v == mEmptyView) {
   1304                 return getDisplayState().stack.peek();
   1305             }
   1306 
   1307             return null;
   1308         }
   1309 
   1310         private void setDropTargetHighlight(View v, boolean highlight) {
   1311             // Note: use exact comparison - this code is searching for views which are children of
   1312             // the RecyclerView instance in the UI.
   1313             if (v.getParent() == mRecView) {
   1314                 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
   1315                 if (vh instanceof DocumentHolder) {
   1316                     ((DocumentHolder) vh).setHighlighted(highlight);
   1317                 }
   1318             }
   1319         }
   1320     };
   1321 
   1322     /**
   1323      * Gets the model ID for a given motion event (using the event position)
   1324      */
   1325     private String getModelId(MotionEvent e) {
   1326         View view = mRecView.findChildViewUnder(e.getX(), e.getY());
   1327         if (view == null) {
   1328             return null;
   1329         }
   1330         RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
   1331         if (vh instanceof DocumentHolder) {
   1332             return ((DocumentHolder) vh).modelId;
   1333         } else {
   1334             return null;
   1335         }
   1336     }
   1337 
   1338     /**
   1339      * Gets the model ID for a given RecyclerView item.
   1340      * @param view A View that is a document item view, or a child of a document item view.
   1341      * @return The Model ID for the given document, or null if the given view is not associated with
   1342      *     a document item view.
   1343      */
   1344     private String getModelId(View view) {
   1345         View itemView = mRecView.findContainingItemView(view);
   1346         if (itemView != null) {
   1347             RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
   1348             if (vh instanceof DocumentHolder) {
   1349                 return ((DocumentHolder) vh).modelId;
   1350             }
   1351         }
   1352         return null;
   1353     }
   1354 
   1355     private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
   1356         String modelId = getModelId(currentItemView);
   1357         if (modelId == null) {
   1358             return Collections.EMPTY_LIST;
   1359         }
   1360 
   1361         final List<DocumentInfo> selectedDocs =
   1362                 mModel.getDocuments(mSelectionManager.getSelection());
   1363         if (!selectedDocs.isEmpty()) {
   1364             if (!isSelected(modelId)) {
   1365                 // There is a selection that does not include the current item, drag nothing.
   1366                 return Collections.EMPTY_LIST;
   1367             }
   1368             return selectedDocs;
   1369         }
   1370 
   1371         final Cursor cursor = mModel.getItem(modelId);
   1372         if (cursor == null) {
   1373             Log.w(TAG, "Undraggable document. Can't obtain cursor for modelId " + modelId);
   1374             return Collections.EMPTY_LIST;
   1375         }
   1376 
   1377         return Lists.newArrayList(
   1378                 DocumentInfo.fromDirectoryCursor(cursor));
   1379     }
   1380 
   1381     private static class DragShadowBuilder extends View.DragShadowBuilder {
   1382 
   1383         private final Context mContext;
   1384         private final IconHelper mIconHelper;
   1385         private final LayoutInflater mInflater;
   1386         private final View mShadowView;
   1387         private final TextView mTitle;
   1388         private final ImageView mIcon;
   1389         private final int mWidth;
   1390         private final int mHeight;
   1391 
   1392         public DragShadowBuilder(Context context, IconHelper iconHelper, List<DocumentInfo> docs) {
   1393             mContext = context;
   1394             mIconHelper = iconHelper;
   1395             mInflater = LayoutInflater.from(context);
   1396 
   1397             mWidth = mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
   1398             mHeight= mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
   1399 
   1400             mShadowView = mInflater.inflate(R.layout.drag_shadow_layout, null);
   1401             mTitle = (TextView) mShadowView.findViewById(android.R.id.title);
   1402             mIcon = (ImageView) mShadowView.findViewById(android.R.id.icon);
   1403 
   1404             mTitle.setText(getTitle(docs));
   1405             mIcon.setImageDrawable(getIcon(docs));
   1406         }
   1407 
   1408         private Drawable getIcon(List<DocumentInfo> docs) {
   1409             if (docs.size() == 1) {
   1410                 final DocumentInfo doc = docs.get(0);
   1411                 return mIconHelper.getDocumentIcon(mContext, doc.authority, doc.documentId,
   1412                         doc.mimeType, doc.icon);
   1413             }
   1414             return mContext.getDrawable(com.android.internal.R.drawable.ic_doc_generic);
   1415         }
   1416 
   1417         private String getTitle(List<DocumentInfo> docs) {
   1418             if (docs.size() == 1) {
   1419                 final DocumentInfo doc = docs.get(0);
   1420                 return doc.displayName;
   1421             }
   1422             return Shared.getQuantityString(mContext, R.plurals.elements_dragged, docs.size());
   1423         }
   1424 
   1425         @Override
   1426         public void onProvideShadowMetrics(
   1427                 Point shadowSize, Point shadowTouchPoint) {
   1428             shadowSize.set(mWidth, mHeight);
   1429             shadowTouchPoint.set(mWidth, mHeight);
   1430         }
   1431 
   1432         @Override
   1433         public void onDrawShadow(Canvas canvas) {
   1434             Rect r = canvas.getClipBounds();
   1435             // Calling measure is necessary in order for all child views to get correctly laid out.
   1436             mShadowView.measure(
   1437                     View.MeasureSpec.makeMeasureSpec(r.right- r.left, View.MeasureSpec.EXACTLY),
   1438                     View.MeasureSpec.makeMeasureSpec(r.top- r.bottom, View.MeasureSpec.EXACTLY));
   1439             mShadowView.layout(r.left, r.top, r.right, r.bottom);
   1440             mShadowView.draw(canvas);
   1441         }
   1442     }
   1443 
   1444     @Override
   1445     public boolean isSelected(String modelId) {
   1446         return mSelectionManager.getSelection().contains(modelId);
   1447     }
   1448 
   1449     private class ItemEventListener implements DocumentHolder.EventListener {
   1450         @Override
   1451         public boolean onActivate(DocumentHolder doc) {
   1452             // Toggle selection if we're in selection mode, othewise, view item.
   1453             if (mSelectionManager.hasSelection()) {
   1454                 mSelectionManager.toggleSelection(doc.modelId);
   1455             } else {
   1456                 handleViewItem(doc.modelId);
   1457             }
   1458             return true;
   1459         }
   1460 
   1461         @Override
   1462         public boolean onSelect(DocumentHolder doc) {
   1463             mSelectionManager.toggleSelection(doc.modelId);
   1464             mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
   1465             return true;
   1466         }
   1467 
   1468         @Override
   1469         public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
   1470             // Only handle key-down events. This is simpler, consistent with most other UIs, and
   1471             // enables the handling of repeated key events from holding down a key.
   1472             if (event.getAction() != KeyEvent.ACTION_DOWN) {
   1473                 return false;
   1474             }
   1475 
   1476             // Ignore tab key events.  Those should be handled by the top-level key handler.
   1477             if (keyCode == KeyEvent.KEYCODE_TAB) {
   1478                 return false;
   1479             }
   1480 
   1481             if (mFocusManager.handleKey(doc, keyCode, event)) {
   1482                 // Handle range selection adjustments. Extending the selection will adjust the
   1483                 // bounds of the in-progress range selection. Each time an unshifted navigation
   1484                 // event is received, the range selection is restarted.
   1485                 if (shouldExtendSelection(doc, event)) {
   1486                     if (!mSelectionManager.isRangeSelectionActive()) {
   1487                         // Start a range selection if one isn't active
   1488                         mSelectionManager.startRangeSelection(doc.getAdapterPosition());
   1489                     }
   1490                     mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
   1491                 } else {
   1492                     mSelectionManager.endRangeSelection();
   1493                 }
   1494                 return true;
   1495             }
   1496 
   1497             // Handle enter key events
   1498             switch (keyCode) {
   1499                 case KeyEvent.KEYCODE_ENTER:
   1500                     if (event.isShiftPressed()) {
   1501                         return onSelect(doc);
   1502                     }
   1503                     // For non-shifted enter keypresses, fall through.
   1504                 case KeyEvent.KEYCODE_DPAD_CENTER:
   1505                 case KeyEvent.KEYCODE_BUTTON_A:
   1506                     return onActivate(doc);
   1507                 case KeyEvent.KEYCODE_FORWARD_DEL:
   1508                     // This has to be handled here instead of in a keyboard shortcut, because
   1509                     // keyboard shortcuts all have to be modified with the 'Ctrl' key.
   1510                     if (mSelectionManager.hasSelection()) {
   1511                         Selection selection = mSelectionManager.getSelection(new Selection());
   1512                         deleteDocuments(selection);
   1513                     }
   1514                     // Always handle the key, even if there was nothing to delete. This is a
   1515                     // precaution to prevent other handlers from potentially picking up the event
   1516                     // and triggering extra behaviours.
   1517                     return true;
   1518             }
   1519 
   1520             return false;
   1521         }
   1522 
   1523         private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) {
   1524             if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
   1525                 return false;
   1526             }
   1527 
   1528             // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
   1529             // the same, and responsible for the same thing (whether to select or not).
   1530             final Cursor cursor = mModel.getItem(doc.modelId);
   1531             if (cursor == null) {
   1532                 Log.w(TAG, "Couldn't obtain cursor for modelId: " + doc.modelId);
   1533                 return false;
   1534             }
   1535 
   1536             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
   1537             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
   1538             return mTuner.canSelectType(docMimeType, docFlags);
   1539         }
   1540     }
   1541 
   1542     private final class ModelUpdateListener implements Model.UpdateListener {
   1543         @Override
   1544         public void onModelUpdate(Model model) {
   1545             if (model.info != null || model.error != null) {
   1546                 mMessageBar.setInfo(model.info);
   1547                 mMessageBar.setError(model.error);
   1548                 mMessageBar.show();
   1549             }
   1550 
   1551             mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
   1552 
   1553             if (model.isEmpty()) {
   1554                 if (mSearchMode) {
   1555                     showNoResults(getDisplayState().stack.root);
   1556                 } else {
   1557                     showEmptyDirectory();
   1558                 }
   1559             } else {
   1560                 showDirectory();
   1561                 mAdapter.notifyDataSetChanged();
   1562             }
   1563 
   1564             if (!model.isLoading()) {
   1565                 ((BaseActivity) getActivity()).notifyDirectoryLoaded(
   1566                     model.doc != null ? model.doc.derivedUri : null);
   1567             }
   1568         }
   1569 
   1570         @Override
   1571         public void onModelUpdateFailed(Exception e) {
   1572             showQueryError();
   1573         }
   1574     }
   1575 
   1576     private DragStartHelper.OnDragStartListener mOnDragStartListener =
   1577             new DragStartHelper.OnDragStartListener() {
   1578         @Override
   1579         public boolean onDragStart(View v, DragStartHelper helper) {
   1580             if (isSelected(getModelId(v))) {
   1581                 List<DocumentInfo> docs = getDraggableDocuments(v);
   1582                 if (docs.isEmpty()) {
   1583                     return false;
   1584                 }
   1585                 v.startDragAndDrop(
   1586                         mClipper.getClipDataForDocuments(docs),
   1587                         new DragShadowBuilder(getActivity(), mIconHelper, docs),
   1588                         getDisplayState().stack.peek(),
   1589                         View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
   1590                                 View.DRAG_FLAG_GLOBAL_URI_WRITE
   1591                 );
   1592                 return true;
   1593             }
   1594 
   1595             return false;
   1596         }
   1597     };
   1598 
   1599     private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
   1600 
   1601     private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
   1602         @Override
   1603         public boolean onLongClick(View v) {
   1604             return mDragHelper.onLongClick(v);
   1605         }
   1606     };
   1607 
   1608     // Previously we listened to events with one class, only to bounce them forward
   1609     // to GestureDetector. We're still doing that here, but with a single class
   1610     // that reduces overall complexity in our glue code.
   1611     private static final class ListeningGestureDetector extends GestureDetector
   1612             implements OnItemTouchListener {
   1613 
   1614         private int mLastTool = -1;
   1615         private DragStartHelper mDragHelper;
   1616 
   1617         public ListeningGestureDetector(
   1618                 Context context, DragStartHelper dragHelper, GestureListener listener) {
   1619             super(context, listener);
   1620             mDragHelper = dragHelper;
   1621             setOnDoubleTapListener(listener);
   1622         }
   1623 
   1624         boolean mouseSpawnedLastEvent() {
   1625             return Events.isMouseType(mLastTool);
   1626         }
   1627 
   1628         boolean touchSpawnedLastEvent() {
   1629             return Events.isTouchType(mLastTool);
   1630         }
   1631 
   1632         @Override
   1633         public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
   1634             mLastTool = e.getToolType(0);
   1635 
   1636             // Detect drag events. When a drag is detected, intercept the rest of the gesture.
   1637             View itemView = rv.findChildViewUnder(e.getX(), e.getY());
   1638             if (itemView != null && mDragHelper.onTouch(itemView,  e)) {
   1639                 return true;
   1640             }
   1641             // Forward unhandled events to the GestureDetector.
   1642             onTouchEvent(e);
   1643 
   1644             return false;
   1645         }
   1646 
   1647         @Override
   1648         public void onTouchEvent(RecyclerView rv, MotionEvent e) {
   1649             View itemView = rv.findChildViewUnder(e.getX(), e.getY());
   1650             mDragHelper.onTouch(itemView,  e);
   1651             // Note: even though this event is being handled as part of a drag gesture, continue
   1652             // forwarding to the GestureDetector. The detector needs to see the entire cluster of
   1653             // events in order to properly interpret gestures.
   1654             onTouchEvent(e);
   1655         }
   1656 
   1657         @Override
   1658         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
   1659     }
   1660 
   1661     /**
   1662      * The gesture listener for items in the list/grid view. Interprets gestures and sends the
   1663      * events to the target DocumentHolder, whence they are routed to the appropriate listener.
   1664      */
   1665     private class GestureListener extends GestureDetector.SimpleOnGestureListener {
   1666         @Override
   1667         public boolean onSingleTapUp(MotionEvent e) {
   1668             // Single tap logic:
   1669             // If the selection manager is active, it gets first whack at handling tap
   1670             // events. Otherwise, tap events are routed to the target DocumentHolder.
   1671             boolean handled = mSelectionManager.onSingleTapUp(
   1672                         new MotionInputEvent(e, mRecView));
   1673 
   1674             if (handled) {
   1675                 return handled;
   1676             }
   1677 
   1678             // Give the DocumentHolder a crack at the event.
   1679             DocumentHolder holder = getTarget(e);
   1680             if (holder != null) {
   1681                 handled = holder.onSingleTapUp(e);
   1682             }
   1683 
   1684             return handled;
   1685         }
   1686 
   1687         @Override
   1688         public void onLongPress(MotionEvent e) {
   1689             // Long-press events get routed directly to the selection manager. They can be
   1690             // changed to route through the DocumentHolder if necessary.
   1691             mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
   1692         }
   1693 
   1694         @Override
   1695         public boolean onDoubleTap(MotionEvent e) {
   1696             // Double-tap events are handled directly by the DirectoryFragment. They can be changed
   1697             // to route through the DocumentHolder if necessary.
   1698             return DirectoryFragment.this.onDoubleTap(e);
   1699         }
   1700 
   1701         private @Nullable DocumentHolder getTarget(MotionEvent e) {
   1702             View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
   1703             if (childView != null) {
   1704                 return (DocumentHolder) mRecView.getChildViewHolder(childView);
   1705             } else {
   1706                 return null;
   1707             }
   1708         }
   1709     }
   1710 
   1711     public static void showDirectory(
   1712             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
   1713         create(fm, TYPE_NORMAL, root, doc, null, anim);
   1714     }
   1715 
   1716     public static void showRecentsOpen(FragmentManager fm, int anim) {
   1717         create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
   1718     }
   1719 
   1720     public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
   1721             String query) {
   1722         DirectoryFragment df = get(fm);
   1723 
   1724         df.mQuery = query;
   1725         df.mRoot = root;
   1726         df.mDocument = doc;
   1727         df.mSearchMode =  query != null;
   1728         df.getLoaderManager().restartLoader(LOADER_ID, null, df);
   1729     }
   1730 
   1731     public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
   1732             String query) {
   1733         DirectoryFragment df = get(fm);
   1734         df.mType = type;
   1735         df.mQuery = query;
   1736         df.mRoot = root;
   1737         df.mDocument = doc;
   1738         df.mSearchMode =  query != null;
   1739         df.getLoaderManager().restartLoader(LOADER_ID, null, df);
   1740     }
   1741 
   1742     public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
   1743             String query, int anim) {
   1744         final Bundle args = new Bundle();
   1745         args.putInt(Shared.EXTRA_TYPE, type);
   1746         args.putParcelable(Shared.EXTRA_ROOT, root);
   1747         args.putParcelable(Shared.EXTRA_DOC, doc);
   1748         args.putString(Shared.EXTRA_QUERY, query);
   1749         args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
   1750 
   1751         final FragmentTransaction ft = fm.beginTransaction();
   1752         AnimationView.setupAnimations(ft, anim, args);
   1753 
   1754         final DirectoryFragment fragment = new DirectoryFragment();
   1755         fragment.setArguments(args);
   1756 
   1757         ft.replace(getFragmentId(), fragment);
   1758         ft.commitAllowingStateLoss();
   1759     }
   1760 
   1761     private static String buildStateKey(RootInfo root, DocumentInfo doc) {
   1762         final StringBuilder builder = new StringBuilder();
   1763         builder.append(root != null ? root.authority : "null").append(';');
   1764         builder.append(root != null ? root.rootId : "null").append(';');
   1765         builder.append(doc != null ? doc.documentId : "null");
   1766         return builder.toString();
   1767     }
   1768 
   1769     public static @Nullable DirectoryFragment get(FragmentManager fm) {
   1770         // TODO: deal with multiple directories shown at once
   1771         Fragment fragment = fm.findFragmentById(getFragmentId());
   1772         return fragment instanceof DirectoryFragment
   1773                 ? (DirectoryFragment) fragment
   1774                 : null;
   1775     }
   1776 
   1777     private static int getFragmentId() {
   1778         return R.id.container_directory;
   1779     }
   1780 
   1781     @Override
   1782     public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
   1783         Context context = getActivity();
   1784         State state = getDisplayState();
   1785 
   1786         Uri contentsUri;
   1787         switch (mType) {
   1788             case TYPE_NORMAL:
   1789                 contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
   1790                         mRoot.authority, mRoot.rootId, mQuery)
   1791                         : DocumentsContract.buildChildDocumentsUri(
   1792                                 mDocument.authority, mDocument.documentId);
   1793                 if (mTuner.managedModeEnabled()) {
   1794                     contentsUri = DocumentsContract.setManageMode(contentsUri);
   1795                 }
   1796                 return new DirectoryLoader(
   1797                         context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
   1798                         mSearchMode);
   1799             case TYPE_RECENT_OPEN:
   1800                 final RootsCache roots = DocumentsApplication.getRootsCache(context);
   1801                 return new RecentsLoader(context, roots, state);
   1802 
   1803             default:
   1804                 throw new IllegalStateException("Unknown type " + mType);
   1805         }
   1806     }
   1807 
   1808     @Override
   1809     public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
   1810         if (!isAdded()) return;
   1811 
   1812         if (mSearchMode) {
   1813             Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
   1814         }
   1815 
   1816         State state = getDisplayState();
   1817 
   1818         mAdapter.notifyDataSetChanged();
   1819         mModel.update(result);
   1820 
   1821         state.derivedSortOrder = result.sortOrder;
   1822 
   1823         updateLayout(state.derivedMode);
   1824 
   1825         if (mSelection != null) {
   1826             mSelectionManager.setItemsSelected(mSelection.toList(), true);
   1827             mSelection.clear();
   1828         }
   1829 
   1830         // Restore any previous instance state
   1831         final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
   1832         if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
   1833             getView().restoreHierarchyState(container);
   1834         } else if (mLastSortOrder != state.derivedSortOrder) {
   1835             // The derived sort order takes the user sort order into account, but applies
   1836             // directory-specific defaults when the user doesn't explicitly set the sort
   1837             // order. Scroll to the top if the sort order actually changed.
   1838             mRecView.smoothScrollToPosition(0);
   1839         }
   1840 
   1841         mLastSortOrder = state.derivedSortOrder;
   1842 
   1843         mTuner.onModelLoaded(mModel, mType, mSearchMode);
   1844 
   1845     }
   1846 
   1847     @Override
   1848     public void onLoaderReset(Loader<DirectoryResult> loader) {
   1849         mModel.update(null);
   1850     }
   1851   }
   1852