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.State.MODE_GRID;
21 import static com.android.documentsui.State.MODE_LIST;
22 import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
23 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
24 import static com.android.documentsui.model.DocumentInfo.getCursorString;
26 import android.annotation.IntDef;
27 import android.annotation.StringRes;
28 import android.app.Activity;
29 import android.app.ActivityManager;
30 import android.app.AlertDialog;
31 import android.app.Fragment;
32 import android.app.FragmentManager;
33 import android.app.FragmentTransaction;
34 import android.app.LoaderManager.LoaderCallbacks;
35 import android.content.ClipData;
36 import android.content.Context;
37 import android.content.DialogInterface;
38 import android.content.Intent;
39 import android.content.Loader;
40 import android.database.Cursor;
41 import android.graphics.Canvas;
42 import android.graphics.Point;
43 import android.graphics.drawable.Drawable;
44 import android.net.Uri;
45 import android.os.AsyncTask;
46 import android.os.Bundle;
47 import android.os.Parcelable;
48 import android.provider.DocumentsContract;
49 import android.provider.DocumentsContract.Document;
50 import android.support.annotation.Nullable;
51 import android.support.design.widget.Snackbar;
52 import android.support.v13.view.DragStartHelper;
53 import android.support.v7.widget.GridLayoutManager;
54 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
55 import android.support.v7.widget.RecyclerView;
56 import android.support.v7.widget.RecyclerView.OnItemTouchListener;
57 import android.support.v7.widget.RecyclerView.RecyclerListener;
58 import android.support.v7.widget.RecyclerView.ViewHolder;
59 import android.text.TextUtils;
60 import android.util.Log;
61 import android.util.SparseArray;
62 import android.util.TypedValue;
63 import android.view.ActionMode;
64 import android.view.DragEvent;
65 import android.view.GestureDetector;
66 import android.view.HapticFeedbackConstants;
67 import android.view.KeyEvent;
68 import android.view.LayoutInflater;
69 import android.view.Menu;
70 import android.view.MenuItem;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.widget.ImageView;
75 import android.widget.Toolbar;
76 import android.widget.TextView;
78 import com.android.documentsui.BaseActivity;
79 import com.android.documentsui.DirectoryLoader;
80 import com.android.documentsui.DirectoryResult;
81 import com.android.documentsui.DocumentClipper;
82 import com.android.documentsui.DocumentsActivity;
83 import com.android.documentsui.DocumentsApplication;
84 import com.android.documentsui.Events;
85 import com.android.documentsui.Events.MotionInputEvent;
86 import com.android.documentsui.Menus;
87 import com.android.documentsui.MessageBar;
88 import com.android.documentsui.R;
89 import com.android.documentsui.RecentsLoader;
90 import com.android.documentsui.RootsCache;
91 import com.android.documentsui.Shared;
92 import com.android.documentsui.Snackbars;
93 import com.android.documentsui.State;
94 import com.android.documentsui.State.ViewMode;
95 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
96 import com.android.documentsui.model.DocumentInfo;
97 import com.android.documentsui.model.DocumentStack;
98 import com.android.documentsui.model.RootInfo;
99 import com.android.documentsui.services.FileOperationService;
100 import com.android.documentsui.services.FileOperationService.OpType;
101 import com.android.documentsui.services.FileOperations;
102 import com.google.common.collect.Lists;
104 import java.lang.annotation.Retention;
105 import java.lang.annotation.RetentionPolicy;
106 import java.util.ArrayList;
107 import java.util.Collections;
108 import java.util.List;
109 import java.util.Objects;
112 * Display the documents inside a single directory.
114 public class DirectoryFragment extends Fragment
115 implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
117 @IntDef(flag = true, value = {
121 @Retention(RetentionPolicy.SOURCE)
122 public @interface ResultType {}
123 public static final int TYPE_NORMAL = 1;
124 public static final int TYPE_RECENT_OPEN = 2;
126 @IntDef(flag = true, value = {
127 REQUEST_COPY_DESTINATION
129 @Retention(RetentionPolicy.SOURCE)
130 public @interface RequestCode {}
131 public static final int REQUEST_COPY_DESTINATION = 1;
133 private static final String TAG = "DirectoryFragment";
134 private static final int LOADER_ID = 42;
136 private Model mModel;
137 private MultiSelectManager mSelectionManager;
138 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
139 private ItemEventListener mItemEventListener = new ItemEventListener();
140 private FocusManager mFocusManager;
142 private IconHelper mIconHelper;
144 private View mEmptyView;
145 private RecyclerView mRecView;
146 private ListeningGestureDetector mGestureDetector;
148 private String mStateKey;
150 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
151 private DocumentsAdapter mAdapter;
152 private FragmentTuner mTuner;
153 private DocumentClipper mClipper;
154 private GridLayoutManager mLayout;
155 private int mColumnCount = 1; // This will get updated when layout changes.
157 private LayoutInflater mInflater;
158 private MessageBar mMessageBar;
159 private View mProgressBar;
161 // Directory fragment state is defined by: root, document, query, type, selection
162 private @ResultType int mType = TYPE_NORMAL;
163 private RootInfo mRoot;
164 private DocumentInfo mDocument;
165 private String mQuery = null;
166 private Selection mSelection = null;
167 private boolean mSearchMode = false;
168 private @Nullable ActionMode mActionMode;
171 public View onCreateView(
172 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
173 mInflater = inflater;
174 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
176 mMessageBar = MessageBar.create(getChildFragmentManager());
177 mProgressBar = view.findViewById(R.id.progressbar);
178 mEmptyView = view.findViewById(android.R.id.empty);
179 mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
180 mRecView.setRecyclerListener(
181 new RecyclerListener() {
183 public void onViewRecycled(ViewHolder holder) {
184 cancelThumbnailTask(holder.itemView);
188 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
190 // Make the recycler and the empty views responsive to drop events.
191 mRecView.setOnDragListener(mOnDragListener);
192 mEmptyView.setOnDragListener(mOnDragListener);
198 public void onDestroyView() {
199 mSelectionManager.clearSelection();
201 // Cancel any outstanding thumbnail requests
202 final int count = mRecView.getChildCount();
203 for (int i = 0; i < count; i++) {
204 final View view = mRecView.getChildAt(i);
205 cancelThumbnailTask(view);
208 super.onDestroyView();
212 public void onActivityCreated(Bundle savedInstanceState) {
213 super.onActivityCreated(savedInstanceState);
215 final Context context = getActivity();
216 final State state = getDisplayState();
218 // Read arguments when object created for the first time.
219 // Restore state if fragment recreated.
220 Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
221 mRoot = args.getParcelable(Shared.EXTRA_ROOT);
222 mDocument = args.getParcelable(Shared.EXTRA_DOC);
223 mStateKey = buildStateKey(mRoot, mDocument);
224 mQuery = args.getString(Shared.EXTRA_QUERY);
225 mType = args.getInt(Shared.EXTRA_TYPE);
226 mSelection = args.getParcelable(Shared.EXTRA_SELECTION);
227 mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
229 mIconHelper = new IconHelper(context, MODE_GRID);
231 mAdapter = new SectionBreakDocumentsAdapterWrapper(
232 this, new ModelBackedDocumentsAdapter(this, mIconHelper));
234 mRecView.setAdapter(mAdapter);
236 mLayout = new GridLayoutManager(getContext(), mColumnCount);
237 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
238 if (lookup != null) {
239 mLayout.setSpanSizeLookup(lookup);
241 mRecView.setLayoutManager(mLayout);
244 new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
246 mRecView.addOnItemTouchListener(mGestureDetector);
248 // TODO: instead of inserting the view into the constructor, extract listener-creation code
249 // and set the listener on the view after the fact. Then the view doesn't need to be passed
250 // into the selection manager.
251 mSelectionManager = new MultiSelectManager(
255 ? MultiSelectManager.MODE_MULTIPLE
256 : MultiSelectManager.MODE_SINGLE,
259 mSelectionManager.addCallback(new SelectionModeListener());
261 mModel = new Model();
262 mModel.addUpdateListener(mAdapter);
263 mModel.addUpdateListener(mModelUpdateListener);
265 // Make sure this is done after the RecyclerView is set up.
266 mFocusManager = new FocusManager(context, mRecView, mModel);
268 mTuner = FragmentTuner.pick(getContext(), state);
269 mClipper = new DocumentClipper(context);
271 final ActivityManager am = (ActivityManager) context.getSystemService(
272 Context.ACTIVITY_SERVICE);
273 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
274 mIconHelper.setThumbnailsEnabled(!svelte);
276 // Kick off loader at least once
277 getLoaderManager().restartLoader(LOADER_ID, null, this);
281 public void onSaveInstanceState(Bundle outState) {
282 super.onSaveInstanceState(outState);
284 mSelectionManager.getSelection(mSelection);
286 outState.putInt(Shared.EXTRA_TYPE, mType);
287 outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
288 outState.putParcelable(Shared.EXTRA_DOC, mDocument);
289 outState.putString(Shared.EXTRA_QUERY, mQuery);
290 outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
291 outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
296 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
297 switch(requestCode) {
298 case REQUEST_COPY_DESTINATION:
299 handleCopyResult(resultCode, data);
302 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
306 private void handleCopyResult(int resultCode, Intent data) {
307 if (resultCode == Activity.RESULT_CANCELED || data == null) {
308 // User pressed the back button or otherwise cancelled the destination pick. Don't
309 // proceed with the copy.
313 @OpType int operationType = data.getIntExtra(
314 FileOperationService.EXTRA_OPERATION,
315 FileOperationService.OPERATION_COPY);
317 FileOperations.start(
319 getDisplayState().selectedDocumentsForCopy,
320 getDisplayState().stack.peek(),
321 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
325 protected boolean onDoubleTap(MotionEvent e) {
326 if (Events.isMouseEvent(e)) {
327 String id = getModelId(e);
329 return handleViewItem(id);
335 private boolean handleViewItem(String id) {
336 final Cursor cursor = mModel.getItem(id);
338 assert(cursor != null);
340 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
341 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
342 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
343 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
344 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
345 mSelectionManager.clearSelection();
352 public void onStop() {
355 // Remember last scroll location
356 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
357 getView().saveHierarchyState(container);
358 final State state = getDisplayState();
359 state.dirState.put(mStateKey, container);
362 public void onDisplayStateChanged() {
363 updateDisplayState();
366 public void onSortOrderChanged() {
367 // Sort order is implemented as a sorting wrapper around directory
368 // results. So when sort order changes, we force a reload of the directory.
369 getLoaderManager().restartLoader(LOADER_ID, null, this);
372 public void onViewModeChanged() {
373 // Mode change is just visual change; no need to kick loader.
374 updateDisplayState();
377 private void updateDisplayState() {
378 State state = getDisplayState();
379 updateLayout(state.derivedMode);
380 mRecView.setAdapter(mAdapter);
384 * Updates the layout after the view mode switches.
385 * @param mode The new view mode.
387 private void updateLayout(@ViewMode int mode) {
388 mColumnCount = calculateColumnCount(mode);
389 if (mLayout != null) {
390 mLayout.setSpanCount(mColumnCount);
393 int pad = getDirectoryPadding(mode);
394 mRecView.setPadding(pad, pad, pad, pad);
395 mRecView.requestLayout();
396 mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
397 mIconHelper.setViewMode(mode);
400 private int calculateColumnCount(@ViewMode int mode) {
401 if (mode == MODE_LIST) {
402 // List mode is a "grid" with 1 column.
406 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
407 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
408 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
410 // RecyclerView sometimes gets a width of 0 (see b/27150284). Clamp so that we always lay
411 // out the grid with at least 2 columns.
412 int columnCount = Math.max(2,
413 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
418 private int getDirectoryPadding(@ViewMode int mode) {
421 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
423 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
425 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
430 public int getColumnCount() {
435 * Manages the integration between our ActionMode and MultiSelectManager, initiating
436 * ActionMode when there is a selection, canceling it when there is no selection,
437 * and clearing selection when action mode is explicitly exited by the user.
439 private final class SelectionModeListener
440 implements MultiSelectManager.Callback, ActionMode.Callback {
442 private Selection mSelected = new Selection();
443 private int mNoCopyCount = 0;
444 private int mNoDeleteCount = 0;
445 private int mNoRenameCount = -1;
449 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
451 final Cursor cursor = mModel.getItem(modelId);
453 assert(cursor != null);
455 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
456 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
457 return mTuner.canSelectType(docMimeType, docFlags);
463 public void onItemStateChanged(String modelId, boolean selected) {
464 final Cursor cursor = mModel.getItem(modelId);
465 if (cursor == null) {
466 Log.e(TAG, "Model returned null cursor for document: " + modelId
467 + ". Ignoring state changed event.");
471 // TODO: Should this be happening in onSelectionChanged? Technically this callback is
472 // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
473 // selection changes here)
474 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
475 if ((docFlags & Document.FLAG_PARTIAL) != 0) {
476 mNoCopyCount += selected ? 1 : -1;
478 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0
479 && (docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
480 mNoDeleteCount += selected ? 1 : -1;
482 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
483 mNoRenameCount += selected ? 1 : -1;
488 public void onSelectionChanged() {
489 mSelectionManager.getSelection(mSelected);
490 TypedValue color = new TypedValue();
491 if (mSelected.size() > 0) {
492 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
493 if (mActionMode == null) {
494 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
495 mActionMode = getActivity().startActionMode(this);
497 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
500 if (DEBUG) Log.d(TAG, "Finishing action mode.");
501 if (mActionMode != null) {
502 mActionMode.finish();
504 getActivity().getTheme().resolveAttribute(
505 android.R.attr.colorPrimaryDark, color, true);
507 getActivity().getWindow().setStatusBarColor(color.data);
509 if (mActionMode != null) {
510 final String title = Shared.getQuantityString(getActivity(),
511 R.plurals.elements_selected, mSelected.size());
512 mActionMode.setTitle(title);
513 mRecView.announceForAccessibility(title);
517 // Called when the user exits the action mode
519 public void onDestroyActionMode(ActionMode mode) {
520 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
523 mSelectionManager.clearSelection();
528 // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
529 final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
530 toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
532 // This toolbar is not present in the fixed_layout
533 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
534 if (rootsToolbar != null) {
535 rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
540 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
541 mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
543 int size = mSelectionManager.getSelection().size();
544 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
545 mode.setTitle(TextUtils.formatSelectedCount(size));
548 // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
549 // these controls when using linear navigation.
550 final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
551 toolbar.setImportantForAccessibility(
552 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
554 // This toolbar is not present in the fixed_layout
555 final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
557 if (rootsToolbar != null) {
558 rootsToolbar.setImportantForAccessibility(
559 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
568 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
574 boolean canCopySelection() {
575 return mNoCopyCount == 0;
578 boolean canDeleteSelection() {
579 return mNoDeleteCount == 0;
582 boolean canRenameSelection() {
583 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
586 private void updateActionMenu() {
587 assert(mMenu != null);
589 // Delegate update logic to our owning action, since specialized logic is desired.
590 mTuner.updateActionMenu(
591 mMenu, mType, canCopySelection(), canDeleteSelection(), canRenameSelection());
592 Menus.disableHiddenItems(mMenu);
596 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
598 Selection selection = mSelectionManager.getSelection(new Selection());
600 switch (item.getItemId()) {
602 openDocuments(selection);
606 case R.id.menu_share:
607 shareDocuments(selection);
610 case R.id.menu_delete:
611 // deleteDocuments will end action mode if the documents are deleted.
612 // It won't end action mode if user cancels the delete.
613 deleteDocuments(selection);
616 case R.id.menu_copy_to:
617 transferDocuments(selection, FileOperationService.OPERATION_COPY);
620 case R.id.menu_move_to:
621 // Exit selection mode first, so we avoid deselecting deleted documents.
623 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
626 case R.id.menu_copy_to_clipboard:
627 copySelectedToClipboard();
630 case R.id.menu_select_all:
634 case R.id.menu_rename:
635 // Exit selection mode first, so we avoid deselecting deleted
636 // (renamed) documents.
638 renameDocuments(selection);
642 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
648 public final boolean onBackPressed() {
649 if (mSelectionManager.hasSelection()) {
650 if (DEBUG) Log.d(TAG, "Clearing selection on back pressed.");
651 mSelectionManager.clearSelection();
657 private void cancelThumbnailTask(View view) {
658 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
659 if (iconThumb != null) {
660 mIconHelper.stopLoading(iconThumb);
664 private void openDocuments(final Selection selected) {
665 new GetDocumentsTask() {
667 void onDocumentsReady(List<DocumentInfo> docs) {
668 // TODO: Implement support in Files activity for opening multiple docs.
669 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
674 private void shareDocuments(final Selection selected) {
675 new GetDocumentsTask() {
677 void onDocumentsReady(List<DocumentInfo> docs) {
680 // Filter out directories - those can't be shared.
681 List<DocumentInfo> docsForSend = new ArrayList<>();
682 for (DocumentInfo doc: docs) {
683 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
684 docsForSend.add(doc);
688 if (docsForSend.size() == 1) {
689 final DocumentInfo doc = docsForSend.get(0);
691 intent = new Intent(Intent.ACTION_SEND);
692 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
693 intent.addCategory(Intent.CATEGORY_DEFAULT);
694 intent.setType(doc.mimeType);
695 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
697 } else if (docsForSend.size() > 1) {
698 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
699 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
700 intent.addCategory(Intent.CATEGORY_DEFAULT);
702 final ArrayList<String> mimeTypes = new ArrayList<>();
703 final ArrayList<Uri> uris = new ArrayList<>();
704 for (DocumentInfo doc : docsForSend) {
705 mimeTypes.add(doc.mimeType);
706 uris.add(doc.derivedUri);
709 intent.setType(findCommonMimeType(mimeTypes));
710 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
716 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
717 startActivity(intent);
722 private String generateDeleteMessage(final List<DocumentInfo> docs) {
726 for (DocumentInfo doc : docs) {
727 if (doc.isDirectory()) {
732 if (docs.size() == 1) {
733 // Deleteing 1 file xor 1 folder in cwd
734 message = dirsCount == 0
735 ? getActivity().getString(R.string.delete_filename_confirmation_message,
736 docs.get(0).displayName)
737 : getActivity().getString(R.string.delete_foldername_confirmation_message,
738 docs.get(0).displayName);
739 } else if (dirsCount == 0) {
740 // Deleting only files in cwd
741 message = Shared.getQuantityString(getActivity(),
742 R.plurals.delete_files_confirmation_message, docs.size());
743 } else if (dirsCount == docs.size()) {
744 // Deleting only folders in cwd
745 message = Shared.getQuantityString(getActivity(),
746 R.plurals.delete_folders_confirmation_message, docs.size());
748 // Deleting mixed items (files and folders) in cwd
749 message = Shared.getQuantityString(getActivity(),
750 R.plurals.delete_items_confirmation_message, docs.size());
755 private void deleteDocuments(final Selection selected) {
756 assert(!selected.isEmpty());
758 final DocumentInfo srcParent = getDisplayState().stack.peek();
759 new GetDocumentsTask() {
761 void onDocumentsReady(final List<DocumentInfo> docs) {
764 (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
765 message.setText(generateDeleteMessage(docs));
767 // This "insta-hides" files that are being deleted, because
768 // the delete operation may be not execute immediately (it
769 // may be queued up on the FileOperationService.)
770 // To hide the files locally, we call the hide method on the adapter
771 // ...which a live object...cannot be parceled.
772 // For that reason, for now, we implement this dialog NOT
773 // as a fragment (which can survive rotation and have its own state),
774 // but as a simple runtime dialog. So rotating a device with an
775 // active delete dialog...results in that dialog disappearing.
776 // We can do better, but don't have cycles for it now.
777 new AlertDialog.Builder(getActivity())
780 android.R.string.yes,
781 new DialogInterface.OnClickListener() {
782 public void onClick(DialogInterface dialog, int id) {
783 // Finish selection mode first which clears selection so we
784 // don't end up trying to deselect deleted documents.
785 // This is done here, rather in the onActionItemClicked
786 // so we can avoid de-selecting items in the case where
787 // the user cancels the delete.
788 if (mActionMode != null) {
789 mActionMode.finish();
791 // Hide the files in the UI...since the operation
792 // might be queued up on FileOperationService.
793 // We're walking a line here.
794 mAdapter.hide(selected.getAll());
795 FileOperations.delete(
796 getActivity(), docs, srcParent, getDisplayState().stack);
799 .setNegativeButton(android.R.string.no, null)
805 private void transferDocuments(final Selection selected, final @OpType int mode) {
806 // Pop up a dialog to pick a destination. This is inadequate but works for now.
807 // TODO: Implement a picker that is to spec.
808 final Intent intent = new Intent(
809 Shared.ACTION_PICK_COPY_DESTINATION,
812 DocumentsActivity.class);
814 // Set an appropriate title on the drawer when it is shown in the picker.
815 // Coupled with the fact that we auto-open the drawer for copy/move operations
816 // it should basically be the thing people see first.
817 int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
818 ? R.string.menu_move : R.string.menu_copy;
819 intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
821 new GetDocumentsTask() {
823 void onDocumentsReady(List<DocumentInfo> docs) {
824 // TODO: Can this move to Fragment bundle state?
825 getDisplayState().selectedDocumentsForCopy = docs;
827 // Determine if there is a directory in the set of documents
828 // to be copied? Why? Directory creation isn't supported by some roots
829 // (like Downloads). This informs DocumentsActivity (the "picker")
830 // to restrict available roots to just those with support.
831 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
832 intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
834 // This just identifies the type of request...we'll check it
835 // when we reveive a response.
836 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
842 private static boolean hasDirectory(List<DocumentInfo> docs) {
843 for (DocumentInfo info : docs) {
844 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
851 private void renameDocuments(Selection selected) {
852 // Batch renaming not supported
853 // Rename option is only available in menu when 1 document selected
854 assert(selected.size() == 1);
856 new GetDocumentsTask() {
858 void onDocumentsReady(List<DocumentInfo> docs) {
859 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
865 public void initDocumentHolder(DocumentHolder holder) {
866 holder.addEventListener(mItemEventListener);
867 holder.itemView.setOnFocusChangeListener(mFocusManager);
871 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
872 setupDragAndDropOnDocumentView(holder.itemView, cursor);
876 public State getDisplayState() {
877 return ((BaseActivity) getActivity()).getDisplayState();
881 public Model getModel() {
886 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
887 return mTuner.isDocumentEnabled(docMimeType, docFlags);
890 private void showEmptyDirectory() {
891 showEmptyView(R.string.empty, R.drawable.cabinet);
894 private void showNoResults(RootInfo root) {
895 CharSequence msg = getContext().getResources().getText(R.string.no_results);
896 showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
899 private void showQueryError() {
900 showEmptyView(R.string.query_error, R.drawable.hourglass);
903 private void showEmptyView(@StringRes int id, int drawable) {
904 showEmptyView(getContext().getResources().getText(id), drawable);
907 private void showEmptyView(CharSequence msg, int drawable) {
908 View content = mEmptyView.findViewById(R.id.content);
909 TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
910 ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
911 msgView.setText(msg);
912 imageView.setImageResource(drawable);
914 mEmptyView.setVisibility(View.VISIBLE);
915 mEmptyView.requestFocus();
916 mRecView.setVisibility(View.GONE);
919 private void showDirectory() {
920 mEmptyView.setVisibility(View.GONE);
921 mRecView.setVisibility(View.VISIBLE);
922 mRecView.requestFocus();
925 private String findCommonMimeType(List<String> mimeTypes) {
926 String[] commonType = mimeTypes.get(0).split("/");
927 if (commonType.length != 2) {
931 for (int i = 1; i < mimeTypes.size(); i++) {
932 String[] type = mimeTypes.get(i).split("/");
933 if (type.length != 2) continue;
935 if (!commonType[1].equals(type[1])) {
939 if (!commonType[0].equals(type[0])) {
946 return commonType[0] + "/" + commonType[1];
949 private void copyFromClipboard() {
950 new AsyncTask<Void, Void, List<DocumentInfo>>() {
953 protected List<DocumentInfo> doInBackground(Void... params) {
954 return mClipper.getClippedDocuments();
958 protected void onPostExecute(List<DocumentInfo> docs) {
959 DocumentInfo destination =
960 ((BaseActivity) getActivity()).getCurrentDirectory();
961 copyDocuments(docs, destination);
966 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
967 assert(clipData != null);
969 new AsyncTask<Void, Void, List<DocumentInfo>>() {
972 protected List<DocumentInfo> doInBackground(Void... params) {
973 return mClipper.getDocumentsFromClipData(clipData);
977 protected void onPostExecute(List<DocumentInfo> docs) {
978 copyDocuments(docs, destination);
983 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
984 BaseActivity activity = (BaseActivity) getActivity();
985 if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
986 Snackbars.makeSnackbar(
988 R.string.clipboard_files_cannot_paste,
989 Snackbar.LENGTH_SHORT)
994 if (docs.isEmpty()) {
998 final DocumentStack curStack = getDisplayState().stack;
999 DocumentStack tmpStack = new DocumentStack();
1000 if (destination != null) {
1001 tmpStack.push(destination);
1002 tmpStack.addAll(curStack);
1004 tmpStack = curStack;
1007 FileOperations.copy(getActivity(), docs, tmpStack);
1010 public void copySelectedToClipboard() {
1011 Selection selection = mSelectionManager.getSelection(new Selection());
1012 if (!selection.isEmpty()) {
1013 copySelectionToClipboard(selection);
1014 mSelectionManager.clearSelection();
1018 void copySelectionToClipboard(Selection selection) {
1019 assert(!selection.isEmpty());
1020 new GetDocumentsTask() {
1022 void onDocumentsReady(List<DocumentInfo> docs) {
1023 mClipper.clipDocuments(docs);
1024 Activity activity = getActivity();
1025 Snackbars.makeSnackbar(activity,
1026 activity.getResources().getQuantityString(
1027 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
1028 Snackbar.LENGTH_SHORT).show();
1030 }.execute(selection);
1033 public void pasteFromClipboard() {
1034 copyFromClipboard();
1035 getActivity().invalidateOptionsMenu();
1039 * Returns true if the list of files can be copied to destination. Note that this
1040 * is a policy check only. Currently the method does not attempt to verify
1041 * available space or any other environmental aspects possibly resulting in
1044 * @return true if the list of files can be copied to destination.
1046 private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
1047 if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
1051 // Can't copy folders to downloads, because we don't show folders there.
1052 if (!root.isDownloads()) {
1053 for (DocumentInfo docs : files) {
1054 if (docs.isDirectory()) {
1063 public void selectAllFiles() {
1064 // Only select things currently visible in the adapter.
1065 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
1067 updateDisplayState();
1072 * Attempts to restore focus on the directory listing.
1074 public void requestFocus() {
1075 mFocusManager.restoreLastFocus();
1078 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1079 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1080 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1081 // Make a directory item a drop target. Drop on non-directories and empty space
1082 // is handled at the list/grid view level.
1083 view.setOnDragListener(mOnDragListener);
1086 // Make all items draggable.
1087 view.setOnLongClickListener(mDragHelper);
1090 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1092 public boolean onDrag(View v, DragEvent event) {
1093 switch (event.getAction()) {
1094 case DragEvent.ACTION_DRAG_STARTED:
1095 // TODO: Check if the event contains droppable data.
1098 // TODO: Expand drop target directory on hover?
1099 case DragEvent.ACTION_DRAG_ENTERED:
1100 setDropTargetHighlight(v, true);
1102 case DragEvent.ACTION_DRAG_EXITED:
1103 setDropTargetHighlight(v, false);
1106 case DragEvent.ACTION_DRAG_LOCATION:
1109 case DragEvent.ACTION_DRAG_ENDED:
1110 if (event.getResult()) {
1111 // Exit selection mode if the drop was handled.
1112 mSelectionManager.clearSelection();
1116 case DragEvent.ACTION_DROP:
1117 // After a drop event, always stop highlighting the target.
1118 setDropTargetHighlight(v, false);
1119 // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
1120 // multi-window drag, because localState isn't carried over from one process to
1122 Object src = event.getLocalState();
1123 DocumentInfo dst = getDestination(v);
1124 if (Objects.equals(src, dst)) {
1127 copyFromClipData(event.getClipData(), dst);
1133 private DocumentInfo getDestination(View v) {
1134 String id = getModelId(v);
1136 Cursor dstCursor = mModel.getItem(id);
1137 assert(dstCursor != null);
1138 return DocumentInfo.fromDirectoryCursor(dstCursor);
1141 if (v == mRecView || v == mEmptyView) {
1142 return getDisplayState().stack.peek();
1148 private void setDropTargetHighlight(View v, boolean highlight) {
1149 // Note: use exact comparison - this code is searching for views which are children of
1150 // the RecyclerView instance in the UI.
1151 if (v.getParent() == mRecView) {
1152 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1153 if (vh instanceof DocumentHolder) {
1154 ((DocumentHolder) vh).setHighlighted(highlight);
1161 * Gets the model ID for a given motion event (using the event position)
1163 private String getModelId(MotionEvent e) {
1164 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1168 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1169 if (vh instanceof DocumentHolder) {
1170 return ((DocumentHolder) vh).modelId;
1177 * Gets the model ID for a given RecyclerView item.
1178 * @param view A View that is a document item view, or a child of a document item view.
1179 * @return The Model ID for the given document, or null if the given view is not associated with
1180 * a document item view.
1182 private String getModelId(View view) {
1183 View itemView = mRecView.findContainingItemView(view);
1184 if (itemView != null) {
1185 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1186 if (vh instanceof DocumentHolder) {
1187 return ((DocumentHolder) vh).modelId;
1193 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
1194 String modelId = getModelId(currentItemView);
1195 if (modelId == null) {
1196 return Collections.EMPTY_LIST;
1199 final List<DocumentInfo> selectedDocs =
1200 mModel.getDocuments(mSelectionManager.getSelection());
1201 if (!selectedDocs.isEmpty()) {
1202 if (!isSelected(modelId)) {
1203 // There is a selection that does not include the current item, drag nothing.
1204 return Collections.EMPTY_LIST;
1206 return selectedDocs;
1209 final Cursor cursor = mModel.getItem(modelId);
1211 assert(cursor != null);
1213 return Lists.newArrayList(
1214 DocumentInfo.fromDirectoryCursor(cursor));
1217 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1218 if (docs.size() == 1) {
1219 final DocumentInfo doc = docs.get(0);
1220 return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1221 doc.mimeType, doc.icon);
1223 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1226 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1228 private final Drawable mShadow;
1230 private final int mShadowDimension;
1232 public DrawableShadowBuilder(Drawable shadow) {
1234 mShadowDimension = getResources().getDimensionPixelSize(
1235 R.dimen.drag_shadow_size);
1236 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1240 public void onProvideShadowMetrics(
1241 Point shadowSize, Point shadowTouchPoint) {
1242 shadowSize.set(mShadowDimension, mShadowDimension);
1243 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1247 public void onDrawShadow(Canvas canvas) {
1248 mShadow.draw(canvas);
1253 * Abstract task providing support for loading documents *off*
1254 * the main thread. And if it isn't obvious, creating a list
1255 * of documents (especially large lists) can be pretty expensive.
1257 private abstract class GetDocumentsTask
1258 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
1260 protected final List<DocumentInfo> doInBackground(Selection... selected) {
1261 return mModel.getDocuments(selected[0]);
1265 protected final void onPostExecute(List<DocumentInfo> docs) {
1266 onDocumentsReady(docs);
1269 abstract void onDocumentsReady(List<DocumentInfo> docs);
1273 public boolean isSelected(String modelId) {
1274 return mSelectionManager.getSelection().contains(modelId);
1277 private class ItemEventListener implements DocumentHolder.EventListener {
1279 public boolean onActivate(DocumentHolder doc) {
1280 // Toggle selection if we're in selection mode, othewise, view item.
1281 if (mSelectionManager.hasSelection()) {
1282 mSelectionManager.toggleSelection(doc.modelId);
1284 handleViewItem(doc.modelId);
1290 public boolean onSelect(DocumentHolder doc) {
1291 mSelectionManager.toggleSelection(doc.modelId);
1292 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1297 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1298 // Only handle key-down events. This is simpler, consistent with most other UIs, and
1299 // enables the handling of repeated key events from holding down a key.
1300 if (event.getAction() != KeyEvent.ACTION_DOWN) {
1304 // Ignore tab key events. Those should be handled by the top-level key handler.
1305 if (keyCode == KeyEvent.KEYCODE_TAB) {
1309 if (mFocusManager.handleKey(doc, keyCode, event)) {
1310 // Handle range selection adjustments. Extending the selection will adjust the
1311 // bounds of the in-progress range selection. Each time an unshifted navigation
1312 // event is received, the range selection is restarted.
1313 if (shouldExtendSelection(event)) {
1314 if (!mSelectionManager.isRangeSelectionActive()) {
1315 // Start a range selection if one isn't active
1316 mSelectionManager.startRangeSelection(doc.getAdapterPosition());
1318 mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1320 mSelectionManager.endRangeSelection();
1325 // Handle enter key events
1327 case KeyEvent.KEYCODE_ENTER:
1328 if (event.isShiftPressed()) {
1329 return onSelect(doc);
1331 // For non-shifted enter keypresses, fall through.
1332 case KeyEvent.KEYCODE_DPAD_CENTER:
1333 case KeyEvent.KEYCODE_BUTTON_A:
1334 return onActivate(doc);
1335 case KeyEvent.KEYCODE_FORWARD_DEL:
1336 // This has to be handled here instead of in a keyboard shortcut, because
1337 // keyboard shortcuts all have to be modified with the 'Ctrl' key.
1338 if (mSelectionManager.hasSelection()) {
1339 deleteDocuments(mSelectionManager.getSelection());
1341 // Always handle the key, even if there was nothing to delete. This is a
1342 // precaution to prevent other handlers from potentially picking up the event
1343 // and triggering extra behaviours.
1350 private boolean shouldExtendSelection(KeyEvent event) {
1351 return Events.isNavigationKeyCode(event.getKeyCode()) &&
1352 event.isShiftPressed();
1356 private final class ModelUpdateListener implements Model.UpdateListener {
1358 public void onModelUpdate(Model model) {
1359 if (model.info != null || model.error != null) {
1360 mMessageBar.setInfo(model.info);
1361 mMessageBar.setError(model.error);
1365 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1367 if (model.isEmpty()) {
1369 showNoResults(getDisplayState().stack.root);
1371 showEmptyDirectory();
1375 mAdapter.notifyDataSetChanged();
1378 if (!model.isLoading()) {
1379 ((BaseActivity) getActivity()).notifyDirectoryLoaded(
1380 model.doc != null ? model.doc.derivedUri : null);
1385 public void onModelUpdateFailed(Exception e) {
1390 private DragStartHelper mDragHelper = new DragStartHelper(null) {
1392 protected boolean onDragStart(View v) {
1393 if (isSelected(getModelId(v))) {
1394 List<DocumentInfo> docs = getDraggableDocuments(v);
1395 if (docs.isEmpty()) {
1399 mClipper.getClipDataForDocuments(docs),
1400 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1401 getDisplayState().stack.peek(),
1402 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1403 View.DRAG_FLAG_GLOBAL_URI_WRITE
1412 // Previously we listened to events with one class, only to bounce them forward
1413 // to GestureDetector. We're still doing that here, but with a single class
1414 // that reduces overall complexity in our glue code.
1415 private static final class ListeningGestureDetector extends GestureDetector
1416 implements OnItemTouchListener {
1418 private int mLastTool = -1;
1419 private DragStartHelper mDragHelper;
1421 public ListeningGestureDetector(
1422 Context context, DragStartHelper dragHelper, GestureListener listener) {
1423 super(context, listener);
1424 mDragHelper = dragHelper;
1425 setOnDoubleTapListener(listener);
1428 boolean mouseSpawnedLastEvent() {
1429 return Events.isMouseType(mLastTool);
1432 boolean touchSpawnedLastEvent() {
1433 return Events.isTouchType(mLastTool);
1437 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1438 mLastTool = e.getToolType(0);
1440 // Detect drag events. When a drag is detected, intercept the rest of the gesture.
1441 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1442 if (itemView != null && mDragHelper.handleTouch(itemView, e)) {
1445 // Forward unhandled events to the GestureDetector.
1452 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
1453 View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1454 mDragHelper.handleTouch(itemView, e);
1455 // Note: even though this event is being handled as part of a drag gesture, continue
1456 // forwarding to the GestureDetector. The detector needs to see the entire cluster of
1457 // events in order to properly interpret gestures.
1462 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
1466 * The gesture listener for items in the list/grid view. Interprets gestures and sends the
1467 * events to the target DocumentHolder, whence they are routed to the appropriate listener.
1469 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
1471 public boolean onSingleTapUp(MotionEvent e) {
1472 // Single tap logic:
1473 // If the selection manager is active, it gets first whack at handling tap
1474 // events. Otherwise, tap events are routed to the target DocumentHolder.
1475 boolean handled = mSelectionManager.onSingleTapUp(
1476 new MotionInputEvent(e, mRecView));
1482 // Give the DocumentHolder a crack at the event.
1483 DocumentHolder holder = getTarget(e);
1484 if (holder != null) {
1485 handled = holder.onSingleTapUp(e);
1492 public void onLongPress(MotionEvent e) {
1493 // Long-press events get routed directly to the selection manager. They can be
1494 // changed to route through the DocumentHolder if necessary.
1495 mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
1499 public boolean onDoubleTap(MotionEvent e) {
1500 // Double-tap events are handled directly by the DirectoryFragment. They can be changed
1501 // to route through the DocumentHolder if necessary.
1502 return DirectoryFragment.this.onDoubleTap(e);
1505 private @Nullable DocumentHolder getTarget(MotionEvent e) {
1506 View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
1507 if (childView != null) {
1508 return (DocumentHolder) mRecView.getChildViewHolder(childView);
1515 public static void showDirectory(
1516 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1517 create(fm, TYPE_NORMAL, root, doc, null, anim);
1520 public static void showRecentsOpen(FragmentManager fm, int anim) {
1521 create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
1524 public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
1526 DirectoryFragment df = get(fm);
1531 df.mSearchMode = query != null;
1532 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1535 public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1537 DirectoryFragment df = get(fm);
1542 df.mSearchMode = query != null;
1543 df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1546 public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1547 String query, int anim) {
1548 final Bundle args = new Bundle();
1549 args.putInt(Shared.EXTRA_TYPE, type);
1550 args.putParcelable(Shared.EXTRA_ROOT, root);
1551 args.putParcelable(Shared.EXTRA_DOC, doc);
1552 args.putString(Shared.EXTRA_QUERY, query);
1553 args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
1555 final FragmentTransaction ft = fm.beginTransaction();
1556 AnimationView.setupAnimations(ft, anim, args);
1558 final DirectoryFragment fragment = new DirectoryFragment();
1559 fragment.setArguments(args);
1561 ft.replace(getFragmentId(), fragment);
1562 ft.commitAllowingStateLoss();
1565 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1566 final StringBuilder builder = new StringBuilder();
1567 builder.append(root != null ? root.authority : "null").append(';');
1568 builder.append(root != null ? root.rootId : "null").append(';');
1569 builder.append(doc != null ? doc.documentId : "null");
1570 return builder.toString();
1573 public static @Nullable DirectoryFragment get(FragmentManager fm) {
1574 // TODO: deal with multiple directories shown at once
1575 Fragment fragment = fm.findFragmentById(getFragmentId());
1576 return fragment instanceof DirectoryFragment
1577 ? (DirectoryFragment) fragment
1581 private static int getFragmentId() {
1582 return R.id.container_directory;
1586 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1587 Context context = getActivity();
1588 State state = getDisplayState();
1593 contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
1594 mRoot.authority, mRoot.rootId, mQuery)
1595 : DocumentsContract.buildChildDocumentsUri(
1596 mDocument.authority, mDocument.documentId);
1597 if (mTuner.enableManagedMode()) {
1598 contentsUri = DocumentsContract.setManageMode(contentsUri);
1600 return new DirectoryLoader(
1601 context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1603 case TYPE_RECENT_OPEN:
1604 final RootsCache roots = DocumentsApplication.getRootsCache(context);
1605 return new RecentsLoader(context, roots, state);
1608 throw new IllegalStateException("Unknown type " + mType);
1613 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1614 if (!isAdded()) return;
1616 State state = getDisplayState();
1618 mAdapter.notifyDataSetChanged();
1619 mModel.update(result);
1621 state.derivedSortOrder = result.sortOrder;
1623 updateLayout(state.derivedMode);
1625 if (mSelection != null) {
1626 mSelectionManager.setItemsSelected(mSelection.toList(), true);
1629 // Restore any previous instance state
1630 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
1631 if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
1632 getView().restoreHierarchyState(container);
1633 } else if (mLastSortOrder != state.derivedSortOrder) {
1634 // The derived sort order takes the user sort order into account, but applies
1635 // directory-specific defaults when the user doesn't explicitly set the sort
1636 // order. Scroll to the top if the sort order actually changed.
1637 mRecView.smoothScrollToPosition(0);
1640 mLastSortOrder = state.derivedSortOrder;
1642 mTuner.onModelLoaded(mModel, mType, mSearchMode);
1647 public void onLoaderReset(Loader<DirectoryResult> loader) {
1648 mModel.update(null);