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