OSDN Git Service

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