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