1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.dirlist;
18 
19 import static com.android.documentsui.base.DocumentInfo.getCursorString;
20 import static com.android.documentsui.base.SharedMinimal.DEBUG;
21 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
22 import static com.android.documentsui.base.State.MODE_GRID;
23 import static com.android.documentsui.base.State.MODE_LIST;
24 
25 import android.app.ActivityManager;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentProviderClient;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.UserProperties;
32 import android.database.Cursor;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Looper;
37 import android.os.Parcelable;
38 import android.os.UserHandle;
39 import android.os.UserManager;
40 import android.provider.DocumentsContract;
41 import android.provider.DocumentsContract.Document;
42 import android.util.Log;
43 import android.util.SparseArray;
44 import android.view.ContextMenu;
45 import android.view.LayoutInflater;
46 import android.view.MenuInflater;
47 import android.view.MenuItem;
48 import android.view.MotionEvent;
49 import android.view.View;
50 import android.view.ViewGroup;
51 import android.view.ViewTreeObserver;
52 import android.widget.ImageView;
53 
54 import androidx.annotation.DimenRes;
55 import androidx.annotation.FractionRes;
56 import androidx.annotation.IntDef;
57 import androidx.annotation.Nullable;
58 import androidx.fragment.app.Fragment;
59 import androidx.fragment.app.FragmentActivity;
60 import androidx.fragment.app.FragmentManager;
61 import androidx.fragment.app.FragmentTransaction;
62 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
63 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
64 import androidx.recyclerview.selection.MutableSelection;
65 import androidx.recyclerview.selection.Selection;
66 import androidx.recyclerview.selection.SelectionTracker;
67 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
68 import androidx.recyclerview.selection.StorageStrategy;
69 import androidx.recyclerview.widget.GridLayoutManager;
70 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
71 import androidx.recyclerview.widget.RecyclerView;
72 import androidx.recyclerview.widget.RecyclerView.RecyclerListener;
73 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
74 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
75 
76 import com.android.documentsui.ActionHandler;
77 import com.android.documentsui.ActionModeController;
78 import com.android.documentsui.BaseActivity;
79 import com.android.documentsui.ContentLock;
80 import com.android.documentsui.DocsSelectionHelper.DocDetailsLookup;
81 import com.android.documentsui.DocumentsApplication;
82 import com.android.documentsui.DragHoverListener;
83 import com.android.documentsui.FocusManager;
84 import com.android.documentsui.Injector;
85 import com.android.documentsui.Injector.ContentScoped;
86 import com.android.documentsui.Injector.Injected;
87 import com.android.documentsui.MetricConsts;
88 import com.android.documentsui.Metrics;
89 import com.android.documentsui.Model;
90 import com.android.documentsui.ProfileTabsController;
91 import com.android.documentsui.R;
92 import com.android.documentsui.ThumbnailCache;
93 import com.android.documentsui.TimeoutTask;
94 import com.android.documentsui.base.DocumentFilters;
95 import com.android.documentsui.base.DocumentInfo;
96 import com.android.documentsui.base.DocumentStack;
97 import com.android.documentsui.base.EventListener;
98 import com.android.documentsui.base.Features;
99 import com.android.documentsui.base.RootInfo;
100 import com.android.documentsui.base.Shared;
101 import com.android.documentsui.base.State;
102 import com.android.documentsui.base.State.ViewMode;
103 import com.android.documentsui.base.UserId;
104 import com.android.documentsui.clipping.ClipStore;
105 import com.android.documentsui.clipping.DocumentClipper;
106 import com.android.documentsui.clipping.UrisSupplier;
107 import com.android.documentsui.dirlist.AnimationView.AnimationType;
108 import com.android.documentsui.picker.PickActivity;
109 import com.android.documentsui.services.FileOperation;
110 import com.android.documentsui.services.FileOperationService;
111 import com.android.documentsui.services.FileOperationService.OpType;
112 import com.android.documentsui.services.FileOperations;
113 import com.android.documentsui.sorting.SortDimension;
114 import com.android.documentsui.sorting.SortModel;
115 import com.android.documentsui.util.VersionUtils;
116 import com.android.modules.utils.build.SdkLevel;
117 
118 import com.google.common.base.Objects;
119 
120 import java.io.IOException;
121 import java.lang.annotation.Retention;
122 import java.lang.annotation.RetentionPolicy;
123 import java.util.Iterator;
124 import java.util.List;
125 
126 /**
127  * Display the documents inside a single directory.
128  */
129 public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
130 
131     static final int TYPE_NORMAL = 1;
132     static final int TYPE_RECENT_OPEN = 2;
133 
134     @IntDef(flag = true, value = {
135             REQUEST_COPY_DESTINATION
136     })
137     @Retention(RetentionPolicy.SOURCE)
138     public @interface RequestCode {
139     }
140 
141     public static final int REQUEST_COPY_DESTINATION = 1;
142 
143     static final String TAG = "DirectoryFragment";
144 
145     private static final int CACHE_EVICT_LIMIT = 100;
146     private static final int REFRESH_SPINNER_TIMEOUT = 500;
147     private static final int PROVIDER_MAX_RETRIES = 10;
148     private static final long PROVIDER_TEST_DELAY = 4000;
149     private static final String ACTION_MEDIA_REMOVED = "android.intent.action.MEDIA_REMOVED";
150     private static final String ACTION_MEDIA_MOUNTED = "android.intent.action.MEDIA_MOUNTED";
151     private static final String ACTION_MEDIA_EJECT = "android.intent.action.MEDIA_EJECT";
152 
153     private BaseActivity mActivity;
154 
155     private State mState;
156     private Model mModel;
157     private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
158     private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment();
159 
160     @Injected
161     @ContentScoped
162     private Injector<?> mInjector;
163 
164     @Injected
165     @ContentScoped
166     private SelectionTracker<String> mSelectionMgr;
167 
168     @Injected
169     @ContentScoped
170     private FocusManager mFocusManager;
171 
172     @Injected
173     @ContentScoped
174     private ActionHandler mActions;
175 
176     @Injected
177     @ContentScoped
178     private ActionModeController mActionModeController;
179 
180     @Injected
181     @ContentScoped
182     private ProfileTabsController mProfileTabsController;
183 
184     private DocDetailsLookup mDetailsLookup;
185     private SelectionMetadata mSelectionMetadata;
186     private KeyInputHandler mKeyListener;
187     private @Nullable DragHoverListener mDragHoverListener;
188     private View mRootView;
189     private IconHelper mIconHelper;
190     private SwipeRefreshLayout mRefreshLayout;
191     private RecyclerView mRecView;
192     private DocumentsAdapter mAdapter;
193     private DocumentClipper mClipper;
194     private GridLayoutManager mLayout;
195     private int mColumnCount = 1;  // This will get updated when layout changes.
196     private int mColumnUnit = 1;
197 
198     private float mLiveScale = 1.0f;
199     private @ViewMode int mMode;
200     private int mAppBarHeight;
201     private int mSaveLayoutHeight;
202 
203     private View mProgressBar;
204 
205     private DirectoryState mLocalState;
206 
207     private Handler mHandler;
208     private Runnable mProviderTestRunnable;
209 
210     // Note, we use !null to indicate that selection was restored (from rotation).
211     // So don't fiddle with this field unless you've got the bigger picture in mind.
212     private @Nullable Bundle mRestoredState;
213 
214     // Blocks loading/reloading of content while user is actively making selection.
215     private ContentLock mContentLock = new ContentLock();
216 
217     private SortModel.UpdateListener mSortListener = (model, updateType) -> {
218         // Only when sort order has changed do we need to trigger another loading.
219         if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
220             mActions.loadDocumentsForCurrentStack();
221         }
222     };
223 
224     private final Runnable mOnDisplayStateChanged = this::onDisplayStateChanged;
225 
226     private final ViewTreeObserver.OnPreDrawListener mToolbarPreDrawListener = () -> {
227         final boolean appBarHeightChanged = mAppBarHeight != getAppBarLayoutHeight();
228         if (appBarHeightChanged || mSaveLayoutHeight != getSaveLayoutHeight()) {
229             updateLayout(mState.derivedMode);
230 
231             if (appBarHeightChanged) {
232                 scrollToTop();
233             }
234             return false;
235         }
236         return true;
237     };
238 
239     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
240         @Override
241         public void onReceive(Context context, Intent intent) {
242             final String action = intent.getAction();
243             if (SdkLevel.isAtLeastV()
244                     && mState.configStore.isPrivateSpaceInDocsUIEnabled()) {
245                 profileStatusReceiverPostV(intent, action);
246             } else {
247                 profileStatusReceiverPreV(intent, action);
248             }
249         }
250 
251         private void profileStatusReceiverPostV(Intent intent, String action) {
252             if (!SdkLevel.isAtLeastV()) return;
253             if (!isProfileStatusAction(action)) return;
254             UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
255             UserId userId = UserId.of(userHandle);
256             UserManager userManager = mActivity.getSystemService(UserManager.class);
257             if (userManager == null) {
258                 Log.e(TAG, "cannot obtain user manager");
259                 return;
260             }
261             UserProperties userProperties = userManager.getUserProperties(userHandle);
262             if (userProperties.getShowInQuietMode()
263                     == UserProperties.SHOW_IN_QUIET_MODE_PAUSED) {
264                 if (Objects.equal(mActivity.getSelectedUser(), userId)) {
265                     // We only need to refresh the layout when the selected user is equal to
266                     // the received profile user.
267                     onPausedProfileStatusChange(action, userId);
268                 }
269                 return;
270             }
271             if (userProperties.getShowInQuietMode()
272                     == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
273                 onHiddenProfileStatusChange(action, userId);
274             }
275         }
276 
277         private void profileStatusReceiverPreV(Intent intent, String action) {
278             if (!isManagedProfileAction(action)) return;
279             UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
280             UserId userId = UserId.of(userHandle);
281             if (Objects.equal(mActivity.getSelectedUser(), userId)) {
282                 // We only need to refresh the layout when the selected user is equal to the
283                 // received profile user.
284                 onPausedProfileStatusChange(action, userId);
285             }
286         }
287     };
288 
onPausedProfileStatusChange(String action, UserId userId)289     private void onPausedProfileStatusChange(String action, UserId userId) {
290         if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)
291                 || (SdkLevel.isAtLeastV() && Intent.ACTION_PROFILE_UNAVAILABLE.equals(action))) {
292             // If the managed/paused profile is turned off, we need to refresh the directory
293             // to update the UI to show an appropriate error message.
294             if (mProviderTestRunnable != null) {
295                 mHandler.removeCallbacks(mProviderTestRunnable);
296                 mProviderTestRunnable = null;
297             }
298             onRefresh();
299             return;
300         }
301 
302         // When the managed/paused profile becomes available, the provider may not be available
303         // immediately, we need to check if it is ready before we reload the content.
304         if (Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)
305                 || (SdkLevel.isAtLeastV() && Intent.ACTION_PROFILE_AVAILABLE.equals(action))) {
306             checkUriAndScheduleCheckIfNeeded(userId);
307         }
308     }
309 
onHiddenProfileStatusChange(String action, UserId userId)310     private void onHiddenProfileStatusChange(String action, UserId userId) {
311         mActivity.updateRecentsSetting();
312         if (Intent.ACTION_PROFILE_UNAVAILABLE.equals(action)) {
313             if (mProviderTestRunnable != null) {
314                 mHandler.removeCallbacks(mProviderTestRunnable);
315                 mProviderTestRunnable = null;
316             }
317             if (!mActivity.isSearchExpanded()) {
318                 if (mActivity.getLastSelectedUser() != null
319                         && mActivity.getLastSelectedUser().equals(userId)) {
320                     mState.stack.reset(mActivity.getInitialStack());
321                 }
322                 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
323             }
324         } else {
325             checkUriAndScheduleCheckIfNeeded(userId);
326         }
327     }
328 
329     private final BroadcastReceiver mSdCardBroadcastReceiver = new BroadcastReceiver() {
330         @Override
331         public void onReceive(Context context, Intent intent) {
332             onRefresh();
333         }
334     };
335 
getSdCardStateChangeFilter()336     private IntentFilter getSdCardStateChangeFilter() {
337         IntentFilter sdCardStateChangeFilter = new IntentFilter();
338         sdCardStateChangeFilter.addAction(ACTION_MEDIA_REMOVED);
339         sdCardStateChangeFilter.addAction(ACTION_MEDIA_MOUNTED);
340         sdCardStateChangeFilter.addAction(ACTION_MEDIA_EJECT);
341         sdCardStateChangeFilter.addDataScheme("file");
342         return sdCardStateChangeFilter;
343     }
344 
checkUriAndScheduleCheckIfNeeded(UserId userId)345     private void checkUriAndScheduleCheckIfNeeded(UserId userId) {
346         RootInfo currentRoot = mActivity.getCurrentRoot();
347         DocumentInfo currentDoc = mActivity.getDisplayState().stack.peek();
348         Uri uri = getCurrentUri(currentRoot, currentDoc);
349         if (isProviderAvailable(uri, userId) || mActivity.isInRecents()) {
350             if (mProviderTestRunnable != null) {
351                 mHandler.removeCallbacks(mProviderTestRunnable);
352                 mProviderTestRunnable = null;
353             }
354             mHandler.post(this::onRefresh);
355         } else {
356             checkUriWithDelay(/* numOfRetries= */1, uri, userId);
357         }
358     }
359 
checkUriWithDelay(int numOfRetries, Uri uri, UserId userId)360     private void checkUriWithDelay(int numOfRetries, Uri uri, UserId userId) {
361         mProviderTestRunnable = () -> {
362             RootInfo currentRoot = mActivity.getCurrentRoot();
363             DocumentInfo currentDoc = mActivity.getDisplayState().stack.peek();
364             if (mActivity.getSelectedUser().equals(userId)
365                     && uri.equals(getCurrentUri(currentRoot, currentDoc))) {
366                 if (isProviderAvailable(uri, userId)
367                         || userId.isQuietModeEnabled(mActivity)
368                         || numOfRetries >= PROVIDER_MAX_RETRIES) {
369                     // We stop the recursive check when
370                     // 1. the provider is available
371                     // 2. the profile is in quiet mode, i.e. provider will not be available
372                     // 3. after maximum retries
373                     onRefresh();
374                     mProviderTestRunnable = null;
375                 } else {
376                     Log.d(TAG, "Provider is not available. Retry after " + PROVIDER_TEST_DELAY);
377                     checkUriWithDelay(numOfRetries + 1, uri, userId);
378                 }
379             }
380         };
381         mHandler.postDelayed(mProviderTestRunnable, PROVIDER_TEST_DELAY);
382     }
383 
getCurrentUri(RootInfo root, @Nullable DocumentInfo doc)384     private Uri getCurrentUri(RootInfo root, @Nullable DocumentInfo doc) {
385         String authority = doc == null ? root.authority : doc.authority;
386         String documentId = doc == null ? root.documentId : doc.documentId;
387         return DocumentsContract.buildDocumentUri(authority, documentId);
388     }
389 
isProviderAvailable(Uri uri, UserId userId)390     private boolean isProviderAvailable(Uri uri, UserId userId) {
391         try (ContentProviderClient userClient =
392                      DocumentsApplication.acquireUnstableProviderOrThrow(
393                              userId.getContentResolver(mActivity), uri.getAuthority())) {
394             Cursor testCursor = userClient.query(uri, /* projection= */ null,
395                     /* queryArgs= */null, /* cancellationSignal= */ null);
396             if (testCursor != null) {
397                 return true;
398             }
399         } catch (Exception e) {
400             // Provider is not available. Ignore.
401         }
402         return false;
403     }
404 
isProfileStatusAction(String action)405     private boolean isProfileStatusAction(String action) {
406         if (!SdkLevel.isAtLeastV()) return isManagedProfileAction(action);
407         return Intent.ACTION_PROFILE_AVAILABLE.equals(action)
408                 || Intent.ACTION_PROFILE_UNAVAILABLE.equals(action)
409                 || Intent.ACTION_PROFILE_ADDED.equals(action)
410                 || Intent.ACTION_PROFILE_REMOVED.equals(action);
411     }
412 
isManagedProfileAction(String action)413     private static boolean isManagedProfileAction(String action) {
414         return Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)
415                 || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action);
416     }
417 
418     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)419     public View onCreateView(
420             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
421 
422         mHandler = new Handler(Looper.getMainLooper());
423         mActivity = (BaseActivity) getActivity();
424         mRootView = inflater.inflate(R.layout.fragment_directory, container, false);
425 
426         mProgressBar = mRootView.findViewById(R.id.progressbar);
427         assert mProgressBar != null;
428 
429         mRecView = (RecyclerView) mRootView.findViewById(R.id.dir_list);
430         mRecView.setRecyclerListener(
431                 new RecyclerListener() {
432                     @Override
433                     public void onViewRecycled(ViewHolder holder) {
434                         cancelThumbnailTask(holder.itemView);
435                     }
436                 });
437 
438         mRefreshLayout = (SwipeRefreshLayout) mRootView.findViewById(R.id.refresh_layout);
439         mRefreshLayout.setOnRefreshListener(this);
440         mRecView.setItemAnimator(new DirectoryItemAnimator());
441 
442         mInjector = mActivity.getInjector();
443         // Initially, this selection tracker (delegator) uses a stub implementation, so it must be
444         // updated (reset) when necessary things are ready.
445         mSelectionMgr = mInjector.selectionMgr;
446         mModel = mInjector.getModel();
447         mModel.reset();
448 
449         mInjector.actions.registerDisplayStateChangedListener(mOnDisplayStateChanged);
450 
451         mClipper = DocumentsApplication.getDocumentClipper(getContext());
452         if (mInjector.config.dragAndDropEnabled()) {
453             DirectoryDragListener listener = new DirectoryDragListener(
454                     new DragHost<>(
455                             mActivity,
456                             DocumentsApplication.getDragAndDropManager(mActivity),
457                             mSelectionMgr,
458                             mInjector.actions,
459                             mActivity.getDisplayState(),
460                             mInjector.dialogs,
461                             (View v) -> {
462                                 return getModelId(v) != null;
463                             },
464                             this::getDocumentHolder,
465                             this::getDestination
466                     ));
467             mDragHoverListener = DragHoverListener.create(listener, mRecView);
468         }
469         // Make the recycler and the empty views responsive to drop events when allowed.
470         mRecView.setOnDragListener(mDragHoverListener);
471 
472         setPreDrawListenerEnabled(true);
473 
474         return mRootView;
475     }
476 
477     @Override
onDestroyView()478     public void onDestroyView() {
479         mInjector.actions.unregisterDisplayStateChangedListener(mOnDisplayStateChanged);
480         if (mState.supportsCrossProfile()) {
481             LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mReceiver);
482             if (mProviderTestRunnable != null) {
483                 mHandler.removeCallbacks(mProviderTestRunnable);
484             }
485         }
486         getContext().unregisterReceiver(mSdCardBroadcastReceiver);
487 
488         // Cancel any outstanding thumbnail requests
489         final int count = mRecView.getChildCount();
490         for (int i = 0; i < count; i++) {
491             final View view = mRecView.getChildAt(i);
492             cancelThumbnailTask(view);
493         }
494 
495         mModel.removeUpdateListener(mModelUpdateListener);
496         mModel.removeUpdateListener(mAdapter.getModelUpdateListener());
497         setPreDrawListenerEnabled(false);
498 
499         super.onDestroyView();
500     }
501 
502     @Override
onActivityCreated(Bundle savedInstanceState)503     public void onActivityCreated(Bundle savedInstanceState) {
504         super.onActivityCreated(savedInstanceState);
505 
506         mState = mActivity.getDisplayState();
507 
508         // Read arguments when object created for the first time.
509         // Restore state if fragment recreated.
510         Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
511         mRestoredState = args;
512 
513         mLocalState = new DirectoryState();
514         mLocalState.restore(args);
515         if (mLocalState.mSelectionId == null) {
516             mLocalState.mSelectionId = Integer.toHexString(System.identityHashCode(mRecView));
517         }
518 
519         mIconHelper = new IconHelper(mActivity, MODE_GRID, mState.supportsCrossProfile(),
520                 mState.configStore);
521 
522         mAdapter = getModelBackedDocumentsAdapter();
523 
524         mRecView.setAdapter(mAdapter);
525 
526         mLayout = new GridLayoutManager(getContext(), mColumnCount) {
527             @Override
528             public void onLayoutCompleted(RecyclerView.State state) {
529                 super.onLayoutCompleted(state);
530                 mFocusManager.onLayoutCompleted();
531             }
532         };
533 
534         SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
535         if (lookup != null) {
536             mLayout.setSpanSizeLookup(lookup);
537         }
538         mRecView.setLayoutManager(mLayout);
539 
540         mModel.addUpdateListener(mAdapter.getModelUpdateListener());
541         mModel.addUpdateListener(mModelUpdateListener);
542 
543         SelectionPredicate<String> selectionPredicate =
544                 new DocsSelectionPredicate(mInjector.config, mState, mModel, mRecView);
545 
546         mFocusManager = mInjector.getFocusManager(mRecView, mModel);
547         mActions = mInjector.getActionHandler(mContentLock);
548 
549         mRecView.setAccessibilityDelegateCompat(
550                 new AccessibilityEventRouter(mRecView,
551                         (View child) -> onAccessibilityClick(child),
552                         (View child) -> onAccessibilityLongClick(child)));
553         mSelectionMetadata = new SelectionMetadata(mModel::getItem);
554         mDetailsLookup = new DocsItemDetailsLookup(mRecView);
555 
556         DragStartListener dragStartListener = mInjector.config.dragAndDropEnabled()
557                 ? DragStartListener.create(
558                 mIconHelper,
559                 mModel,
560                 mSelectionMgr,
561                 mSelectionMetadata,
562                 mState,
563                 this::getModelId,
564                 mRecView::findChildViewUnder,
565                 DocumentsApplication.getDragAndDropManager(mActivity))
566                 : DragStartListener.STUB;
567 
568         {
569             // Limiting the scope of the localTracker so nobody uses it.
570             // This block initializes/updates the global SelectionTracker held in mSelectionMgr.
571             SelectionTracker<String> localTracker = new SelectionTracker.Builder<>(
572                     mLocalState.mSelectionId,
573                     mRecView,
574                     new DocsStableIdProvider(mAdapter),
575                     mDetailsLookup,
576                     StorageStrategy.createStringStorage())
577                     .withBandOverlay(R.drawable.band_select_overlay)
578                     .withFocusDelegate(mFocusManager)
579                     .withOnDragInitiatedListener(dragStartListener::onDragEvent)
580                     .withOnContextClickListener(this::onContextMenuClick)
581                     .withOnItemActivatedListener(this::onItemActivated)
582                     .withOperationMonitor(mContentLock.getMonitor())
583                     .withSelectionPredicate(selectionPredicate)
584                     .withGestureTooltypes(MotionEvent.TOOL_TYPE_FINGER,
585                             MotionEvent.TOOL_TYPE_STYLUS)
586                     .build();
587             mInjector.updateSharedSelectionTracker(localTracker);
588         }
589 
590         mSelectionMgr.addObserver(mSelectionMetadata);
591 
592         // Construction of the input handlers is non trivial, so to keep logic clear,
593         // and code flexible, and DirectoryFragment small, the construction has been
594         // moved off into a separate class.
595         InputHandlers handlers = new InputHandlers(
596                 mActions,
597                 mSelectionMgr,
598                 selectionPredicate,
599                 mFocusManager,
600                 mRecView);
601 
602         // This little guy gets added to each Holder, so that we can be notified of key events
603         // on RecyclerView items.
604         mKeyListener = handlers.createKeyHandler();
605 
606         if (DEBUG) {
607             new ScaleHelper(this.getContext(), mInjector.features, this::scaleLayout)
608                     .attach(mRecView);
609         }
610 
611         new RefreshHelper(mRefreshLayout::setEnabled)
612                 .attach(mRecView);
613 
614         mActionModeController = mInjector.getActionModeController(
615                 mSelectionMetadata,
616                 this::handleMenuItemClick);
617 
618         mSelectionMgr.addObserver(mActionModeController);
619 
620         mProfileTabsController = mInjector.profileTabsController;
621         mSelectionMgr.addObserver(mProfileTabsController);
622 
623         final ActivityManager am = (ActivityManager) mActivity.getSystemService(
624                 Context.ACTIVITY_SERVICE);
625         boolean svelte = am.isLowRamDevice() && (mState.stack.isRecents());
626         mIconHelper.setThumbnailsEnabled(!svelte);
627 
628         // If mDocument is null, we sort it by last modified by default because it's in Recents.
629         final boolean prefersLastModified =
630                 (mLocalState.mDocument == null)
631                         || mLocalState.mDocument.prefersSortByLastModified();
632         // Call this before adding the listener to avoid restarting the loader one more time
633         mState.sortModel.setDefaultDimension(
634                 prefersLastModified
635                         ? SortModel.SORT_DIMENSION_ID_DATE
636                         : SortModel.SORT_DIMENSION_ID_TITLE);
637 
638         // Kick off loader at least once
639         mActions.loadDocumentsForCurrentStack();
640 
641         if (mState.supportsCrossProfile()) {
642             final IntentFilter filter = new IntentFilter();
643             filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
644             filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
645             if (SdkLevel.isAtLeastV()) {
646                 filter.addAction(Intent.ACTION_PROFILE_AVAILABLE);
647                 filter.addAction(Intent.ACTION_PROFILE_UNAVAILABLE);
648                 filter.addAction(Intent.ACTION_PROFILE_ADDED);
649                 filter.addAction(Intent.ACTION_PROFILE_REMOVED);
650             }
651             // DocumentsApplication will resend the broadcast locally after roots are updated.
652             // Register to a local broadcast manager to avoid this fragment from updating before
653             // roots are updated.
654             LocalBroadcastManager.getInstance(mActivity).registerReceiver(mReceiver, filter);
655         }
656         getContext().registerReceiver(mSdCardBroadcastReceiver, getSdCardStateChangeFilter());
657     }
658 
getModelBackedDocumentsAdapter()659     private DocumentsAdapter getModelBackedDocumentsAdapter() {
660         if (SdkLevel.isAtLeastS() && mState.configStore.isPrivateSpaceInDocsUIEnabled()) {
661             return new DirectoryAddonsAdapter(
662                     mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper,
663                     mInjector.fileTypeLookup, mState.configStore),
664                     UserId.CURRENT_USER,
665                     mActivity.getSelectedUser(),
666                     DocumentsApplication.getUserManagerState(getContext()).getUserIdToLabelMap(),
667                     getContext().getSystemService(UserManager.class), mState.configStore);
668         }
669         return new DirectoryAddonsAdapter(
670                         mAdapterEnv,
671                         new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper,
672                                 mInjector.fileTypeLookup, mState.configStore),
673                         mState.configStore);
674     }
675 
676     @Override
onStart()677     public void onStart() {
678         super.onStart();
679 
680         // Add listener to update contents on sort model change
681         mState.sortModel.addListener(mSortListener);
682         // After SD card is formatted, we go out of the view and come back. Similarly when users
683         // go out of the app to delete some files, we want to refresh the directory.
684         onRefresh();
685     }
686 
687     @Override
onStop()688     public void onStop() {
689         super.onStop();
690 
691         mState.sortModel.removeListener(mSortListener);
692 
693         // Remember last scroll location
694         final SparseArray<Parcelable> container = new SparseArray<>();
695         getView().saveHierarchyState(container);
696         mState.dirConfigs.put(mLocalState.getConfigKey(), container);
697     }
698 
699     @Override
onSaveInstanceState(Bundle outState)700     public void onSaveInstanceState(Bundle outState) {
701         super.onSaveInstanceState(outState);
702 
703         mLocalState.save(outState);
704         mSelectionMgr.onSaveInstanceState(outState);
705     }
706 
707     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)708     public void onCreateContextMenu(ContextMenu menu,
709             View v,
710             ContextMenu.ContextMenuInfo menuInfo) {
711         super.onCreateContextMenu(menu, v, menuInfo);
712         final MenuInflater inflater = getActivity().getMenuInflater();
713 
714         final String modelId = getModelId(v);
715         if (modelId == null) {
716             // TODO: inject DirectoryDetails into MenuManager constructor
717             // Since both classes are supplied by Activity and created
718             // at the same time.
719             mInjector.menuManager.inflateContextMenuForContainer(
720                     menu, inflater, mSelectionMetadata);
721         } else {
722             mInjector.menuManager.inflateContextMenuForDocs(
723                     menu, inflater, mSelectionMetadata);
724         }
725     }
726 
727     @Override
onContextItemSelected(MenuItem item)728     public boolean onContextItemSelected(MenuItem item) {
729         return handleMenuItemClick(item);
730     }
731 
onCopyDestinationPicked(int resultCode, Intent data)732     private void onCopyDestinationPicked(int resultCode, Intent data) {
733 
734         FileOperation operation = mLocalState.claimPendingOperation();
735 
736         if (resultCode == FragmentActivity.RESULT_CANCELED || data == null) {
737             // User pressed the back button or otherwise cancelled the destination pick. Don't
738             // proceed with the copy.
739             operation.dispose();
740             return;
741         }
742 
743         operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
744         final String jobId = FileOperations.createJobId();
745         mInjector.dialogs.showProgressDialog(jobId, operation);
746         FileOperations.start(
747                 mActivity,
748                 operation,
749                 mInjector.dialogs::showFileOperationStatus,
750                 jobId);
751     }
752 
753     // TODO: Move to UserInputHander.
onContextMenuClick(MotionEvent e)754     protected boolean onContextMenuClick(MotionEvent e) {
755 
756         if (mDetailsLookup.overItemWithSelectionKey(e)) {
757             View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
758             ViewHolder holder = mRecView.getChildViewHolder(childView);
759 
760             View view = holder.itemView;
761             float x = e.getX() - view.getLeft();
762             float y = e.getY() - view.getTop();
763             mInjector.menuManager.showContextMenu(this, view, x, y);
764             return true;
765         }
766 
767         mInjector.menuManager.showContextMenu(this, mRecView, e.getX(), e.getY());
768         return true;
769     }
770 
onItemActivated(ItemDetails<String> item, MotionEvent e)771     private boolean onItemActivated(ItemDetails<String> item, MotionEvent e) {
772         if (((DocumentItemDetails) item).inPreviewIconHotspot(e)) {
773             return mActions.previewItem(item);
774         }
775 
776         return mActions.openItem(
777                 item,
778                 ActionHandler.VIEW_TYPE_PREVIEW,
779                 ActionHandler.VIEW_TYPE_REGULAR);
780     }
781 
onViewModeChanged()782     public void onViewModeChanged() {
783         // Mode change is just visual change; no need to kick loader.
784         mRootView.announceForAccessibility(getString(
785                 mState.derivedMode == State.MODE_GRID ? R.string.grid_mode_showing
786                         : R.string.list_mode_showing));
787         onDisplayStateChanged();
788     }
789 
onDisplayStateChanged()790     private void onDisplayStateChanged() {
791         updateLayout(mState.derivedMode);
792         mRecView.setAdapter(mAdapter);
793     }
794 
795     /**
796      * Updates the layout after the view mode switches.
797      *
798      * @param mode The new view mode.
799      */
updateLayout(@iewMode int mode)800     private void updateLayout(@ViewMode int mode) {
801         mMode = mode;
802         mColumnCount = calculateColumnCount(mode);
803         if (mLayout != null) {
804             mLayout.setSpanCount(mColumnCount);
805         }
806 
807         int pad = getDirectoryPadding(mode);
808         mAppBarHeight = getAppBarLayoutHeight();
809         mSaveLayoutHeight = getSaveLayoutHeight();
810         mRecView.setPadding(pad, mAppBarHeight, pad, mSaveLayoutHeight);
811         mRecView.requestLayout();
812         mIconHelper.setViewMode(mode);
813 
814         int range = getResources().getDimensionPixelOffset(R.dimen.refresh_icon_range);
815         mRefreshLayout.setProgressViewOffset(true, mAppBarHeight, mAppBarHeight + range);
816     }
817 
getAppBarLayoutHeight()818     private int getAppBarLayoutHeight() {
819         View appBarLayout = getActivity().findViewById(R.id.app_bar);
820         View collapsingBar = getActivity().findViewById(R.id.collapsing_toolbar);
821         return collapsingBar == null ? 0 : appBarLayout.getHeight();
822     }
823 
getSaveLayoutHeight()824     private int getSaveLayoutHeight() {
825         View containerSave = getActivity().findViewById(R.id.container_save);
826         return containerSave == null ? 0 : containerSave.getHeight();
827     }
828 
829     /**
830      * Updates the layout after the view mode switches.
831      *
832      * @param scale The new view mode.
833      */
scaleLayout(float scale)834     private void scaleLayout(float scale) {
835         assert DEBUG;
836 
837         if (VERBOSE) {
838             Log.v(
839                     TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale);
840         }
841 
842         if (mMode == MODE_GRID) {
843             float minScale = getFraction(R.fraction.grid_scale_min);
844             float maxScale = getFraction(R.fraction.grid_scale_max);
845             float nextScale = mLiveScale * scale;
846 
847             if (VERBOSE) {
848                 Log.v(TAG,
849                         "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale);
850             }
851 
852             if (nextScale > minScale && nextScale < maxScale) {
853                 if (DEBUG) {
854                     Log.d(TAG, "Updating grid scale: " + scale);
855                 }
856                 mLiveScale = nextScale;
857                 updateLayout(mMode);
858             }
859 
860         } else {
861             if (DEBUG) {
862                 Log.d(TAG, "List mode, ignoring scale: " + scale);
863             }
864             mLiveScale = 1.0f;
865         }
866     }
867 
calculateColumnCount(@iewMode int mode)868     private int calculateColumnCount(@ViewMode int mode) {
869         // For fixing a11y issue b/141223688, if there's only "no items" displayed, we should set
870         // span column to 1 to avoid talkback speaking unnecessary information.
871         if (mModel != null && mModel.getItemCount() == 0) {
872             return 1;
873         }
874 
875         if (mode == MODE_LIST) {
876             // List mode is a "grid" with 1 column.
877             return 1;
878         }
879 
880         int cellWidth = getScaledSize(R.dimen.grid_width);
881         int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin);
882         int viewPadding =
883                 (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale);
884 
885         // RecyclerView sometimes gets a width of 0 (see b/27150284).
886         // Clamp so that we always lay out the grid with at least 2 columns by default.
887         // If on photo picking state, the UI should show 3 images a row or 2 folders a row,
888         // so use 6 columns by default and set folder size to 3 and document size is to 2.
889         mColumnUnit = mState.isPhotoPicking() ? 3 : 1;
890         int columnCount = mColumnUnit * Math.max(2,
891                 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
892 
893         // Finally with our grid count logic firmly in place, we apply any live scaling
894         // captured by the scale gesture detector.
895         return Math.max(1, Math.round(columnCount / mLiveScale));
896     }
897 
898 
899     /**
900      * Moderately abuse the "fraction" resource type for our purposes.
901      */
getFraction(@ractionRes int id)902     private float getFraction(@FractionRes int id) {
903         return getResources().getFraction(id, 1, 0);
904     }
905 
getScaledSize(@imenRes int id)906     private int getScaledSize(@DimenRes int id) {
907         return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
908     }
909 
getDirectoryPadding(@iewMode int mode)910     private int getDirectoryPadding(@ViewMode int mode) {
911         switch (mode) {
912             case MODE_GRID:
913                 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
914             case MODE_LIST:
915                 return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
916             default:
917                 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
918         }
919     }
920 
handleMenuItemClick(MenuItem item)921     private boolean handleMenuItemClick(MenuItem item) {
922         if (mInjector.pickResult != null) {
923             mInjector.pickResult.increaseActionCount();
924         }
925         MutableSelection<String> selection = new MutableSelection<>();
926         mSelectionMgr.copySelection(selection);
927 
928         final int id = item.getItemId();
929         if (id == R.id.action_menu_select || id == R.id.dir_menu_open) {
930             openDocuments(selection);
931             mActionModeController.finishActionMode();
932             return true;
933         } else if (id == R.id.action_menu_open_with || id == R.id.dir_menu_open_with) {
934             showChooserForDoc(selection);
935             return true;
936         } else if (id == R.id.dir_menu_open_in_new_window) {
937             mActions.openSelectedInNewWindow();
938             return true;
939         } else if (id == R.id.action_menu_share || id == R.id.dir_menu_share) {
940             mActions.shareSelectedDocuments();
941             return true;
942         } else if (id == R.id.action_menu_delete || id == R.id.dir_menu_delete) {
943             // deleteDocuments will end action mode if the documents are deleted.
944             // It won't end action mode if user cancels the delete.
945             mActions.showDeleteDialog();
946             return true;
947         } else if (id == R.id.action_menu_copy_to) {
948             transferDocuments(selection, null, FileOperationService.OPERATION_COPY);
949             // TODO: Only finish selection mode if copy-to is not canceled.
950             // Need to plum down into handling the way we do with deleteDocuments.
951             mActionModeController.finishActionMode();
952             return true;
953         } else if (id == R.id.action_menu_compress) {
954             transferDocuments(selection, mState.stack,
955                     FileOperationService.OPERATION_COMPRESS);
956             // TODO: Only finish selection mode if compress is not canceled.
957             // Need to plum down into handling the way we do with deleteDocuments.
958             mActionModeController.finishActionMode();
959             return true;
960 
961             // TODO: Implement extract (to the current directory).
962         } else if (id == R.id.action_menu_extract_to) {
963             transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT);
964             // TODO: Only finish selection mode if compress-to is not canceled.
965             // Need to plum down into handling the way we do with deleteDocuments.
966             mActionModeController.finishActionMode();
967             return true;
968         } else if (id == R.id.action_menu_move_to) {
969             if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
970                 mInjector.dialogs.showOperationUnsupported();
971                 return true;
972             }
973             // Exit selection mode first, so we avoid deselecting deleted documents.
974             mActionModeController.finishActionMode();
975             transferDocuments(selection, null, FileOperationService.OPERATION_MOVE);
976             return true;
977         } else if (id == R.id.action_menu_inspect || id == R.id.dir_menu_inspect) {
978             mActionModeController.finishActionMode();
979             assert selection.size() <= 1;
980             DocumentInfo doc = selection.isEmpty()
981                     ? mActivity.getCurrentDirectory()
982                     : mModel.getDocuments(selection).get(0);
983 
984             mActions.showInspector(doc);
985             return true;
986         } else if (id == R.id.dir_menu_cut_to_clipboard) {
987             mActions.cutToClipboard();
988             return true;
989         } else if (id == R.id.dir_menu_copy_to_clipboard) {
990             mActions.copyToClipboard();
991             return true;
992         } else if (id == R.id.dir_menu_paste_from_clipboard) {
993             pasteFromClipboard();
994             return true;
995         } else if (id == R.id.dir_menu_paste_into_folder) {
996             pasteIntoFolder();
997             return true;
998         } else if (id == R.id.action_menu_select_all || id == R.id.dir_menu_select_all) {
999             mActions.selectAllFiles();
1000             return true;
1001         } else if (id == R.id.action_menu_deselect_all || id == R.id.dir_menu_deselect_all) {
1002             mActions.deselectAllFiles();
1003             return true;
1004         } else if (id == R.id.action_menu_rename || id == R.id.dir_menu_rename) {
1005             renameDocuments(selection);
1006             return true;
1007         } else if (id == R.id.dir_menu_create_dir) {
1008             mActions.showCreateDirectoryDialog();
1009             return true;
1010         } else if (id == R.id.dir_menu_view_in_owner) {
1011             mActions.viewInOwner();
1012             return true;
1013         } else if (id == R.id.action_menu_sort) {
1014             mActions.showSortDialog();
1015             return true;
1016         }
1017         if (DEBUG) {
1018             Log.d(TAG, "Unhandled menu item selected: " + item);
1019         }
1020         return false;
1021     }
1022 
onAccessibilityClick(View child)1023     private boolean onAccessibilityClick(View child) {
1024         if (mSelectionMgr.hasSelection()) {
1025             selectItem(child);
1026         } else {
1027             DocumentHolder holder = getDocumentHolder(child);
1028             mActions.openItem(holder.getItemDetails(), ActionHandler.VIEW_TYPE_PREVIEW,
1029                     ActionHandler.VIEW_TYPE_REGULAR);
1030         }
1031         return true;
1032     }
1033 
onAccessibilityLongClick(View child)1034     private boolean onAccessibilityLongClick(View child) {
1035         selectItem(child);
1036         return true;
1037     }
1038 
selectItem(View child)1039     private void selectItem(View child) {
1040         final String id = getModelId(child);
1041         if (mSelectionMgr.isSelected(id)) {
1042             mSelectionMgr.deselect(id);
1043         } else {
1044             mSelectionMgr.select(id);
1045         }
1046     }
1047 
cancelThumbnailTask(View view)1048     private void cancelThumbnailTask(View view) {
1049         final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
1050         if (iconThumb != null) {
1051             mIconHelper.stopLoading(iconThumb);
1052         }
1053     }
1054 
1055     // Support for opening multiple documents is currently exclusive to DocumentsActivity.
openDocuments(final Selection selected)1056     private void openDocuments(final Selection selected) {
1057         Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN);
1058 
1059         if (selected.isEmpty()) {
1060             return;
1061         }
1062 
1063         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
1064         List<DocumentInfo> docs = mModel.getDocuments(selected);
1065         if (docs.size() > 1) {
1066             mActivity.onDocumentsPicked(docs);
1067         } else {
1068             mActivity.onDocumentPicked(docs.get(0));
1069         }
1070     }
1071 
showChooserForDoc(final Selection<String> selected)1072     private void showChooserForDoc(final Selection<String> selected) {
1073         Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN);
1074 
1075         if (selected.isEmpty()) {
1076             return;
1077         }
1078 
1079         assert selected.size() == 1;
1080         DocumentInfo doc =
1081                 DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
1082         mActions.showChooserForDoc(doc);
1083     }
1084 
transferDocuments( final Selection<String> selected, @Nullable DocumentStack destination, final @OpType int mode)1085     private void transferDocuments(
1086             final Selection<String> selected, @Nullable DocumentStack destination,
1087             final @OpType int mode) {
1088         if (selected.isEmpty()) {
1089             return;
1090         }
1091 
1092         switch (mode) {
1093             case FileOperationService.OPERATION_COPY:
1094                 Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_TO);
1095                 break;
1096             case FileOperationService.OPERATION_COMPRESS:
1097                 Metrics.logUserAction(MetricConsts.USER_ACTION_COMPRESS);
1098                 break;
1099             case FileOperationService.OPERATION_EXTRACT:
1100                 Metrics.logUserAction(MetricConsts.USER_ACTION_EXTRACT_TO);
1101                 break;
1102             case FileOperationService.OPERATION_MOVE:
1103                 Metrics.logUserAction(MetricConsts.USER_ACTION_MOVE_TO);
1104                 break;
1105         }
1106 
1107         UrisSupplier srcs;
1108         try {
1109             ClipStore clipStorage = DocumentsApplication.getClipStore(getContext());
1110             srcs = UrisSupplier.create(selected, mModel::getItemUri, clipStorage);
1111         } catch (IOException e) {
1112             throw new RuntimeException("Failed to create uri supplier.", e);
1113         }
1114 
1115         final DocumentInfo parent = mActivity.getCurrentDirectory();
1116         final FileOperation operation = new FileOperation.Builder()
1117                 .withOpType(mode)
1118                 .withSrcParent(parent == null ? null : parent.derivedUri)
1119                 .withSrcs(srcs)
1120                 .build();
1121 
1122         if (destination != null) {
1123             operation.setDestination(destination);
1124             final String jobId = FileOperations.createJobId();
1125             mInjector.dialogs.showProgressDialog(jobId, operation);
1126             FileOperations.start(
1127                     mActivity,
1128                     operation,
1129                     mInjector.dialogs::showFileOperationStatus,
1130                     jobId);
1131             return;
1132         }
1133 
1134         // Pop up a dialog to pick a destination.  This is inadequate but works for now.
1135         // TODO: Implement a picker that is to spec.
1136         mLocalState.mPendingOperation = operation;
1137         final Intent intent = new Intent(
1138                 Shared.ACTION_PICK_COPY_DESTINATION,
1139                 Uri.EMPTY,
1140                 getActivity(),
1141                 PickActivity.class);
1142 
1143         // Set an appropriate title on the drawer when it is shown in the picker.
1144         // Coupled with the fact that we auto-open the drawer for copy/move operations
1145         // it should basically be the thing people see first.
1146         int drawerTitleId;
1147         switch (mode) {
1148             case FileOperationService.OPERATION_COPY:
1149                 drawerTitleId = R.string.menu_copy;
1150                 break;
1151             case FileOperationService.OPERATION_COMPRESS:
1152                 drawerTitleId = R.string.menu_compress;
1153                 break;
1154             case FileOperationService.OPERATION_EXTRACT:
1155                 drawerTitleId = R.string.menu_extract;
1156                 break;
1157             case FileOperationService.OPERATION_MOVE:
1158                 drawerTitleId = R.string.menu_move;
1159                 break;
1160             default:
1161                 throw new UnsupportedOperationException("Unknown mode: " + mode);
1162         }
1163 
1164         intent.putExtra(DocumentsContract.EXTRA_PROMPT, drawerTitleId);
1165 
1166         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
1167         List<DocumentInfo> docs = mModel.getDocuments(selected);
1168 
1169         // Determine if there is a directory in the set of documents
1170         // to be copied? Why? Directory creation isn't supported by some roots
1171         // (like Downloads). This informs DocumentsActivity (the "picker")
1172         // to restrict available roots to just those with support.
1173         intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode);
1174 
1175         // This just identifies the type of request...we'll check it
1176         // when we reveive a response.
1177         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
1178     }
1179 
1180     @Override
onActivityResult(@equestCode int requestCode, int resultCode, Intent data)1181     public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
1182         switch (requestCode) {
1183             case REQUEST_COPY_DESTINATION:
1184                 onCopyDestinationPicked(resultCode, data);
1185                 break;
1186             default:
1187                 throw new UnsupportedOperationException("Unknown request code: " + requestCode);
1188         }
1189     }
1190 
renameDocuments(Selection selected)1191     private void renameDocuments(Selection selected) {
1192         Metrics.logUserAction(MetricConsts.USER_ACTION_RENAME);
1193 
1194         if (selected.isEmpty()) {
1195             return;
1196         }
1197 
1198         // Batch renaming not supported
1199         // Rename option is only available in menu when 1 document selected
1200         assert selected.size() == 1;
1201 
1202         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
1203         List<DocumentInfo> docs = mModel.getDocuments(selected);
1204         RenameDocumentFragment.show(getChildFragmentManager(), docs.get(0));
1205     }
1206 
getModel()1207     Model getModel() {
1208         return mModel;
1209     }
1210 
1211     /**
1212      * Paste selection files from the primary clip into the current window.
1213      */
pasteFromClipboard()1214     public void pasteFromClipboard() {
1215         Metrics.logUserAction(MetricConsts.USER_ACTION_PASTE_CLIPBOARD);
1216         // Since we are pasting into the current window, we already have the destination in the
1217         // stack. No need for a destination DocumentInfo.
1218         mClipper.copyFromClipboard(
1219                 mState.stack,
1220                 mInjector.dialogs::showFileOperationStatus);
1221         getActivity().invalidateOptionsMenu();
1222     }
1223 
pasteIntoFolder()1224     public void pasteIntoFolder() {
1225         if (mSelectionMgr.getSelection().isEmpty()) {
1226             return;
1227         }
1228         assert (mSelectionMgr.getSelection().size() == 1);
1229 
1230         String modelId = mSelectionMgr.getSelection().iterator().next();
1231         Cursor dstCursor = mModel.getItem(modelId);
1232         if (dstCursor == null) {
1233             Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
1234             return;
1235         }
1236         DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
1237         mClipper.copyFromClipboard(
1238                 destination,
1239                 mState.stack,
1240                 mInjector.dialogs::showFileOperationStatus);
1241         getActivity().invalidateOptionsMenu();
1242     }
1243 
setupDragAndDropOnDocumentView(View view, Cursor cursor)1244     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1245         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1246         if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1247             // Make a directory item a drop target. Drop on non-directories and empty space
1248             // is handled at the list/grid view level.
1249             view.setOnDragListener(mDragHoverListener);
1250         }
1251     }
1252 
getDestination(View v)1253     private DocumentInfo getDestination(View v) {
1254         String id = getModelId(v);
1255         if (id != null) {
1256             Cursor dstCursor = mModel.getItem(id);
1257             if (dstCursor == null) {
1258                 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
1259                 return null;
1260             }
1261             return DocumentInfo.fromDirectoryCursor(dstCursor);
1262         }
1263 
1264         if (v == mRecView) {
1265             return mActivity.getCurrentDirectory();
1266         }
1267 
1268         return null;
1269     }
1270 
1271     /**
1272      * Gets the model ID for a given RecyclerView item.
1273      *
1274      * @param view A View that is a document item view, or a child of a document item view.
1275      * @return The Model ID for the given document, or null if the given view is not associated with
1276      * a document item view.
1277      */
getModelId(View view)1278     private @Nullable String getModelId(View view) {
1279         View itemView = mRecView.findContainingItemView(view);
1280         if (itemView != null) {
1281             RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1282             if (vh instanceof DocumentHolder) {
1283                 return ((DocumentHolder) vh).getModelId();
1284             }
1285         }
1286         return null;
1287     }
1288 
getDocumentHolder(View v)1289     private @Nullable DocumentHolder getDocumentHolder(View v) {
1290         RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1291         if (vh instanceof DocumentHolder) {
1292             return (DocumentHolder) vh;
1293         }
1294         return null;
1295     }
1296 
1297     /**
1298      * Add or remove mToolbarPreDrawListener implement on DirectoryFragment to ViewTreeObserver.
1299      */
setPreDrawListenerEnabled(boolean enable)1300     public void setPreDrawListenerEnabled(boolean enable) {
1301         if (mActivity == null) {
1302             return;
1303         }
1304 
1305         final View bar = mActivity.findViewById(R.id.collapsing_toolbar);
1306         if (bar != null) {
1307             bar.getViewTreeObserver().removeOnPreDrawListener(mToolbarPreDrawListener);
1308             if (enable) {
1309                 bar.getViewTreeObserver().addOnPreDrawListener(mToolbarPreDrawListener);
1310             }
1311         }
1312     }
1313 
showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim)1314     public static void showDirectory(
1315             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1316         if (DEBUG) {
1317             Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc));
1318         }
1319         create(fm, root, doc, anim);
1320     }
1321 
showRecentsOpen(FragmentManager fm, int anim)1322     public static void showRecentsOpen(FragmentManager fm, int anim) {
1323         create(fm, null, null, anim);
1324     }
1325 
create( FragmentManager fm, RootInfo root, @Nullable DocumentInfo doc, @AnimationType int anim)1326     public static void create(
1327             FragmentManager fm,
1328             RootInfo root,
1329             @Nullable DocumentInfo doc,
1330             @AnimationType int anim) {
1331 
1332         if (DEBUG) {
1333             if (doc == null) {
1334                 Log.d(TAG, "Creating new fragment null directory");
1335             } else {
1336                 Log.d(TAG, "Creating new fragment for directory: " + DocumentInfo.debugString(doc));
1337             }
1338         }
1339 
1340         final Bundle args = new Bundle();
1341         args.putParcelable(Shared.EXTRA_ROOT, root);
1342         args.putParcelable(Shared.EXTRA_DOC, doc);
1343 
1344         final FragmentTransaction ft = fm.beginTransaction();
1345         AnimationView.setupAnimations(ft, anim, args);
1346 
1347         final DirectoryFragment fragment = new DirectoryFragment();
1348         fragment.setArguments(args);
1349 
1350         ft.replace(getFragmentId(), fragment);
1351         ft.commitAllowingStateLoss();
1352     }
1353 
1354     /** Gets the fragment from the fragment manager. */
get(FragmentManager fm)1355     public static @Nullable DirectoryFragment get(FragmentManager fm) {
1356         // TODO: deal with multiple directories shown at once
1357         Fragment fragment = fm.findFragmentById(getFragmentId());
1358         return fragment instanceof DirectoryFragment
1359                 ? (DirectoryFragment) fragment
1360                 : null;
1361     }
1362 
getFragmentId()1363     private static int getFragmentId() {
1364         return R.id.container_directory;
1365     }
1366 
1367     /**
1368      * Scroll to top of recyclerView in fragment
1369      */
scrollToTop()1370     public void scrollToTop() {
1371         if (mRecView != null) {
1372             mRecView.scrollToPosition(0);
1373         }
1374     }
1375 
1376     /**
1377      * Stop the scroll of recyclerView in fragment
1378      */
stopScroll()1379     public void stopScroll() {
1380         if (mRecView != null) {
1381             mRecView.stopScroll();
1382         }
1383     }
1384 
1385     @Override
onRefresh()1386     public void onRefresh() {
1387         // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it
1388         // should be covered by last modified value we store in thumbnail cache, but rather to give
1389         // the user a greater sense that contents are being reloaded.
1390         ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext());
1391         String[] ids = mModel.getModelIds();
1392         int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT);
1393         for (int i = 0; i < numOfEvicts; ++i) {
1394             cache.removeUri(mModel.getItemUri(ids[i]), mModel.getItemUserId(ids[i]));
1395         }
1396 
1397         final DocumentInfo doc = mActivity.getCurrentDirectory();
1398         if (doc == null && !mActivity.getSelectedUser().isQuietModeEnabled(mActivity)) {
1399             // If there is no root doc, try to reload the root doc from root info.
1400             Log.w(TAG, "No root document. Try to get root document.");
1401             getRootDocumentAndMaybeRefreshDocument();
1402             return;
1403         }
1404         mActions.refreshDocument(doc, (boolean refreshSupported) -> {
1405             if (refreshSupported) {
1406                 mRefreshLayout.setRefreshing(false);
1407             } else {
1408                 // If Refresh API isn't available, we will explicitly reload the loader
1409                 mActions.loadDocumentsForCurrentStack();
1410             }
1411         });
1412     }
1413 
getRootDocumentAndMaybeRefreshDocument()1414     private void getRootDocumentAndMaybeRefreshDocument() {
1415         // If we can reload the root doc successfully, we will push it to the stack and load the
1416         // stack.
1417         final RootInfo emptyDocRoot = mActivity.getCurrentRoot();
1418         mInjector.actions.getRootDocument(
1419                 emptyDocRoot,
1420                 TimeoutTask.DEFAULT_TIMEOUT,
1421                 rootDoc -> {
1422                     mRefreshLayout.setRefreshing(false);
1423                     if (rootDoc != null && mActivity.getCurrentDirectory() == null) {
1424                         // Make sure the stack does not change during task was running.
1425                         mState.stack.push(rootDoc);
1426                         mActivity.updateNavigator();
1427                         mActions.loadDocumentsForCurrentStack();
1428                     }
1429                 }
1430         );
1431     }
1432 
1433     private final class ModelUpdateListener implements EventListener<Model.Update> {
1434 
1435         @Override
accept(Model.Update update)1436         public void accept(Model.Update update) {
1437             if (DEBUG) {
1438                 Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
1439             }
1440 
1441             mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
1442 
1443             updateLayout(mState.derivedMode);
1444 
1445             // Update the selection to remove any disappeared IDs.
1446             Iterator<String> selectionIter = mSelectionMgr.getSelection().iterator();
1447             while (selectionIter.hasNext()) {
1448                 if (!mAdapter.getStableIds().contains(selectionIter.next())) {
1449                     selectionIter.remove();
1450                 }
1451             }
1452 
1453             mAdapter.notifyDataSetChanged();
1454 
1455             if (mRestoredState != null) {
1456                 mSelectionMgr.onRestoreInstanceState(mRestoredState);
1457                 mRestoredState = null;
1458             }
1459 
1460             // Restore any previous instance state
1461             final SparseArray<Parcelable> container =
1462                     mState.dirConfigs.remove(mLocalState.getConfigKey());
1463             final int curSortedDimensionId = mState.sortModel.getSortedDimensionId();
1464 
1465             final SortDimension curSortedDimension =
1466                     mState.sortModel.getDimensionById(curSortedDimensionId);
1467 
1468             // Default not restore to avoid app bar layout expand to confuse users.
1469             if (container != null
1470                     && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, true)) {
1471                 getView().restoreHierarchyState(container);
1472             } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
1473                     || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
1474                     || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
1475                 // Scroll to the top if the sort order actually changed.
1476                 mRecView.smoothScrollToPosition(0);
1477             }
1478 
1479             mLocalState.mLastSortDimensionId = curSortedDimension.getId();
1480             mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
1481 
1482             if (mRefreshLayout.isRefreshing()) {
1483                 new Handler().postDelayed(
1484                         () -> mRefreshLayout.setRefreshing(false),
1485                         REFRESH_SPINNER_TIMEOUT);
1486             }
1487 
1488             if (!mModel.isLoading()) {
1489                 mActivity.notifyDirectoryLoaded(
1490                         mModel.doc != null ? mModel.doc.derivedUri : null);
1491                 // For orientation changed case, sometimes the docs loading comes after the menu
1492                 // update. We need to update the menu here to ensure the status is correct.
1493                 mInjector.menuManager.updateModel(mModel);
1494                 mInjector.menuManager.updateOptionMenu();
1495                 if (VersionUtils.isAtLeastS()) {
1496                     mActivity.updateHeader(update.hasCrossProfileException());
1497                 } else {
1498                     mActivity.updateHeaderTitle();
1499                 }
1500             }
1501         }
1502     }
1503 
1504     private final class AdapterEnvironment implements DocumentsAdapter.Environment {
1505 
1506         @Override
getFeatures()1507         public Features getFeatures() {
1508             return mInjector.features;
1509         }
1510 
1511         @Override
getContext()1512         public Context getContext() {
1513             return mActivity;
1514         }
1515 
1516         @Override
getDisplayState()1517         public State getDisplayState() {
1518             return mState;
1519         }
1520 
1521         @Override
isInSearchMode()1522         public boolean isInSearchMode() {
1523             return mInjector.searchManager.isSearching();
1524         }
1525 
1526         @Override
getModel()1527         public Model getModel() {
1528             return mModel;
1529         }
1530 
1531         @Override
getColumnCount()1532         public int getColumnCount() {
1533             return mColumnCount;
1534         }
1535 
1536         @Override
isSelected(String id)1537         public boolean isSelected(String id) {
1538             return mSelectionMgr.isSelected(id);
1539         }
1540 
1541         @Override
isDocumentEnabled(String mimeType, int flags)1542         public boolean isDocumentEnabled(String mimeType, int flags) {
1543             return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
1544         }
1545 
1546         @Override
initDocumentHolder(DocumentHolder holder)1547         public void initDocumentHolder(DocumentHolder holder) {
1548             holder.addKeyEventListener(mKeyListener);
1549             holder.itemView.setOnFocusChangeListener(mFocusManager);
1550         }
1551 
1552         @Override
onBindDocumentHolder(DocumentHolder holder, Cursor cursor)1553         public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
1554             setupDragAndDropOnDocumentView(holder.itemView, cursor);
1555         }
1556 
1557         @Override
getActionHandler()1558         public ActionHandler getActionHandler() {
1559             return mActions;
1560         }
1561 
1562         @Override
getCallingAppName()1563         public String getCallingAppName() {
1564             return Shared.getCallingAppName(mActivity);
1565         }
1566     }
1567 }
1568