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.ACTION_MANAGE;
21 import static com.android.documentsui.State.MODE_GRID;
22 import static com.android.documentsui.State.MODE_LIST;
23 import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
24 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
25 import static com.android.documentsui.model.DocumentInfo.getCursorString;
26 import static com.android.internal.util.Preconditions.checkNotNull;
27 import static com.android.internal.util.Preconditions.checkState;
28 import static com.google.common.base.Preconditions.checkArgument;
30 import android.annotation.IntDef;
31 import android.annotation.StringRes;
32 import android.app.Activity;
33 import android.app.ActivityManager;
34 import android.app.Fragment;
35 import android.app.FragmentManager;
36 import android.app.FragmentTransaction;
37 import android.app.LoaderManager.LoaderCallbacks;
38 import android.content.ClipData;
39 import android.content.ContentResolver;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.Loader;
43 import android.database.Cursor;
44 import android.graphics.Canvas;
45 import android.graphics.Point;
46 import android.graphics.drawable.Drawable;
47 import android.net.Uri;
48 import android.os.AsyncTask;
49 import android.os.Bundle;
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.v7.widget.GridLayoutManager;
56 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
57 import android.support.v7.widget.RecyclerView;
58 import android.support.v7.widget.RecyclerView.OnItemTouchListener;
59 import android.support.v7.widget.RecyclerView.RecyclerListener;
60 import android.support.v7.widget.RecyclerView.ViewHolder;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.util.SparseArray;
64 import android.util.TypedValue;
65 import android.view.ActionMode;
66 import android.view.DragEvent;
67 import android.view.GestureDetector;
68 import android.view.KeyEvent;
69 import android.view.LayoutInflater;
70 import android.view.Menu;
71 import android.view.MenuItem;
72 import android.view.MotionEvent;
73 import android.view.View;
74 import android.view.ViewGroup;
75 import android.view.ViewParent;
76 import android.widget.ImageView;
77 import android.widget.TextView;
79 import com.android.documentsui.BaseActivity;
80 import com.android.documentsui.DirectoryLoader;
81 import com.android.documentsui.DirectoryResult;
82 import com.android.documentsui.DocumentClipper;
83 import com.android.documentsui.DocumentsActivity;
84 import com.android.documentsui.DocumentsApplication;
85 import com.android.documentsui.Events;
86 import com.android.documentsui.Events.MotionInputEvent;
87 import com.android.documentsui.Menus;
88 import com.android.documentsui.MessageBar;
89 import com.android.documentsui.MimePredicate;
90 import com.android.documentsui.R;
91 import com.android.documentsui.RecentLoader;
92 import com.android.documentsui.RootsCache;
93 import com.android.documentsui.Shared;
94 import com.android.documentsui.Snackbars;
95 import com.android.documentsui.State;
96 import com.android.documentsui.State.ViewMode;
97 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
98 import com.android.documentsui.model.DocumentInfo;
99 import com.android.documentsui.model.DocumentStack;
100 import com.android.documentsui.model.RootInfo;
101 import com.android.documentsui.services.FileOperationService;
102 import com.android.documentsui.services.FileOperationService.OpType;
103 import com.android.documentsui.services.FileOperations;
105 import com.google.common.collect.Lists;
107 import java.lang.annotation.Retention;
108 import java.lang.annotation.RetentionPolicy;
109 import java.util.ArrayList;
110 import java.util.Collections;
111 import java.util.List;
114 * Display the documents inside a single directory.
116 public class DirectoryFragment extends Fragment implements DocumentsAdapter.Environment {
118 @IntDef(flag = true, value = {
123 @Retention(RetentionPolicy.SOURCE)
124 public @interface ResultType {}
125 public static final int TYPE_NORMAL = 1;
126 public static final int TYPE_SEARCH = 2;
127 public static final int TYPE_RECENT_OPEN = 3;
129 public static final int ANIM_NONE = 1;
130 public static final int ANIM_SIDE = 2;
131 public static final int ANIM_LEAVE = 3;
132 public static final int ANIM_ENTER = 4;
134 public static final int REQUEST_COPY_DESTINATION = 1;
136 static final boolean DEBUG_ENABLE_DND = true;
138 private static final String TAG = "DirectoryFragment";
139 private static final int LOADER_ID = 42;
140 private static final int DELETE_UNDO_TIMEOUT = 5000;
141 private static final int DELETE_JOB_DELAY = 5500;
142 private static final int EMPTY_REVEAL_DURATION = 250;
144 private static final String EXTRA_TYPE = "type";
145 private static final String EXTRA_ROOT = "root";
146 private static final String EXTRA_DOC = "doc";
147 private static final String EXTRA_QUERY = "query";
148 private static final String EXTRA_IGNORE_STATE = "ignoreState";
150 private Model mModel;
151 private MultiSelectManager mSelectionManager;
152 private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
153 private ItemEventListener mItemEventListener = new ItemEventListener();
155 private IconHelper mIconHelper;
157 private View mEmptyView;
158 private RecyclerView mRecView;
159 private ListeningGestureDetector mGestureDetector;
161 private @ResultType int mType = TYPE_NORMAL;
162 private String mStateKey;
164 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
165 private DocumentsAdapter mAdapter;
166 private LoaderCallbacks<DirectoryResult> mCallbacks;
167 private FragmentTuner mTuner;
168 private DocumentClipper mClipper;
169 private GridLayoutManager mLayout;
170 private int mColumnCount = 1; // This will get updated when layout changes.
172 private MessageBar mMessageBar;
173 private View mProgressBar;
176 public View onCreateView(
177 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
178 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
180 mMessageBar = MessageBar.create(getChildFragmentManager());
181 mProgressBar = view.findViewById(R.id.progressbar);
183 mEmptyView = view.findViewById(android.R.id.empty);
185 mRecView = (RecyclerView) view.findViewById(R.id.list);
186 mRecView.setRecyclerListener(
187 new RecyclerListener() {
189 public void onViewRecycled(ViewHolder holder) {
190 cancelThumbnailTask(holder.itemView);
194 mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
196 // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
197 if (DEBUG_ENABLE_DND) {
198 setupDragAndDropOnDirectoryView(mRecView);
205 public void onDestroyView() {
206 super.onDestroyView();
208 // Cancel any outstanding thumbnail requests
209 final int count = mRecView.getChildCount();
210 for (int i = 0; i < count; i++) {
211 final View view = mRecView.getChildAt(i);
212 cancelThumbnailTask(view);
215 // Clear any outstanding selection
216 mSelectionManager.clearSelection();
220 public void onActivityCreated(Bundle savedInstanceState) {
221 super.onActivityCreated(savedInstanceState);
223 final Context context = getActivity();
224 final State state = getDisplayState();
226 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
227 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
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);
243 mGestureDetector = new ListeningGestureDetector(this.getContext(), new GestureListener());
245 mRecView.addOnItemTouchListener(mGestureDetector);
247 // TODO: instead of inserting the view into the constructor, extract listener-creation code
248 // and set the listener on the view after the fact. Then the view doesn't need to be passed
249 // into the selection manager.
250 mSelectionManager = new MultiSelectManager(
254 ? MultiSelectManager.MODE_MULTIPLE
255 : MultiSelectManager.MODE_SINGLE);
256 mSelectionManager.addCallback(new SelectionModeListener());
258 mModel = new Model();
259 mModel.addUpdateListener(mAdapter);
260 mModel.addUpdateListener(mModelUpdateListener);
262 mType = getArguments().getInt(EXTRA_TYPE);
263 mStateKey = buildStateKey(root, doc);
265 mTuner = FragmentTuner.pick(getContext(), state);
266 mClipper = new DocumentClipper(context);
268 boolean hideGridTitles;
269 if (mType == TYPE_RECENT_OPEN) {
270 // Hide titles when showing recents for picking images/videos
271 hideGridTitles = MimePredicate.mimeMatches(
272 MimePredicate.VISUAL_MIMES, state.acceptMimes);
274 hideGridTitles = (doc != null) && doc.isGridTitlesHidden();
276 GridDocumentHolder.setHideTitles(hideGridTitles);
278 final ActivityManager am = (ActivityManager) context.getSystemService(
279 Context.ACTIVITY_SERVICE);
280 boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
281 mIconHelper.setThumbnailsEnabled(!svelte);
283 mCallbacks = new LoaderCallbacks<DirectoryResult>() {
285 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
286 final String query = getArguments().getString(EXTRA_QUERY);
291 contentsUri = DocumentsContract.buildChildDocumentsUri(
292 doc.authority, doc.documentId);
293 if (state.action == ACTION_MANAGE) {
294 contentsUri = DocumentsContract.setManageMode(contentsUri);
296 return new DirectoryLoader(
297 context, mType, root, doc, contentsUri, state.userSortOrder);
299 contentsUri = DocumentsContract.buildSearchDocumentsUri(
300 root.authority, root.rootId, query);
301 if (state.action == ACTION_MANAGE) {
302 contentsUri = DocumentsContract.setManageMode(contentsUri);
304 return new DirectoryLoader(
305 context, mType, root, doc, contentsUri, state.userSortOrder);
306 case TYPE_RECENT_OPEN:
307 final RootsCache roots = DocumentsApplication.getRootsCache(context);
308 return new RecentLoader(context, roots, state);
310 throw new IllegalStateException("Unknown type " + mType);
315 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
316 if (!isAdded()) return;
318 mModel.update(result);
319 state.derivedSortOrder = result.sortOrder;
321 updateDisplayState();
323 // Restore any previous instance state
324 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
325 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
326 getView().restoreHierarchyState(container);
327 } else if (mLastSortOrder != state.derivedSortOrder) {
328 // The derived sort order takes the user sort order into account, but applies
329 // directory-specific defaults when the user doesn't explicitly set the sort
330 // order. Scroll to the top if the sort order actually changed.
331 mRecView.smoothScrollToPosition(0);
334 mLastSortOrder = state.derivedSortOrder;
336 mTuner.onModelLoaded(mModel, mType);
340 public void onLoaderReset(Loader<DirectoryResult> loader) {
345 // Kick off loader at least once
346 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
350 public void onActivityResult(int requestCode, int resultCode, Intent data) {
351 // There's only one request code right now. Replace this with a switch statement or
352 // something more scalable when more codes are added.
353 if (requestCode != REQUEST_COPY_DESTINATION) {
356 if (resultCode == Activity.RESULT_CANCELED || data == null) {
357 // User pressed the back button or otherwise cancelled the destination pick. Don't
358 // proceed with the copy.
362 int operationType = data.getIntExtra(
363 FileOperationService.EXTRA_OPERATION,
364 FileOperationService.OPERATION_COPY);
366 FileOperations.start(
368 getDisplayState().selectedDocumentsForCopy,
369 getDisplayState().stack.peek(),
370 (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
374 protected boolean onDoubleTap(MotionEvent e) {
375 if (Events.isMouseEvent(e)) {
376 String id = getModelId(e);
378 return handleViewItem(id);
384 private boolean handleViewItem(String id) {
385 final Cursor cursor = mModel.getItem(id);
386 checkNotNull(cursor, "Cursor cannot be null.");
387 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
388 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
389 if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
390 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
391 ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
392 mSelectionManager.clearSelection();
399 public void onStop() {
402 // Remember last scroll location
403 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
404 getView().saveHierarchyState(container);
405 final State state = getDisplayState();
406 state.dirState.put(mStateKey, container);
409 public void onDisplayStateChanged() {
410 updateDisplayState();
413 public void onSortOrderChanged() {
414 // Sort order is implemented as a sorting wrapper around directory
415 // results. So when sort order changes, we force a reload of the directory.
416 getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks);
419 public void onViewModeChanged() {
420 // Mode change is just visual change; no need to kick loader.
421 updateDisplayState();
424 private void updateDisplayState() {
425 State state = getDisplayState();
426 updateLayout(state.derivedMode);
427 mRecView.setAdapter(mAdapter);
431 * Updates the layout after the view mode switches.
432 * @param mode The new view mode.
434 private void updateLayout(@ViewMode int mode) {
435 mColumnCount = calculateColumnCount(mode);
436 if (mLayout != null) {
437 mLayout.setSpanCount(mColumnCount);
440 int pad = getDirectoryPadding(mode);
441 mRecView.setPadding(pad, pad, pad, pad);
442 mRecView.requestLayout();
443 mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
444 mIconHelper.setViewMode(mode);
447 private int calculateColumnCount(@ViewMode int mode) {
448 if (mode == MODE_LIST) {
449 // List mode is a "grid" with 1 column.
453 int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
454 int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
455 int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
457 checkState(mRecView.getWidth() > 0);
458 int columnCount = Math.max(1,
459 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
464 private int getDirectoryPadding(@ViewMode int mode) {
467 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
469 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
471 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
476 public int getColumnCount() {
481 * Manages the integration between our ActionMode and MultiSelectManager, initiating
482 * ActionMode when there is a selection, canceling it when there is no selection,
483 * and clearing selection when action mode is explicitly exited by the user.
485 private final class SelectionModeListener
486 implements MultiSelectManager.Callback, ActionMode.Callback {
488 private Selection mSelected = new Selection();
489 private ActionMode mActionMode;
490 private int mNoDeleteCount = 0;
491 private int mNoRenameCount = -1;
495 public boolean onBeforeItemStateChange(String modelId, boolean selected) {
497 final Cursor cursor = mModel.getItem(modelId);
498 checkNotNull(cursor, "Cursor cannot be null.");
499 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
500 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
501 return mTuner.canSelectType(docMimeType, docFlags);
507 public void onItemStateChanged(String modelId, boolean selected) {
508 final Cursor cursor = mModel.getItem(modelId);
509 checkNotNull(cursor, "Cursor cannot be null.");
511 // TODO: Should this be happening in onSelectionChanged? Technically this callback is
512 // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
513 // selection changes here)
514 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
515 if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
516 mNoDeleteCount += selected ? 1 : -1;
518 if ((docFlags & Document.FLAG_SUPPORTS_RENAME) != 0) {
519 mNoRenameCount += selected ? 1 : -1;
524 public void onSelectionChanged() {
525 mSelectionManager.getSelection(mSelected);
526 TypedValue color = new TypedValue();
527 if (mSelected.size() > 0) {
528 if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
529 if (mActionMode == null) {
530 if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
531 mActionMode = getActivity().startActionMode(this);
533 getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
536 if (DEBUG) Log.d(TAG, "Finishing action mode.");
537 if (mActionMode != null) {
538 mActionMode.finish();
540 getActivity().getTheme().resolveAttribute(
541 android.R.attr.colorPrimaryDark, color, true);
543 getActivity().getWindow().setStatusBarColor(color.data);
545 if (mActionMode != null) {
546 mActionMode.setTitle(String.valueOf(mSelected.size()));
550 // Called when the user exits the action mode
552 public void onDestroyActionMode(ActionMode mode) {
553 if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
556 mSelectionManager.clearSelection();
563 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
564 int size = mSelectionManager.getSelection().size();
565 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
566 mode.setTitle(TextUtils.formatSelectedCount(size));
571 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
577 boolean canRenameSelection() {
578 return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
581 boolean canDeleteSelection() {
582 return mNoDeleteCount == 0;
585 private void updateActionMenu() {
588 // Delegate update logic to our owning action, since specialized logic is desired.
589 mTuner.updateActionMenu(mMenu, mType, canDeleteSelection(), canRenameSelection());
590 Menus.disableHiddenItems(mMenu);
594 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
596 Selection selection = mSelectionManager.getSelection(new Selection());
598 switch (item.getItemId()) {
600 openDocuments(selection);
604 case R.id.menu_share:
605 shareDocuments(selection);
609 case R.id.menu_delete:
610 // Exit selection mode first, so we avoid deselecting deleted documents.
612 deleteDocuments(selection);
615 case R.id.menu_copy_to:
616 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 renameDocuments(selection);
640 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
646 public final boolean onBackPressed() {
647 if (mSelectionManager.hasSelection()) {
648 if (DEBUG) Log.d(TAG, "Clearing selection on back pressed.");
649 mSelectionManager.clearSelection();
655 private void cancelThumbnailTask(View view) {
656 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
657 if (iconThumb != null) {
658 mIconHelper.stopLoading(iconThumb);
662 private void openDocuments(final Selection selected) {
663 new GetDocumentsTask() {
665 void onDocumentsReady(List<DocumentInfo> docs) {
666 // TODO: Implement support in Files activity for opening multiple docs.
667 BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
672 private void shareDocuments(final Selection selected) {
673 new GetDocumentsTask() {
675 void onDocumentsReady(List<DocumentInfo> docs) {
678 // Filter out directories - those can't be shared.
679 List<DocumentInfo> docsForSend = new ArrayList<>();
680 for (DocumentInfo doc: docs) {
681 if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
682 docsForSend.add(doc);
686 if (docsForSend.size() == 1) {
687 final DocumentInfo doc = docsForSend.get(0);
689 intent = new Intent(Intent.ACTION_SEND);
690 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
691 intent.addCategory(Intent.CATEGORY_DEFAULT);
692 intent.setType(doc.mimeType);
693 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
695 } else if (docsForSend.size() > 1) {
696 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
697 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
698 intent.addCategory(Intent.CATEGORY_DEFAULT);
700 final ArrayList<String> mimeTypes = new ArrayList<>();
701 final ArrayList<Uri> uris = new ArrayList<>();
702 for (DocumentInfo doc : docsForSend) {
703 mimeTypes.add(doc.mimeType);
704 uris.add(doc.derivedUri);
707 intent.setType(findCommonMimeType(mimeTypes));
708 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
714 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
715 startActivity(intent);
720 private void deleteDocuments(final Selection selected) {
722 checkArgument(!selected.isEmpty());
723 final DocumentInfo srcParent = getDisplayState().stack.peek();
724 new GetDocumentsTask() {
726 void onDocumentsReady(List<DocumentInfo> docs) {
727 // Hide the files in the UI.
728 final SparseArray<String> hidden = mAdapter.hide(selected.getAll());
730 checkState(DELETE_JOB_DELAY > DELETE_UNDO_TIMEOUT);
731 String operationId = FileOperations.delete(
732 getActivity(), docs, srcParent, getDisplayState().stack,
734 showDeleteSnackbar(hidden, operationId);
739 private void showDeleteSnackbar(final SparseArray<String> hidden, final String jobId) {
741 Context context = getActivity();
742 String message = Shared.getQuantityString(context, R.plurals.deleting, hidden.size());
744 // Show a snackbar informing the user that files will be deleted, and give them an option to
746 final Activity activity = getActivity();
747 Snackbars.makeSnackbar(activity, message, DELETE_UNDO_TIMEOUT)
750 new View.OnClickListener() {
752 public void onClick(View view) {}
755 new Snackbar.Callback() {
757 public void onDismissed(Snackbar snackbar, int event) {
758 if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
759 // If the delete was cancelled, just unhide the files.
760 FileOperations.cancel(activity, jobId);
761 mAdapter.unhide(hidden);
768 private void transferDocuments(final Selection selected, final @OpType int mode) {
769 // Pop up a dialog to pick a destination. This is inadequate but works for now.
770 // TODO: Implement a picker that is to spec.
771 final Intent intent = new Intent(
772 Shared.ACTION_PICK_COPY_DESTINATION,
775 DocumentsActivity.class);
777 new GetDocumentsTask() {
779 void onDocumentsReady(List<DocumentInfo> docs) {
780 getDisplayState().selectedDocumentsForCopy = docs;
782 boolean directoryCopy = false;
783 for (DocumentInfo info : docs) {
784 if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
785 directoryCopy = true;
789 intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, directoryCopy);
790 intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
791 startActivityForResult(intent, REQUEST_COPY_DESTINATION);
796 private void renameDocuments(Selection selected) {
797 // Batch renaming not supported
798 // Rename option is only available in menu when 1 document selected
799 checkArgument(selected.size() == 1);
801 new GetDocumentsTask() {
803 void onDocumentsReady(List<DocumentInfo> docs) {
804 RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
810 public void initDocumentHolder(DocumentHolder holder) {
811 holder.addEventListener(mItemEventListener);
815 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
816 if (DEBUG_ENABLE_DND) {
817 setupDragAndDropOnDocumentView(holder.itemView, cursor);
822 public State getDisplayState() {
823 return ((BaseActivity) getActivity()).getDisplayState();
827 public Model getModel() {
832 public boolean isDocumentEnabled(String docMimeType, int docFlags) {
833 return mTuner.isDocumentEnabled(docMimeType, docFlags);
836 private void showEmptyDirectory() {
837 showEmptyView(R.string.empty, R.drawable.cabinet);
840 private void showNoResults(RootInfo root) {
841 CharSequence msg = getContext().getResources().getText(R.string.no_results);
842 showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
845 private void showQueryError() {
846 showEmptyView(R.string.query_error, R.drawable.hourglass);
849 private void showEmptyView(@StringRes int id, int drawable) {
850 showEmptyView(getContext().getResources().getText(id), drawable);
853 private void showEmptyView(CharSequence msg, int drawable) {
854 View content = mEmptyView.findViewById(R.id.content);
855 TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
856 ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
857 msgView.setText(msg);
858 imageView.setImageResource(drawable);
860 mEmptyView.setVisibility(View.VISIBLE);
861 mRecView.setVisibility(View.GONE);
864 private void showDirectory() {
865 mEmptyView.setVisibility(View.GONE);
866 mRecView.setVisibility(View.VISIBLE);
869 private String findCommonMimeType(List<String> mimeTypes) {
870 String[] commonType = mimeTypes.get(0).split("/");
871 if (commonType.length != 2) {
875 for (int i = 1; i < mimeTypes.size(); i++) {
876 String[] type = mimeTypes.get(i).split("/");
877 if (type.length != 2) continue;
879 if (!commonType[1].equals(type[1])) {
883 if (!commonType[0].equals(type[0])) {
890 return commonType[0] + "/" + commonType[1];
893 private void copyFromClipboard() {
894 new AsyncTask<Void, Void, List<DocumentInfo>>() {
897 protected List<DocumentInfo> doInBackground(Void... params) {
898 return mClipper.getClippedDocuments();
902 protected void onPostExecute(List<DocumentInfo> docs) {
903 DocumentInfo destination =
904 ((BaseActivity) getActivity()).getCurrentDirectory();
905 copyDocuments(docs, destination);
910 private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
911 checkNotNull(clipData);
912 new AsyncTask<Void, Void, List<DocumentInfo>>() {
915 protected List<DocumentInfo> doInBackground(Void... params) {
916 return mClipper.getDocumentsFromClipData(clipData);
920 protected void onPostExecute(List<DocumentInfo> docs) {
921 copyDocuments(docs, destination);
926 private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
927 if (!canCopy(docs, destination)) {
928 Snackbars.makeSnackbar(
930 R.string.clipboard_files_cannot_paste,
931 Snackbar.LENGTH_SHORT)
936 if (docs.isEmpty()) {
940 final DocumentStack curStack = getDisplayState().stack;
941 DocumentStack tmpStack = new DocumentStack();
942 if (destination != null) {
943 tmpStack.push(destination);
944 tmpStack.addAll(curStack);
949 FileOperations.copy(getActivity(), docs, tmpStack);
952 private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
953 Context context = getActivity();
954 final ContentResolver resolver = context.getContentResolver();
955 ClipData clipData = null;
956 for (DocumentInfo doc : docs) {
957 final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
958 if (clipData == null) {
959 // TODO: figure out what this string should be.
960 // Currently it is not displayed anywhere in the UI, but this might change.
961 final String label = "";
962 clipData = ClipData.newUri(resolver, label, uri);
964 // TODO: update list of mime types in ClipData.
965 clipData.addItem(new ClipData.Item(uri));
971 public void copySelectedToClipboard() {
972 Selection selection = mSelectionManager.getSelection(new Selection());
973 if (!selection.isEmpty()) {
974 copySelectionToClipboard(selection);
975 mSelectionManager.clearSelection();
979 void copySelectionToClipboard(Selection selection) {
980 checkArgument(!selection.isEmpty());
981 new GetDocumentsTask() {
983 void onDocumentsReady(List<DocumentInfo> docs) {
984 mClipper.clipDocuments(docs);
985 Activity activity = getActivity();
986 Snackbars.makeSnackbar(activity,
987 activity.getResources().getQuantityString(
988 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
989 Snackbar.LENGTH_SHORT).show();
991 }.execute(selection);
994 public void pasteFromClipboard() {
996 getActivity().invalidateOptionsMenu();
1000 * Returns true if the list of files can be copied to destination. Note that this
1001 * is a policy check only. Currently the method does not attempt to verify
1002 * available space or any other environmental aspects possibly resulting in
1005 * @return true if the list of files can be copied to destination.
1007 boolean canCopy(List<DocumentInfo> files, DocumentInfo dest) {
1008 BaseActivity activity = (BaseActivity) getActivity();
1010 final RootInfo root = activity.getCurrentRoot();
1012 // Can't copy folders to Downloads.
1013 if (root.isDownloads()) {
1014 for (DocumentInfo docs : files) {
1015 if (docs.isDirectory()) {
1021 return dest != null && dest.isDirectory() && dest.isCreateSupported();
1024 public void selectAllFiles() {
1025 // Only select things currently visible in the adapter.
1026 boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
1028 updateDisplayState();
1032 private void setupDragAndDropOnDirectoryView(View view) {
1033 // Listen for drops on non-directory items and empty space.
1034 view.setOnDragListener(mOnDragListener);
1037 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1038 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1039 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1040 // Make a directory item a drop target. Drop on non-directories and empty space
1041 // is handled at the list/grid view level.
1042 view.setOnDragListener(mOnDragListener);
1045 view.setOnLongClickListener(mLongClickListener);
1048 private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1050 public boolean onDrag(View v, DragEvent event) {
1051 switch (event.getAction()) {
1052 case DragEvent.ACTION_DRAG_STARTED:
1053 // TODO: Check if the event contains droppable data.
1056 // TODO: Highlight potential drop target directory?
1057 // TODO: Expand drop target directory on hover?
1058 case DragEvent.ACTION_DRAG_ENTERED:
1059 case DragEvent.ACTION_DRAG_LOCATION:
1060 case DragEvent.ACTION_DRAG_EXITED:
1061 case DragEvent.ACTION_DRAG_ENDED:
1064 case DragEvent.ACTION_DROP:
1065 String dstId = getModelId(v);
1066 DocumentInfo dstDir = null;
1067 if (dstId != null) {
1068 Cursor dstCursor = mModel.getItem(dstId);
1069 checkNotNull(dstCursor, "Cursor cannot be null.");
1070 dstDir = DocumentInfo.fromDirectoryCursor(dstCursor);
1071 // TODO: Do not drop into the directory where the documents came from.
1073 copyFromClipData(event.getClipData(), dstDir);
1081 * Gets the model ID for a given motion event (using the event position)
1083 private String getModelId(MotionEvent e) {
1084 View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1088 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1089 if (vh instanceof DocumentHolder) {
1090 return ((DocumentHolder) vh).modelId;
1097 * Gets the model ID for a given RecyclerView item.
1098 * @param view A View that is a document item view, or a child of a document item view.
1099 * @return The Model ID for the given document, or null if the given view is not associated with
1100 * a document item view.
1102 private String getModelId(View view) {
1104 if (view.getLayoutParams() instanceof RecyclerView.LayoutParams) {
1105 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1106 if (vh instanceof DocumentHolder) {
1107 return ((DocumentHolder) vh).modelId;
1112 ViewParent parent = view.getParent();
1113 if (parent == null || !(parent instanceof View)) {
1116 view = (View) parent;
1120 private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
1121 String modelId = getModelId(currentItemView);
1122 if (modelId == null) {
1123 return Collections.EMPTY_LIST;
1126 final List<DocumentInfo> selectedDocs =
1127 mModel.getDocuments(mSelectionManager.getSelection());
1128 if (!selectedDocs.isEmpty()) {
1129 if (!isSelected(modelId)) {
1130 // There is a selection that does not include the current item, drag nothing.
1131 return Collections.EMPTY_LIST;
1133 return selectedDocs;
1136 final Cursor cursor = mModel.getItem(modelId);
1137 checkNotNull(cursor, "Cursor cannot be null.");
1138 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
1140 return Lists.newArrayList(doc);
1143 private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
1144 if (docs.size() == 1) {
1145 final DocumentInfo doc = docs.get(0);
1146 return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
1147 doc.mimeType, doc.icon);
1149 return getActivity().getDrawable(R.drawable.ic_doc_generic);
1152 private class DrawableShadowBuilder extends View.DragShadowBuilder {
1154 private final Drawable mShadow;
1156 private final int mShadowDimension;
1158 public DrawableShadowBuilder(Drawable shadow) {
1160 mShadowDimension = getResources().getDimensionPixelSize(
1161 R.dimen.drag_shadow_size);
1162 mShadow.setBounds(0, 0, mShadowDimension, mShadowDimension);
1166 public void onProvideShadowMetrics(
1167 Point shadowSize, Point shadowTouchPoint) {
1168 shadowSize.set(mShadowDimension, mShadowDimension);
1169 shadowTouchPoint.set(mShadowDimension / 2, mShadowDimension / 2);
1173 public void onDrawShadow(Canvas canvas) {
1174 mShadow.draw(canvas);
1179 * Abstract task providing support for loading documents *off*
1180 * the main thread. And if it isn't obvious, creating a list
1181 * of documents (especially large lists) can be pretty expensive.
1183 private abstract class GetDocumentsTask
1184 extends AsyncTask<Selection, Void, List<DocumentInfo>> {
1186 protected final List<DocumentInfo> doInBackground(Selection... selected) {
1187 return mModel.getDocuments(selected[0]);
1191 protected final void onPostExecute(List<DocumentInfo> docs) {
1192 onDocumentsReady(docs);
1195 abstract void onDocumentsReady(List<DocumentInfo> docs);
1199 public boolean isSelected(String modelId) {
1200 return mSelectionManager.getSelection().contains(modelId);
1203 private class ItemEventListener implements DocumentHolder.EventListener {
1205 public boolean onActivate(DocumentHolder doc) {
1206 // Toggle selection if we're in selection mode, othewise, view item.
1207 if (mSelectionManager.hasSelection()) {
1208 mSelectionManager.toggleSelection(doc.modelId);
1210 handleViewItem(doc.modelId);
1216 public boolean onSelect(DocumentHolder doc) {
1217 mSelectionManager.toggleSelection(doc.modelId);
1218 mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1223 public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1224 // Only handle key-down events. This is simpler, consistent with most other UIs, and
1225 // enables the handling of repeated key events from holding down a key.
1226 if (event.getAction() != KeyEvent.ACTION_DOWN) {
1230 boolean handled = false;
1231 if (Events.isNavigationKeyCode(keyCode)) {
1232 // Find the target item and focus it.
1233 int endPos = findTargetPosition(doc.itemView, keyCode);
1235 if (endPos != RecyclerView.NO_POSITION) {
1238 // Handle any necessary adjustments to selection.
1239 boolean extendSelection = event.isShiftPressed();
1240 if (extendSelection) {
1241 int startPos = doc.getAdapterPosition();
1242 mSelectionManager.selectRange(startPos, endPos);
1247 // Handle enter key events
1248 if (keyCode == KeyEvent.KEYCODE_ENTER) {
1249 handled = onActivate(doc);
1257 * Finds the destination position where the focus should land for a given navigation event.
1259 * @param view The view that received the event.
1260 * @param keyCode The key code for the event.
1261 * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
1263 private int findTargetPosition(View view, int keyCode) {
1265 case KeyEvent.KEYCODE_MOVE_HOME:
1267 case KeyEvent.KEYCODE_MOVE_END:
1268 return mAdapter.getItemCount() - 1;
1269 case KeyEvent.KEYCODE_PAGE_UP:
1270 case KeyEvent.KEYCODE_PAGE_DOWN:
1271 return findTargetPositionByPage(view, keyCode);
1274 // Find a navigation target based on the arrow key that the user pressed.
1277 case KeyEvent.KEYCODE_DPAD_UP:
1278 searchDir = View.FOCUS_UP;
1280 case KeyEvent.KEYCODE_DPAD_DOWN:
1281 searchDir = View.FOCUS_DOWN;
1283 case KeyEvent.KEYCODE_DPAD_LEFT:
1284 searchDir = View.FOCUS_LEFT;
1286 case KeyEvent.KEYCODE_DPAD_RIGHT:
1287 searchDir = View.FOCUS_RIGHT;
1291 if (searchDir != -1) {
1292 View targetView = view.focusSearch(searchDir);
1293 // TargetView can be null, for example, if the user pressed <down> at the bottom
1295 if (targetView != null) {
1296 // Ignore navigation targets that aren't items in the RecyclerView.
1297 if (targetView.getParent() == mRecView) {
1298 return mRecView.getChildAdapterPosition(targetView);
1303 return RecyclerView.NO_POSITION;
1307 * Given a PgUp/PgDn event and the current view, find the position of the target view.
1309 * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
1310 * the top- or bottom-most visible item.
1311 * <li>The position of an item that is one page's worth of items up (or down) if the current
1312 * item is the top- or bottom-most visible item.
1313 * <li>The first (or last) item, if paging up (or down) would go past those limits.
1314 * @param view The view that received the key event.
1315 * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
1316 * @return The adapter position of the target item.
1318 private int findTargetPositionByPage(View view, int keyCode) {
1319 int first = mLayout.findFirstVisibleItemPosition();
1320 int last = mLayout.findLastVisibleItemPosition();
1321 int current = mRecView.getChildAdapterPosition(view);
1322 int pageSize = last - first + 1;
1324 if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
1325 if (current > first) {
1326 // If the current item isn't the first item, target the first item.
1329 // If the current item is the first item, target the item one page up.
1330 int target = current - pageSize;
1331 return target < 0 ? 0 : target;
1335 if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
1336 if (current < last) {
1337 // If the current item isn't the last item, target the last item.
1340 // If the current item is the last item, target the item one page down.
1341 int target = current + pageSize;
1342 int max = mAdapter.getItemCount() - 1;
1343 return target < max ? target : max;
1347 throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
1351 * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
1356 public void focusItem(final int pos) {
1357 // If the item is already in view, focus it; otherwise, scroll to it and focus it.
1358 RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos);
1360 vh.itemView.requestFocus();
1362 mRecView.smoothScrollToPosition(pos);
1363 // Set a one-time listener to request focus when the scroll has completed.
1364 mRecView.addOnScrollListener(
1365 new RecyclerView.OnScrollListener() {
1367 public void onScrollStateChanged (RecyclerView view, int newState) {
1368 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
1369 // When scrolling stops, find the item and focus it.
1370 RecyclerView.ViewHolder vh =
1371 view.findViewHolderForAdapterPosition(pos);
1373 vh.itemView.requestFocus();
1375 // This might happen in weird corner cases, e.g. if the user is
1376 // scrolling while a delete operation is in progress. In that
1377 // case, just don't attempt to focus the missing item.
1379 TAG, "Unable to focus position " + pos + " after a scroll");
1381 view.removeOnScrollListener(this);
1391 private final class ModelUpdateListener implements Model.UpdateListener {
1393 public void onModelUpdate(Model model) {
1394 if (model.info != null || model.error != null) {
1395 mMessageBar.setInfo(model.info);
1396 mMessageBar.setError(model.error);
1400 mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1402 if (model.isEmpty()) {
1403 if (getDisplayState().currentSearch != null) {
1404 showNoResults(getDisplayState().stack.root);
1406 showEmptyDirectory();
1410 mAdapter.notifyDataSetChanged();
1415 public void onModelUpdateFailed(Exception e) {
1420 private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
1422 public boolean onLongClick(View v) {
1423 if (mGestureDetector.mouseSpawnedLastEvent()) {
1424 List<DocumentInfo> docs = getDraggableDocuments(v);
1425 if (docs.isEmpty()) {
1429 getClipDataFromDocuments(docs),
1430 new DrawableShadowBuilder(getDragShadowIcon(docs)),
1432 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1433 View.DRAG_FLAG_GLOBAL_URI_WRITE
1442 // Previously we listened to events with one class, only to bounce them forward
1443 // to GestureDetector. We're still doing that here, but with a single class
1444 // that reduces overall complexity in our glue code.
1445 private static final class ListeningGestureDetector extends GestureDetector
1446 implements OnItemTouchListener {
1448 private int mLastTool = -1;
1450 public ListeningGestureDetector(Context context, GestureListener listener) {
1451 super(context, listener);
1452 setOnDoubleTapListener(listener);
1455 boolean mouseSpawnedLastEvent() {
1456 return Events.isMouseType(mLastTool);
1459 boolean touchSpawnedLastEvent() {
1460 return Events.isTouchType(mLastTool);
1464 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1465 mLastTool = e.getToolType(0);
1466 onTouchEvent(e); // bounce this forward to our detecty heart
1471 public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
1474 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
1478 * The gesture listener for items in the list/grid view. Interprets gestures and sends the
1479 * events to the target DocumentHolder, whence they are routed to the appropriate listener.
1481 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
1483 public boolean onSingleTapUp(MotionEvent e) {
1484 // Single tap logic:
1485 // If the selection manager is active, it gets first whack at handling tap
1486 // events. Otherwise, tap events are routed to the target DocumentHolder.
1487 boolean handled = mSelectionManager.onSingleTapUp(
1488 new MotionInputEvent(e, mRecView));
1494 // Give the DocumentHolder a crack at the event.
1495 DocumentHolder holder = getTarget(e);
1496 if (holder != null) {
1497 handled = holder.onSingleTapUp(e);
1504 public void onLongPress(MotionEvent e) {
1505 // Long-press events get routed directly to the selection manager. They can be
1506 // changed to route through the DocumentHolder if necessary.
1507 mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
1511 public boolean onDoubleTap(MotionEvent e) {
1512 // Double-tap events are handled directly by the DirectoryFragment. They can be changed
1513 // to route through the DocumentHolder if necessary.
1514 return DirectoryFragment.this.onDoubleTap(e);
1517 private @Nullable DocumentHolder getTarget(MotionEvent e) {
1518 View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
1519 if (childView != null) {
1520 return (DocumentHolder) mRecView.getChildViewHolder(childView);
1527 public static void showDirectory(
1528 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1529 show(fm, TYPE_NORMAL, root, doc, null, anim);
1532 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
1533 show(fm, TYPE_SEARCH, root, null, query, anim);
1536 public static void showRecentsOpen(FragmentManager fm, int anim) {
1537 show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
1540 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1541 String query, int anim) {
1542 final Bundle args = new Bundle();
1543 args.putInt(EXTRA_TYPE, type);
1544 args.putParcelable(EXTRA_ROOT, root);
1545 args.putParcelable(EXTRA_DOC, doc);
1546 args.putString(EXTRA_QUERY, query);
1548 final FragmentTransaction ft = fm.beginTransaction();
1551 args.putBoolean(EXTRA_IGNORE_STATE, true);
1554 args.putBoolean(EXTRA_IGNORE_STATE, true);
1555 ft.setCustomAnimations(R.animator.dir_enter, R.animator.dir_frozen);
1558 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_leave);
1562 final DirectoryFragment fragment = new DirectoryFragment();
1563 fragment.setArguments(args);
1565 ft.replace(R.id.container_directory, fragment);
1566 ft.commitAllowingStateLoss();
1569 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1570 final StringBuilder builder = new StringBuilder();
1571 builder.append(root != null ? root.authority : "null").append(';');
1572 builder.append(root != null ? root.rootId : "null").append(';');
1573 builder.append(doc != null ? doc.documentId : "null");
1574 return builder.toString();
1577 public static @Nullable DirectoryFragment get(FragmentManager fm) {
1578 // TODO: deal with multiple directories shown at once
1579 Fragment fragment = fm.findFragmentById(R.id.container_directory);
1580 return fragment instanceof DirectoryFragment
1581 ? (DirectoryFragment) fragment