• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.dirlist;
18 
19 import static com.android.documentsui.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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onDestroyView()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
onActivityCreated(Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)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
onActivityResult(@equestCode int requestCode, int resultCode, Intent data)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 
handleCopyResult(int resultCode, Intent data)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 
onDoubleTap(MotionEvent e)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 
handleViewItem(String id)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
onStop()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 
onDisplayStateChanged()422     public void onDisplayStateChanged() {
423         updateDisplayState();
424     }
425 
onSortOrderChanged()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 
onViewModeChanged()432     public void onViewModeChanged() {
433         // Mode change is just visual change; no need to kick loader.
434         updateDisplayState();
435     }
436 
updateDisplayState()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      */
updateLayout(@iewMode int mode)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 
calculateColumnCount(@iewMode int mode)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 
getDirectoryPadding(@iewMode int mode)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
getColumnCount()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
onBeforeItemStateChange(String modelId, boolean selected)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
onItemStateChanged(String modelId, boolean selected)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
onSelectionChanged()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
onDestroyActionMode(ActionMode mode)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
onCreateActionMode(ActionMode mode, Menu menu)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
onPrepareActionMode(ActionMode mode, Menu menu)648         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
649             mMenu = menu;
650             updateActionMenu();
651             return true;
652         }
653 
654         @Override
containsDirectories()655         public boolean containsDirectories() {
656             return mDirectoryCount > 0;
657         }
658 
659         @Override
containsPartialFiles()660         public boolean containsPartialFiles() {
661             return mPartialCount > 0;
662         }
663 
664         @Override
canDelete()665         public boolean canDelete() {
666             return mNoDeleteCount == 0;
667         }
668 
669         @Override
canRename()670         public boolean canRename() {
671             return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
672         }
673 
updateActionMenu()674         private void updateActionMenu() {
675             assert(mMenu != null);
676             mTuner.updateActionMenu(mMenu, this);
677             Menus.disableHiddenItems(mMenu);
678         }
679 
680         @Override
onActionItemClicked(ActionMode mode, MenuItem item)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 
onBackPressed()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 
cancelThumbnailTask(View view)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 
openDocuments(final Selection selected)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 
shareDocuments(final Selection selected)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 
generateDeleteMessage(final List<DocumentInfo> docs)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 
deleteDocuments(final Selection selected)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 
transferDocuments(final Selection selected, final @OpType int mode)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 
hasDirectory(List<DocumentInfo> docs)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 
renameDocuments(Selection selected)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
initDocumentHolder(DocumentHolder holder)969     public void initDocumentHolder(DocumentHolder holder) {
970         holder.addEventListener(mItemEventListener);
971         holder.itemView.setOnFocusChangeListener(mFocusManager);
972     }
973 
974     @Override
onBindDocumentHolder(DocumentHolder holder, Cursor cursor)975     public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
976         setupDragAndDropOnDocumentView(holder.itemView, cursor);
977     }
978 
979     @Override
getDisplayState()980     public State getDisplayState() {
981         return ((BaseActivity) getActivity()).getDisplayState();
982     }
983 
984     @Override
getModel()985     public Model getModel() {
986         return mModel;
987     }
988 
989     @Override
isDocumentEnabled(String docMimeType, int docFlags)990     public boolean isDocumentEnabled(String docMimeType, int docFlags) {
991         return mTuner.isDocumentEnabled(docMimeType, docFlags);
992     }
993 
showEmptyDirectory()994     private void showEmptyDirectory() {
995         showEmptyView(R.string.empty, R.drawable.cabinet);
996     }
997 
showNoResults(RootInfo root)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 
showQueryError()1003     private void showQueryError() {
1004         showEmptyView(R.string.query_error, R.drawable.hourglass);
1005     }
1006 
showEmptyView(@tringRes int id, int drawable)1007     private void showEmptyView(@StringRes int id, int drawable) {
1008         showEmptyView(getContext().getResources().getText(id), drawable);
1009     }
1010 
showEmptyView(CharSequence msg, int drawable)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 
showDirectory()1023     private void showDirectory() {
1024         mEmptyView.setVisibility(View.GONE);
1025         mRecView.setVisibility(View.VISIBLE);
1026         mRecView.requestFocus();
1027     }
1028 
findCommonMimeType(List<String> mimeTypes)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 
copyFromClipboard()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 
copyFromClipData(final ClipData clipData, final DocumentInfo destination)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 
copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination)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 
copySelectedToClipboard()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 
copySelectionToClipboard(Selection selected)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 
pasteFromClipboard()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      */
canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest)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 
selectAllFiles()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      */
requestFocus()1213     public void requestFocus() {
1214         mFocusManager.restoreLastFocus();
1215     }
1216 
setupDragAndDropOnDocumentView(View view, Cursor cursor)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      */
getModelId(MotionEvent e)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      */
getModelId(View view)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 
getDraggableDocuments(View currentItemView)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 
DragShadowBuilder(Context context, IconHelper iconHelper, List<DocumentInfo> docs)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 
getIcon(List<DocumentInfo> docs)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 
getTitle(List<DocumentInfo> docs)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
onProvideShadowMetrics( Point shadowSize, Point shadowTouchPoint)1426         public void onProvideShadowMetrics(
1427                 Point shadowSize, Point shadowTouchPoint) {
1428             shadowSize.set(mWidth, mHeight);
1429             shadowTouchPoint.set(mWidth, mHeight);
1430         }
1431 
1432         @Override
onDrawShadow(Canvas canvas)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
isSelected(String modelId)1445     public boolean isSelected(String modelId) {
1446         return mSelectionManager.getSelection().contains(modelId);
1447     }
1448 
1449     private class ItemEventListener implements DocumentHolder.EventListener {
1450         @Override
onActivate(DocumentHolder doc)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
onSelect(DocumentHolder doc)1462         public boolean onSelect(DocumentHolder doc) {
1463             mSelectionManager.toggleSelection(doc.modelId);
1464             mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1465             return true;
1466         }
1467 
1468         @Override
onKey(DocumentHolder doc, int keyCode, KeyEvent event)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 
shouldExtendSelection(DocumentHolder doc, KeyEvent event)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
onModelUpdate(Model model)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
onModelUpdateFailed(Exception e)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 
ListeningGestureDetector( Context context, DragStartHelper dragHelper, GestureListener listener)1617         public ListeningGestureDetector(
1618                 Context context, DragStartHelper dragHelper, GestureListener listener) {
1619             super(context, listener);
1620             mDragHelper = dragHelper;
1621             setOnDoubleTapListener(listener);
1622         }
1623 
mouseSpawnedLastEvent()1624         boolean mouseSpawnedLastEvent() {
1625             return Events.isMouseType(mLastTool);
1626         }
1627 
touchSpawnedLastEvent()1628         boolean touchSpawnedLastEvent() {
1629             return Events.isTouchType(mLastTool);
1630         }
1631 
1632         @Override
onInterceptTouchEvent(RecyclerView rv, MotionEvent e)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
onTouchEvent(RecyclerView rv, MotionEvent e)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
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)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
onSingleTapUp(MotionEvent e)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
onLongPress(MotionEvent e)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
onDoubleTap(MotionEvent e)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 
getTarget(MotionEvent e)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 
showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim)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 
showRecentsOpen(FragmentManager fm, int anim)1716     public static void showRecentsOpen(FragmentManager fm, int anim) {
1717         create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
1718     }
1719 
reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc, String query)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 
reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc, String query)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 
create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc, String query, int anim)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 
buildStateKey(RootInfo root, DocumentInfo doc)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 
get(FragmentManager fm)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 
getFragmentId()1777     private static int getFragmentId() {
1778         return R.id.container_directory;
1779     }
1780 
1781     @Override
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)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
onLoaderReset(Loader<DirectoryResult> loader)1848     public void onLoaderReset(Loader<DirectoryResult> loader) {
1849         mModel.update(null);
1850     }
1851   }
1852