2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.documentsui.dirlist;
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;
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;
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;
108 import com.google.common.collect.Lists;
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;
120 * Display the documents inside a single directory.
122 public class DirectoryFragment extends Fragment
123 implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
125 @IntDef(flag = true, value = {
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;
134 @IntDef(flag = true, value = {
135 REQUEST_COPY_DESTINATION
137 @Retention(RetentionPolicy.SOURCE)
138 public @interface RequestCode {}
139 public static final int REQUEST_COPY_DESTINATION = 1;
141 private static final String TAG = "DirectoryFragment";
142 private static final int LOADER_ID = 42;
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;
150 private IconHelper mIconHelper;
152 private View mEmptyView;
153 private RecyclerView mRecView;
154 private ListeningGestureDetector mGestureDetector;
156 private String mStateKey;
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.
165 private LayoutInflater mInflater;
166 private MessageBar mMessageBar;
167 private View mProgressBar;
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;
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);
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() {
192 public void onViewRecycled(ViewHolder holder) {
193 cancelThumbnailTask(holder.itemView);
197 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
199 // Make the recycler and the empty views responsive to drop events.
200 mRecView.setOnDragListener(mOnDragListener);
201 mEmptyView.setOnDragListener(mOnDragListener);
207 public void onDestroyView() {
208 mSelectionManager.clearSelection();
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);
217 super.onDestroyView();
221 public void onActivityCreated(Bundle savedInstanceState) {
222 super.onActivityCreated(savedInstanceState);
224 final Context context = getActivity();
225 final State state = getDisplayState();
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);
239 mIconHelper = new IconHelper(context, MODE_GRID);
241 mAdapter = new SectionBreakDocumentsAdapterWrapper(
242 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
244 mRecView.setAdapter(mAdapter);
246 mLayout = new GridLayoutManager(getContext(), mColumnCount);
247 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
248 if (lookup != null) {
249 mLayout.setSpanSizeLookup(lookup);
251 mRecView.setLayoutManager(mLayout);
254 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
256 mRecView.addOnItemTouchListener(mGestureDetector);
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(
265 ? MultiSelectManager.MODE_MULTIPLE
266 : MultiSelectManager.MODE_SINGLE,
269 mSelectionManager.addCallback(new SelectionModeListener());
271 mModel = new Model();
272 mModel.addUpdateListener(mAdapter);
273 mModel.addUpdateListener(mModelUpdateListener);
275 // Make sure this is done after the RecyclerView is set up.
276 mFocusManager = new FocusManager(context, mRecView, mModel);
278 mTuner = FragmentTuner.pick(getContext(), state);
279 mClipper = new DocumentClipper(context);
281 final ActivityManager am = (ActivityManager) context.getSystemService(
282 Context.ACTIVITY_SERVICE);
283 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
284 mIconHelper.setThumbnailsEnabled(!svelte);
286 // Kick off loader at least once
287 getLoaderManager().restartLoader(LOADER_ID, null, this);
291 public void onSaveInstanceState(Bundle outState) {
292 super.onSaveInstanceState(outState);
294 mSelectionManager.getSelection(mSelection);
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);
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();
305 mSelection.writeToParcel(parcel, 0);
306 if (parcel.dataSize() <= 512 * 1024) {
307 outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
313 outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
317 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
318 switch (requestCode) {
319 case REQUEST_COPY_DESTINATION:
320 handleCopyResult(resultCode, data);
323 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
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.
334 @OpType int operationType = data.getIntExtra(
335 FileOperationService.EXTRA_OPERATION,
336 FileOperationService.OPERATION_COPY);
338 FileOperations.start(
340 getDisplayState().selectedDocumentsForCopy,
341 getDisplayState().stack.peek(),
342 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
346 protected boolean onDoubleTap(MotionEvent e) {
347 if (Events.isMouseEvent(e)) {
348 String id = getModelId(e);
350 return handleViewItem(id);
356 private boolean handleViewItem(String id) {
357 final Cursor cursor = mModel.getItem(id);
359 if (cursor == null) {
360 Log.w(TAG, "Can't view item. Can't obtain cursor for modeId" + id);
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();
376 public void onStop() {
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);
386 public void onDisplayStateChanged() {
387 updateDisplayState();
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);
396 public void onViewModeChanged() {
397 // Mode change is just visual change; no need to kick loader.
398 updateDisplayState();
401 private void updateDisplayState() {
402 State state = getDisplayState();
403 updateLayout(state.derivedMode);
404 mRecView.setAdapter(mAdapter);
408 * Updates the layout after the view mode switches.
409 * @param mode The new view mode.
411 private void updateLayout(@ViewMode int mode) {
412 mColumnCount = calculateColumnCount(mode);
413 if (mLayout != null) {
414 mLayout.setSpanCount(mColumnCount);
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);
424 private int calculateColumnCount(@ViewMode int mode) {
425 if (mode == MODE_LIST) {
426 // List mode is a "grid" with 1 column.
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();
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));
442 private int getDirectoryPadding(@ViewMode int mode) {
445 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
447 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
449 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
454 public int getColumnCount() {
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.
463 private final class SelectionModeListener implements MultiSelectManager.Callback,
464 ActionMode.Callback, FragmentTuner.SelectionDetails {
466 private Selection mSelected = new Selection();
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;
477 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
479 final Cursor cursor = mModel.getItem(modelId);
480 if (cursor == null) {
481 Log.w(TAG, "Can't obtain cursor for modelId: " + modelId);
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)) {
491 if (mSelected.size() >= MAX_DOCS_IN_INTENT) {
492 Snackbars.makeSnackbar(
494 R.string.too_many_selected,
495 Snackbar.LENGTH_SHORT)
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.");
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;
520 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
521 if ((docFlags & Document.FLAG_PARTIAL) != 0) {
522 mPartialCount += selected ? 1 : -1;
524 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
525 mNoDeleteCount += selected ? 1 : -1;
527 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
528 mNoRenameCount += selected ? 1 : -1;
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);
543 if (DEBUG) Log.d(TAG, "Finishing action mode.");
544 if (mActionMode != null) {
545 mActionMode.finish();
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);
558 // Called when the user exits the action mode
560 public void onDestroyActionMode(ActionMode mode) {
561 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
564 mSelectionManager.clearSelection();
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);
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);
584 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
585 mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
587 int size = mSelectionManager.getSelection().size();
588 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
589 mode.setTitle(TextUtils.formatSelectedCount(size));
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);
598 // This toolbar is not present in the fixed_layout
599 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
601 if (rootsToolbar != null) {
602 rootsToolbar.setImportantForAccessibility(
603 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
612 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
619 public boolean containsDirectories() {
620 return mDirectoryCount > 0;
624 public boolean containsPartialFiles() {
625 return mPartialCount > 0;
629 public boolean canDelete() {
630 return mNoDeleteCount == 0;
634 public boolean canRename() {
635 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
638 private void updateActionMenu() {
639 assert(mMenu != null);
640 mTuner.updateActionMenu(mMenu, this);
641 Menus.disableHiddenItems(mMenu);
645 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
646 Selection selection = mSelectionManager.getSelection(new Selection());
648 switch (item.getItemId()) {
650 openDocuments(selection);
654 case R.id.menu_share:
655 shareDocuments(selection);
656 // TODO: Only finish selection if share action is completed.
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);
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.
673 case R.id.menu_move_to:
674 // Exit selection mode first, so we avoid deselecting deleted documents.
676 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
679 case R.id.menu_copy_to_clipboard:
680 copySelectedToClipboard();
683 case R.id.menu_select_all:
687 case R.id.menu_rename:
688 // Exit selection mode first, so we avoid deselecting deleted
689 // (renamed) documents.
691 renameDocuments(selection);
695 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
701 public final boolean onBackPressed() {
702 if (mSelectionManager.hasSelection()) {
703 if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
704 mSelectionManager.clearSelection();
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);
717 private void openDocuments(final Selection selected) {
718 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
720 new GetDocumentsTask() {
722 void onDocumentsReady(List<DocumentInfo> docs) {
723 // TODO: Implement support in Files activity for opening multiple docs.
724 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
729 private void shareDocuments(final Selection selected) {
730 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
732 new GetDocumentsTask() {
734 void onDocumentsReady(List<DocumentInfo> docs) {
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);
745 if (docsForSend.size() == 1) {
746 final DocumentInfo doc = docsForSend.get(0);
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);
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);
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);
766 intent.setType(findCommonMimeType(mimeTypes));
767 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
773 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
774 startActivity(intent);
779 private String generateDeleteMessage(final List<DocumentInfo> docs) {
783 for (DocumentInfo doc : docs) {
784 if (doc.isDirectory()) {
789 if (docs.size() == 1) {
790 // Deleteing 1 file xor 1 folder in cwd
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,
798 : getActivity().getString(R.string.delete_foldername_confirmation_message,
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());
809 // Deleting mixed items (files and folders) in cwd
810 message = Shared.getQuantityString(getActivity(),
811 R.plurals.delete_items_confirmation_message, docs.size());
816 private void deleteDocuments(final Selection selected) {
817 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
819 assert(!selected.isEmpty());
821 final DocumentInfo srcParent = getDisplayState().stack.peek();
822 new GetDocumentsTask() {
824 void onDocumentsReady(final List<DocumentInfo> docs) {
827 (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
828 message.setText(generateDeleteMessage(docs));
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())
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();
854 Log.w(TAG, "Action mode is null before deleting documents.");
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);
864 .setNegativeButton(android.R.string.no, null)
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);
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,
883 DocumentsActivity.class);
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)) {
890 Shared.EXTRA_PRODUCTIVITY_MODE,
891 original.getBooleanExtra(Shared.EXTRA_PRODUCTIVITY_MODE, false));
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));
901 new GetDocumentsTask() {
903 void onDocumentsReady(List<DocumentInfo> docs) {
904 // TODO: Can this move to Fragment bundle state?
905 getDisplayState().selectedDocumentsForCopy = docs;
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);
914 // This just identifies the type of request...we'll check it
915 // when we reveive a response.
916 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
922 private static boolean hasDirectory(List<DocumentInfo> docs) {
923 for (DocumentInfo info : docs) {
924 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
931 private void renameDocuments(Selection selected) {
932 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_RENAME);
934 // Batch renaming not supported
935 // Rename option is only available in menu when 1 document selected
936 assert(selected.size() == 1);
938 new GetDocumentsTask() {
940 void onDocumentsReady(List<DocumentInfo> docs) {
941 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
947 public void initDocumentHolder(DocumentHolder holder) {
948 holder.addEventListener(mItemEventListener);
949 holder.itemView.setOnFocusChangeListener(mFocusManager);
953 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
954 setupDragAndDropOnDocumentView(holder.itemView, cursor);
958 public State getDisplayState() {
959 return ((BaseActivity) getActivity()).getDisplayState();
963 public Model getModel() {
968 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
969 return mTuner.isDocumentEnabled(docMimeType, docFlags);
972 private void showEmptyDirectory() {
973 showEmptyView(R.string.empty, R.drawable.cabinet);
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);
981 private void showQueryError() {
982 showEmptyView(R.string.query_error, R.drawable.hourglass);
985 private void showEmptyView(@StringRes int id, int drawable) {
986 showEmptyView(getContext().getResources().getText(id), drawable);
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);
996 mEmptyView.setVisibility(View.VISIBLE);
997 mEmptyView.requestFocus();
998 mRecView.setVisibility(View.GONE);
1001 private void showDirectory() {
1002 mEmptyView.setVisibility(View.GONE);
1003 mRecView.setVisibility(View.VISIBLE);
1004 mRecView.requestFocus();
1007 private String findCommonMimeType(List<String> mimeTypes) {
1008 String[] commonType = mimeTypes.get(0).split("/");
1009 if (commonType.length != 2) {
1013 for (int i = 1; i < mimeTypes.size(); i++) {
1014 String[] type = mimeTypes.get(i).split("/");
1015 if (type.length != 2) continue;
1017 if (!commonType[1].equals(type[1])) {
1018 commonType[1] = "*";
1021 if (!commonType[0].equals(type[0])) {
1022 commonType[0] = "*";
1023 commonType[1] = "*";
1028 return commonType[0] + "/" + commonType[1];
1031 private void copyFromClipboard() {
1032 new AsyncTask<Void, Void, List<DocumentInfo>>() {
1035 protected List<DocumentInfo> doInBackground(Void... params) {
1036 return mClipper.getClippedDocuments();
1040 protected void onPostExecute(List<DocumentInfo> docs) {
1041 DocumentInfo destination =
1042 ((BaseActivity) getActivity()).getCurrentDirectory();
1043 copyDocuments(docs, destination);
1048 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
1049 assert(clipData != null);
1051 new AsyncTask<Void, Void, List<DocumentInfo>>() {
1054 protected List<DocumentInfo> doInBackground(Void... params) {
1055 return mClipper.getDocumentsFromClipData(clipData);
1059 protected void onPostExecute(List<DocumentInfo> docs) {
1060 copyDocuments(docs, destination);
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(
1070 R.string.clipboard_files_cannot_paste,
1071 Snackbar.LENGTH_SHORT)
1076 if (docs.isEmpty()) {
1080 final DocumentStack curStack = getDisplayState().stack;
1081 DocumentStack tmpStack = new DocumentStack();
1082 if (destination != null) {
1083 tmpStack.push(destination);
1084 tmpStack.addAll(curStack);
1086 tmpStack = curStack;
1089 FileOperations.copy(getActivity(), docs, tmpStack);
1092 public void copySelectedToClipboard() {
1093 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
1095 Selection selection = mSelectionManager.getSelection(new Selection());
1096 if (!selection.isEmpty()) {
1097 copySelectionToClipboard(selection);
1098 mSelectionManager.clearSelection();
1102 void copySelectionToClipboard(Selection selection) {
1103 assert(!selection.isEmpty());
1104 new GetDocumentsTask() {
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();
1114 }.execute(selection);
1117 public void pasteFromClipboard() {
1118 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
1120 copyFromClipboard();
1121 getActivity().invalidateOptionsMenu();
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
1130 * @return true if the list of files can be copied to destination.
1132 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
1133 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
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()) {
1149 public void selectAllFiles() {
1150 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL);
1152 // Exclude disabled files.
1153 Set<String> enabled = new HashSet<String>();
1154 List<String> modelIds = mAdapter.getModelIds();
1156 // Get the current selection.
1157 String[] alreadySelected = mSelectionManager.getSelection().getAll();
1158 for (String id : alreadySelected) {
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);
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(
1174 R.string.too_many_in_select_all,
1175 Snackbar.LENGTH_SHORT)
1183 // Only select things currently visible in the adapter.
1184 boolean changed = mSelectionManager.setItemsSelected(enabled, true);
1186 updateDisplayState();
1191 * Attempts to restore focus on the directory listing.
1193 public void requestFocus() {
1194 mFocusManager.restoreLastFocus();
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);
1205 if (mTuner.dragAndDropEnabled()) {
1206 // Make all items draggable.
1207 view.setOnLongClickListener(onLongClickListener);
1211 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
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.
1219 // TODO: Expand drop target directory on hover?
1220 case DragEvent.ACTION_DRAG_ENTERED:
1221 setDropTargetHighlight(v, true);
1223 case DragEvent.ACTION_DRAG_EXITED:
1224 setDropTargetHighlight(v, false);
1227 case DragEvent.ACTION_DRAG_LOCATION:
1230 case DragEvent.ACTION_DRAG_ENDED:
1231 if (event.getResult()) {
1232 // Exit selection mode if the drop was handled.
1233 mSelectionManager.clearSelection();
1237 case DragEvent.ACTION_DROP:
1238 // After a drop event, always stop highlighting the target.
1239 setDropTargetHighlight(v, false);
1241 ClipData clipData = event.getClipData();
1242 if (clipData == null) {
1243 Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
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
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.");
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);
1266 copyFromClipData(clipData, dst);
1272 private DocumentInfo getDestination(View v) {
1273 String id = getModelId(v);
1275 Cursor dstCursor = mModel.getItem(id);
1276 if (dstCursor == null) {
1277 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
1280 return DocumentInfo.fromDirectoryCursor(dstCursor);
1283 if (v == mRecView || v == mEmptyView) {
1284 return getDisplayState().stack.peek();
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);
1303 * Gets the model ID for a given motion event (using the event position)
1305 private String getModelId(MotionEvent e) {
1306 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1310 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1311 if (vh instanceof DocumentHolder) {
1312 return ((DocumentHolder) vh).modelId;
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.
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;
1335 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
1336 String modelId = getModelId(currentItemView);
1337 if (modelId == null) {
1338 return Collections.EMPTY_LIST;
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;
1348 return selectedDocs;
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;
1357 return Lists.newArrayList(
1358 DocumentInfo.fromDirectoryCursor(cursor));
1361 private static class DragShadowBuilder extends View.DragShadowBuilder {
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;
1372 public DragShadowBuilder(Context context, IconHelper iconHelper, List<DocumentInfo> docs) {
1374 mIconHelper = iconHelper;
1375 mInflater = LayoutInflater.from(context);
1377 mWidth = mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
1378 mHeight= mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
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);
1384 mTitle.setText(getTitle(docs));
1385 mIcon.setImageDrawable(getIcon(docs));
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);
1394 return mContext.getDrawable(R.drawable.ic_doc_generic);
1397 private String getTitle(List<DocumentInfo> docs) {
1398 if (docs.size() == 1) {
1399 final DocumentInfo doc = docs.get(0);
1400 return doc.displayName;
1402 return Shared.getQuantityString(mContext, R.plurals.elements_dragged, docs.size());
1406 public void onProvideShadowMetrics(
1407 Point shadowSize, Point shadowTouchPoint) {
1408 shadowSize.set(mWidth, mHeight);
1409 shadowTouchPoint.set(mWidth, mHeight);
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);
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.
1428 private abstract class GetDocumentsTask
1429 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
1431 protected final List<DocumentInfo> doInBackground(Selection... selected) {
1432 return mModel.getDocuments(selected[0]);
1436 protected final void onPostExecute(List<DocumentInfo> docs) {
1437 onDocumentsReady(docs);
1440 abstract void onDocumentsReady(List<DocumentInfo> docs);
1444 public boolean isSelected(String modelId) {
1445 return mSelectionManager.getSelection().contains(modelId);
1448 private class ItemEventListener implements DocumentHolder.EventListener {
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);
1455 handleViewItem(doc.modelId);
1461 public boolean onSelect(DocumentHolder doc) {
1462 mSelectionManager.toggleSelection(doc.modelId);
1463 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
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) {
1475 // Ignore tab key events. Those should be handled by the top-level key handler.
1476 if (keyCode == KeyEvent.KEYCODE_TAB) {
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());
1489 mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1491 mSelectionManager.endRangeSelection();
1496 // Handle enter key events
1498 case KeyEvent.KEYCODE_ENTER:
1499 if (event.isShiftPressed()) {
1500 return onSelect(doc);
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);
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.
1522 private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) {
1523 if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
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);
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);
1541 private final class ModelUpdateListener implements Model.UpdateListener {
1543 public void onModelUpdate(Model model) {
1544 if (model.info != null || model.error != null) {
1545 mMessageBar.setInfo(model.info);
1546 mMessageBar.setError(model.error);
1550 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1552 if (model.isEmpty()) {
1554 showNoResults(getDisplayState().stack.root);
1556 showEmptyDirectory();
1560 mAdapter.notifyDataSetChanged();
1563 if (!model.isLoading()) {
1564 ((BaseActivity) getActivity()).notifyDirectoryLoaded(
1565 model.doc != null ? model.doc.derivedUri : null);
1570 public void onModelUpdateFailed(Exception e) {
1575 private DragStartHelper.OnDragStartListener mOnDragStartListener =
1576 new DragStartHelper.OnDragStartListener() {
1578 public boolean onDragStart(View v, DragStartHelper helper) {
1579 if (isSelected(getModelId(v))) {
1580 List<DocumentInfo> docs = getDraggableDocuments(v);
1581 if (docs.isEmpty()) {
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
1598 private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
1600 private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
1602 public boolean onLongClick(View v) {
1603 return mDragHelper.onLongClick(v);
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 {
1613 private int mLastTool = -1;
1614 private DragStartHelper mDragHelper;
1616 public ListeningGestureDetector(
1617 Context context, DragStartHelper dragHelper, GestureListener listener) {
1618 super(context, listener);
1619 mDragHelper = dragHelper;
1620 setOnDoubleTapListener(listener);
1623 boolean mouseSpawnedLastEvent() {
1624 return Events.isMouseType(mLastTool);
1627 boolean touchSpawnedLastEvent() {
1628 return Events.isTouchType(mLastTool);
1632 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1633 mLastTool = e.getToolType(0);
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)) {
1640 // Forward unhandled events to the GestureDetector.
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.
1657 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
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.
1664 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
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));
1677 // Give the DocumentHolder a crack at the event.
1678 DocumentHolder holder = getTarget(e);
1679 if (holder != null) {
1680 handled = holder.onSingleTapUp(e);
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));
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);
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);
1710 public static void showDirectory(
1711 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1712 create(fm, TYPE_NORMAL, root, doc, null, anim);
1715 public static void showRecentsOpen(FragmentManager fm, int anim) {
1716 create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
1719 public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
1721 DirectoryFragment df = get(fm);
1726 df.mSearchMode = query != null;
1727 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1730 public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1732 DirectoryFragment df = get(fm);
1737 df.mSearchMode = query != null;
1738 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
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());
1750 final FragmentTransaction ft = fm.beginTransaction();
1751 AnimationView.setupAnimations(ft, anim, args);
1753 final DirectoryFragment fragment = new DirectoryFragment();
1754 fragment.setArguments(args);
1756 ft.replace(getFragmentId(), fragment);
1757 ft.commitAllowingStateLoss();
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();
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
1776 private static int getFragmentId() {
1777 return R.id.container_directory;
1781 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1782 Context context = getActivity();
1783 State state = getDisplayState();
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);
1795 return new DirectoryLoader(
1796 context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1798 case TYPE_RECENT_OPEN:
1799 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1800 return new RecentsLoader(context, roots, state);
1803 throw new IllegalStateException("Unknown type " + mType);
1808 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1809 if (!isAdded()) return;
1812 Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
1815 State state = getDisplayState();
1817 mAdapter.notifyDataSetChanged();
1818 mModel.update(result);
1820 state.derivedSortOrder = result.sortOrder;
1822 updateLayout(state.derivedMode);
1824 if (mSelection != null) {
1825 mSelectionManager.setItemsSelected(mSelection.toList(), true);
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);
1840 mLastSortOrder = state.derivedSortOrder;
1842 mTuner.onModelLoaded(mModel, mType, mSearchMode);
1847 public void onLoaderReset(Loader<DirectoryResult> loader) {
1848 mModel.update(null);