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