OSDN Git Service

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