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