• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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;
18 
19 import static com.android.documentsui.base.DocumentInfo.getCursorInt;
20 import static com.android.documentsui.base.DocumentInfo.getCursorString;
21 import static com.android.documentsui.base.SharedMinimal.DEBUG;
22 
23 import android.app.PendingIntent;
24 import android.content.ActivityNotFoundException;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentSender;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Parcelable;
35 import android.provider.DocumentsContract;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.DragEvent;
39 
40 import androidx.annotation.VisibleForTesting;
41 import androidx.fragment.app.FragmentActivity;
42 import androidx.loader.app.LoaderManager.LoaderCallbacks;
43 import androidx.loader.content.Loader;
44 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
45 import androidx.recyclerview.selection.MutableSelection;
46 import androidx.recyclerview.selection.SelectionTracker;
47 
48 import com.android.documentsui.AbstractActionHandler.CommonAddons;
49 import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
50 import com.android.documentsui.base.BooleanConsumer;
51 import com.android.documentsui.base.DocumentInfo;
52 import com.android.documentsui.base.DocumentStack;
53 import com.android.documentsui.base.Lookup;
54 import com.android.documentsui.base.MimeTypes;
55 import com.android.documentsui.base.Providers;
56 import com.android.documentsui.base.RootInfo;
57 import com.android.documentsui.base.Shared;
58 import com.android.documentsui.base.State;
59 import com.android.documentsui.base.UserId;
60 import com.android.documentsui.dirlist.AnimationView;
61 import com.android.documentsui.dirlist.AnimationView.AnimationType;
62 import com.android.documentsui.dirlist.FocusHandler;
63 import com.android.documentsui.files.LauncherActivity;
64 import com.android.documentsui.files.QuickViewIntentBuilder;
65 import com.android.documentsui.queries.SearchViewManager;
66 import com.android.documentsui.roots.GetRootDocumentTask;
67 import com.android.documentsui.roots.LoadFirstRootTask;
68 import com.android.documentsui.roots.LoadRootTask;
69 import com.android.documentsui.roots.ProvidersAccess;
70 import com.android.documentsui.sidebar.EjectRootTask;
71 import com.android.documentsui.sorting.SortListFragment;
72 import com.android.documentsui.ui.DialogController;
73 import com.android.documentsui.ui.Snackbars;
74 
75 import java.util.ArrayList;
76 import java.util.List;
77 import java.util.Objects;
78 import java.util.concurrent.Executor;
79 import java.util.concurrent.Semaphore;
80 import java.util.function.Consumer;
81 
82 import javax.annotation.Nullable;
83 
84 /**
85  * Provides support for specializing the actions (openDocument etc.) to the host activity.
86  */
87 public abstract class AbstractActionHandler<T extends FragmentActivity & CommonAddons>
88         implements ActionHandler {
89 
90     @VisibleForTesting
91     public static final int CODE_AUTHENTICATION = 43;
92 
93     @VisibleForTesting
94     static final int LOADER_ID = 42;
95 
96     private static final String TAG = "AbstractActionHandler";
97     private static final int REFRESH_SPINNER_TIMEOUT = 500;
98     private final Semaphore mLoaderSemaphore = new Semaphore(1);
99 
100     protected final T mActivity;
101     protected final State mState;
102     protected final ProvidersAccess mProviders;
103     protected final DocumentsAccess mDocs;
104     protected final FocusHandler mFocusHandler;
105     protected final SelectionTracker<String> mSelectionMgr;
106     protected final SearchViewManager mSearchMgr;
107     protected final Lookup<String, Executor> mExecutors;
108     protected final DialogController mDialogs;
109     protected final Model mModel;
110     protected final Injector<?> mInjector;
111 
112     private final LoaderBindings mBindings;
113 
114     private Runnable mDisplayStateChangedListener;
115 
116     private ContentLock mContentLock;
117 
118     @Override
registerDisplayStateChangedListener(Runnable l)119     public void registerDisplayStateChangedListener(Runnable l) {
120         mDisplayStateChangedListener = l;
121     }
122     @Override
unregisterDisplayStateChangedListener(Runnable l)123     public void unregisterDisplayStateChangedListener(Runnable l) {
124         if (mDisplayStateChangedListener == l) {
125             mDisplayStateChangedListener = null;
126         }
127     }
128 
AbstractActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector<?> injector)129     public AbstractActionHandler(
130             T activity,
131             State state,
132             ProvidersAccess providers,
133             DocumentsAccess docs,
134             SearchViewManager searchMgr,
135             Lookup<String, Executor> executors,
136             Injector<?> injector) {
137 
138         assert(activity != null);
139         assert(state != null);
140         assert(providers != null);
141         assert(searchMgr != null);
142         assert(docs != null);
143         assert(injector != null);
144 
145         mActivity = activity;
146         mState = state;
147         mProviders = providers;
148         mDocs = docs;
149         mFocusHandler = injector.focusManager;
150         mSelectionMgr = injector.selectionMgr;
151         mSearchMgr = searchMgr;
152         mExecutors = executors;
153         mDialogs = injector.dialogs;
154         mModel = injector.getModel();
155         mInjector = injector;
156 
157         mBindings = new LoaderBindings();
158     }
159 
160     @Override
ejectRoot(RootInfo root, BooleanConsumer listener)161     public void ejectRoot(RootInfo root, BooleanConsumer listener) {
162         new EjectRootTask(
163                 mActivity.getContentResolver(),
164                 root.authority,
165                 root.rootId,
166                 listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
167     }
168 
169     @Override
startAuthentication(PendingIntent intent)170     public void startAuthentication(PendingIntent intent) {
171         try {
172             mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION,
173                     null, 0, 0, 0);
174         } catch (IntentSender.SendIntentException cancelled) {
175             Log.d(TAG, "Authentication Pending Intent either canceled or ignored.");
176         }
177     }
178 
179     @Override
requestQuietModeDisabled(RootInfo info, UserId userId)180     public void requestQuietModeDisabled(RootInfo info, UserId userId) {
181         new RequestQuietModeDisabledTask(mActivity, userId).execute();
182     }
183 
184     @Override
onActivityResult(int requestCode, int resultCode, Intent data)185     public void onActivityResult(int requestCode, int resultCode, Intent data) {
186         switch (requestCode) {
187             case CODE_AUTHENTICATION:
188                 onAuthenticationResult(resultCode);
189                 break;
190         }
191     }
192 
onAuthenticationResult(int resultCode)193     private void onAuthenticationResult(int resultCode) {
194         if (resultCode == FragmentActivity.RESULT_OK) {
195             Log.v(TAG, "Authentication was successful. Refreshing directory now.");
196             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
197         }
198     }
199 
200     @Override
getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback)201     public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) {
202         GetRootDocumentTask task = new GetRootDocumentTask(
203                 root,
204                 mActivity,
205                 timeout,
206                 mDocs,
207                 callback);
208 
209         task.executeOnExecutor(mExecutors.lookup(root.authority));
210     }
211 
212     @Override
refreshDocument(DocumentInfo doc, BooleanConsumer callback)213     public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) {
214         RefreshTask task = new RefreshTask(
215                 mInjector.features,
216                 mState,
217                 doc,
218                 REFRESH_SPINNER_TIMEOUT,
219                 mActivity.getApplicationContext(),
220                 mActivity::isDestroyed,
221                 callback);
222         task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority));
223     }
224 
225     @Override
openSelectedInNewWindow()226     public void openSelectedInNewWindow() {
227         throw new UnsupportedOperationException("Can't open in new window.");
228     }
229 
230     @Override
openInNewWindow(DocumentStack path)231     public void openInNewWindow(DocumentStack path) {
232         Metrics.logUserAction(MetricConsts.USER_ACTION_NEW_WINDOW);
233 
234         Intent intent = LauncherActivity.createLaunchIntent(mActivity);
235         intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
236 
237         // Multi-window necessitates we pick how we are launched.
238         // By default we'd be launched in-place above the existing app.
239         // By setting launch-to-side ActivityManager will open us to side.
240         if (mActivity.isInMultiWindowMode()) {
241             intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
242         }
243 
244         mActivity.startActivity(intent);
245     }
246 
247     @Override
openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback)248     public boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) {
249         throw new UnsupportedOperationException("Can't open document.");
250     }
251 
252     @Override
showInspector(DocumentInfo doc)253     public void showInspector(DocumentInfo doc) {
254         throw new UnsupportedOperationException("Can't open properties.");
255     }
256 
257     @Override
springOpenDirectory(DocumentInfo doc)258     public void springOpenDirectory(DocumentInfo doc) {
259         throw new UnsupportedOperationException("Can't spring open directories.");
260     }
261 
262     @Override
openSettings(RootInfo root)263     public void openSettings(RootInfo root) {
264         throw new UnsupportedOperationException("Can't open settings.");
265     }
266 
267     @Override
openRoot(ResolveInfo app, UserId userId)268     public void openRoot(ResolveInfo app, UserId userId) {
269         throw new UnsupportedOperationException("Can't open an app.");
270     }
271 
272     @Override
showAppDetails(ResolveInfo info, UserId userId)273     public void showAppDetails(ResolveInfo info, UserId userId) {
274         throw new UnsupportedOperationException("Can't show app details.");
275     }
276 
277     @Override
dropOn(DragEvent event, RootInfo root)278     public boolean dropOn(DragEvent event, RootInfo root) {
279         throw new UnsupportedOperationException("Can't open an app.");
280     }
281 
282     @Override
pasteIntoFolder(RootInfo root)283     public void pasteIntoFolder(RootInfo root) {
284         throw new UnsupportedOperationException("Can't paste into folder.");
285     }
286 
287     @Override
viewInOwner()288     public void viewInOwner() {
289         throw new UnsupportedOperationException("Can't view in application.");
290     }
291 
292     @Override
selectAllFiles()293     public void selectAllFiles() {
294         Metrics.logUserAction(MetricConsts.USER_ACTION_SELECT_ALL);
295         Model model = mInjector.getModel();
296 
297         // Exclude disabled files
298         List<String> enabled = new ArrayList<>();
299         for (String id : model.getModelIds()) {
300             Cursor cursor = model.getItem(id);
301             if (cursor == null) {
302                 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
303                 continue;
304             }
305             String docMimeType = getCursorString(
306                     cursor, DocumentsContract.Document.COLUMN_MIME_TYPE);
307             int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS);
308             if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) {
309                 enabled.add(id);
310             }
311         }
312 
313         // Only select things currently visible in the adapter.
314         boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
315         if (changed) {
316             mDisplayStateChangedListener.run();
317         }
318     }
319 
320     @Override
deselectAllFiles()321     public void deselectAllFiles() {
322         mSelectionMgr.clearSelection();
323     }
324 
325     @Override
showCreateDirectoryDialog()326     public void showCreateDirectoryDialog() {
327         Metrics.logUserAction(MetricConsts.USER_ACTION_CREATE_DIR);
328 
329         CreateDirectoryFragment.show(mActivity.getSupportFragmentManager());
330     }
331 
332     @Override
showSortDialog()333     public void showSortDialog() {
334         SortListFragment.show(mActivity.getSupportFragmentManager(), mState.sortModel);
335     }
336 
337     @Override
338     @Nullable
renameDocument(String name, DocumentInfo document)339     public DocumentInfo renameDocument(String name, DocumentInfo document) {
340         throw new UnsupportedOperationException("Can't rename documents.");
341     }
342 
343     @Override
showChooserForDoc(DocumentInfo doc)344     public void showChooserForDoc(DocumentInfo doc) {
345         throw new UnsupportedOperationException("Show chooser for doc not supported!");
346     }
347 
348     @Override
openRootDocument(@ullable DocumentInfo rootDoc)349     public void openRootDocument(@Nullable DocumentInfo rootDoc) {
350         if (rootDoc == null) {
351             // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root
352             // document. Either case we should call refreshCurrentRootAndDirectory() to let
353             // DirectoryFragment update UI.
354             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
355         } else {
356             openContainerDocument(rootDoc);
357         }
358     }
359 
360     @Override
openContainerDocument(DocumentInfo doc)361     public void openContainerDocument(DocumentInfo doc) {
362         assert(doc.isContainer());
363 
364         if (mSearchMgr.isSearching()) {
365             loadDocument(
366                     doc.derivedUri,
367                     doc.userId,
368                     (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc));
369         } else {
370             openChildContainer(doc);
371         }
372     }
373 
374     // TODO: Make this private and make tests call interface method instead.
375     /**
376      * Behavior when a document is opened.
377      */
378     @VisibleForTesting
onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback, boolean fromPicker)379     public void onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback,
380             boolean fromPicker) {
381         // In picker mode, don't access archive container to avoid pick file in archive files.
382         if (doc.isContainer() && !fromPicker) {
383             openContainerDocument(doc);
384             return;
385         }
386 
387         if (manageDocument(doc)) {
388             return;
389         }
390 
391         // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow
392         // PackageManager to install it.  This allows users to install APKs from any root.
393         // The Downloads special case is handled above in #manageDocument.
394         if (MimeTypes.isApkType(doc.mimeType)) {
395             viewDocument(doc);
396             return;
397         }
398 
399         switch (type) {
400             case VIEW_TYPE_REGULAR:
401                 if (viewDocument(doc)) {
402                     return;
403                 }
404                 break;
405 
406             case VIEW_TYPE_PREVIEW:
407                 if (previewDocument(doc, fromPicker)) {
408                     return;
409                 }
410                 break;
411 
412             default:
413                 throw new IllegalArgumentException("Illegal view type.");
414         }
415 
416         switch (fallback) {
417             case VIEW_TYPE_REGULAR:
418                 if (viewDocument(doc)) {
419                     return;
420                 }
421                 break;
422 
423             case VIEW_TYPE_PREVIEW:
424                 if (previewDocument(doc, fromPicker)) {
425                     return;
426                 }
427                 break;
428 
429             case VIEW_TYPE_NONE:
430                 break;
431 
432             default:
433                 throw new IllegalArgumentException("Illegal fallback view type.");
434         }
435 
436         // Failed to view including fallback, and it's in an archive.
437         if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) {
438             mDialogs.showViewInArchivesUnsupported();
439         }
440     }
441 
viewDocument(DocumentInfo doc)442     private boolean viewDocument(DocumentInfo doc) {
443         if (doc.isPartial()) {
444             Log.w(TAG, "Can't view partial file.");
445             return false;
446         }
447 
448         if (doc.isInArchive()) {
449             Log.w(TAG, "Can't view files in archives.");
450             return false;
451         }
452 
453         if (doc.isDirectory()) {
454             Log.w(TAG, "Can't view directories.");
455             return true;
456         }
457 
458         Intent intent = buildViewIntent(doc);
459         if (DEBUG && intent.getClipData() != null) {
460             Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
461         }
462 
463         try {
464             doc.userId.startActivityAsUser(mActivity, intent);
465             return true;
466         } catch (ActivityNotFoundException e) {
467             mDialogs.showNoApplicationFound();
468         }
469         return false;
470     }
471 
previewDocument(DocumentInfo doc, boolean fromPicker)472     private boolean previewDocument(DocumentInfo doc, boolean fromPicker) {
473         if (doc.isPartial()) {
474             Log.w(TAG, "Can't view partial file.");
475             return false;
476         }
477 
478         Intent intent = new QuickViewIntentBuilder(
479                 mActivity,
480                 mActivity.getResources(),
481                 doc,
482                 mModel,
483                 fromPicker).build();
484 
485         if (intent != null) {
486             // TODO: un-work around issue b/24963914. Should be fixed soon.
487             try {
488                 doc.userId.startActivityAsUser(mActivity, intent);
489                 return true;
490             } catch (SecurityException e) {
491                 // Carry on to regular view mode.
492                 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
493             }
494         }
495 
496         return false;
497     }
498 
499 
manageDocument(DocumentInfo doc)500     protected boolean manageDocument(DocumentInfo doc) {
501         if (isManagedDownload(doc)) {
502             // First try managing the document; we expect manager to filter
503             // based on authority, so we don't grant.
504             Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
505             manage.setData(doc.getDocumentUri());
506             try {
507                 doc.userId.startActivityAsUser(mActivity, manage);
508                 return true;
509             } catch (ActivityNotFoundException ex) {
510                 // Fall back to regular handling.
511             }
512         }
513 
514         return false;
515     }
516 
isManagedDownload(DocumentInfo doc)517     private boolean isManagedDownload(DocumentInfo doc) {
518         // Anything on downloads goes through the back through downloads manager
519         // (that's the MANAGE_DOCUMENT bit).
520         // This is done for two reasons:
521         // 1) The file in question might be a failed/queued or otherwise have some
522         //    specialized download handling.
523         // 2) For APKs, the download manager will add on some important security stuff
524         //    like origin URL.
525         // 3) For partial files, the download manager will offer to restart/retry downloads.
526 
527         // All other files not on downloads, event APKs, would get no benefit from this
528         // treatment, thusly the "isDownloads" check.
529 
530         // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
531         // files in archives or in child folders. Also, if the activity is already browsing
532         // a ZIP from downloads, then skip MANAGE_DOCUMENTS.
533         if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
534                 && mState.stack.size() > 1) {
535             // viewing the contents of an archive.
536             return false;
537         }
538 
539         // management is only supported in Downloads root or downloaded files show in Recent root.
540         if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) {
541             // only on APKs or partial files.
542             return MimeTypes.isApkType(doc.mimeType) || doc.isPartial();
543         }
544 
545         return false;
546     }
547 
buildViewIntent(DocumentInfo doc)548     protected Intent buildViewIntent(DocumentInfo doc) {
549         Intent intent = new Intent(Intent.ACTION_VIEW);
550         intent.setDataAndType(doc.getDocumentUri(), doc.mimeType);
551 
552         // Downloads has traditionally added the WRITE permission
553         // in the TrampolineActivity. Since this behavior is long
554         // established, we set the same permission for non-managed files
555         // This ensures consistent behavior between the Downloads root
556         // and other roots.
557         int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP;
558         if (doc.isWriteSupported()) {
559             flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
560         }
561         intent.setFlags(flags);
562 
563         return intent;
564     }
565 
566     @Override
previewItem(ItemDetails<String> doc)567     public boolean previewItem(ItemDetails<String> doc) {
568         throw new UnsupportedOperationException("Can't handle preview.");
569     }
570 
openFolderInSearchResult(@ullable DocumentStack stack, DocumentInfo doc)571     private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) {
572         if (stack == null) {
573             mState.stack.popToRootDocument();
574 
575             // Update navigator to give horizontal breadcrumb a chance to update documents. It
576             // doesn't update its content if the size of document stack doesn't change.
577             // TODO: update breadcrumb to take range update.
578             mActivity.updateNavigator();
579 
580             mState.stack.push(doc);
581         } else {
582             if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) {
583                 // It is now possible when opening cross-profile folder.
584                 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected "
585                         + mState.stack.getRoot());
586             }
587 
588             final DocumentInfo top = stack.peek();
589             if (top.isArchive()) {
590                 // Swap the zip file in original provider and the one provided by ArchiveProvider.
591                 stack.pop();
592                 stack.push(mDocs.getArchiveDocument(top.derivedUri, top.userId));
593             }
594 
595             mState.stack.reset();
596             // Update navigator to give horizontal breadcrumb a chance to update documents. It
597             // doesn't update its content if the size of document stack doesn't change.
598             // TODO: update breadcrumb to take range update.
599             mActivity.updateNavigator();
600 
601             mState.stack.reset(stack);
602         }
603 
604         // Show an opening animation only if pressing "back" would get us back to the
605         // previous directory. Especially after opening a root document, pressing
606         // back, wouldn't go to the previous root, but close the activity.
607         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
608                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
609         mActivity.refreshCurrentRootAndDirectory(anim);
610     }
611 
openChildContainer(DocumentInfo doc)612     private void openChildContainer(DocumentInfo doc) {
613         DocumentInfo currentDoc = null;
614 
615         if (doc.isDirectory()) {
616             // Regular directory.
617             currentDoc = doc;
618         } else if (doc.isArchive()) {
619             // Archive.
620             currentDoc = mDocs.getArchiveDocument(doc.derivedUri, doc.userId);
621         }
622 
623         assert(currentDoc != null);
624         if (currentDoc.equals(mState.stack.peek())) {
625             Log.w(TAG, "This DocumentInfo is already in current DocumentsStack");
626             return;
627         }
628 
629         mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);
630 
631         mState.stack.push(currentDoc);
632         // Show an opening animation only if pressing "back" would get us back to the
633         // previous directory. Especially after opening a root document, pressing
634         // back, wouldn't go to the previous root, but close the activity.
635         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
636                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
637         mActivity.refreshCurrentRootAndDirectory(anim);
638     }
639 
640     @Override
setDebugMode(boolean enabled)641     public void setDebugMode(boolean enabled) {
642         if (!mInjector.features.isDebugSupportEnabled()) {
643             return;
644         }
645 
646         mState.debugMode = enabled;
647         mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled);
648         mInjector.features.forceFeature(R.bool.feature_inspector, enabled);
649         mActivity.invalidateOptionsMenu();
650 
651         if (enabled) {
652             showDebugMessage();
653         } else {
654             mActivity.getWindow().setStatusBarColor(
655                     mActivity.getResources().getColor(R.color.app_background_color));
656         }
657     }
658 
659     @Override
showDebugMessage()660     public void showDebugMessage() {
661         assert (mInjector.features.isDebugSupportEnabled());
662 
663         int[] colors = mInjector.debugHelper.getNextColors();
664         Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage();
665 
666         Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second);
667 
668         mActivity.getWindow().setStatusBarColor(colors[1]);
669     }
670 
671     @Override
switchLauncherIcon()672     public void switchLauncherIcon() {
673         PackageManager pm = mActivity.getPackageManager();
674         if (pm != null) {
675             final boolean enalbled = Shared.isLauncherEnabled(mActivity);
676             ComponentName component = new ComponentName(
677                     mActivity.getPackageName(), Shared.LAUNCHER_TARGET_CLASS);
678             pm.setComponentEnabledSetting(component, enalbled
679                     ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
680                     : PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
681                     PackageManager.DONT_KILL_APP);
682         }
683     }
684 
685     @Override
cutToClipboard()686     public void cutToClipboard() {
687         throw new UnsupportedOperationException("Cut not supported!");
688     }
689 
690     @Override
copyToClipboard()691     public void copyToClipboard() {
692         throw new UnsupportedOperationException("Copy not supported!");
693     }
694 
695     @Override
showDeleteDialog()696     public void showDeleteDialog() {
697         throw new UnsupportedOperationException("Delete not supported!");
698     }
699 
700     @Override
deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent)701     public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) {
702         throw new UnsupportedOperationException("Delete not supported!");
703     }
704 
705     @Override
shareSelectedDocuments()706     public void shareSelectedDocuments() {
707         throw new UnsupportedOperationException("Share not supported!");
708     }
709 
loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback)710     protected final void loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback) {
711         new LoadDocStackTask(
712                 mActivity,
713                 mProviders,
714                 mDocs,
715                 userId,
716                 callback
717                 ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri);
718     }
719 
720     @Override
loadRoot(Uri uri, UserId userId)721     public final void loadRoot(Uri uri, UserId userId) {
722         new LoadRootTask<>(mActivity, mProviders, uri, userId, this::onRootLoaded)
723                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
724     }
725 
726     @Override
loadCrossProfileRoot(RootInfo info, UserId selectedUser)727     public final void loadCrossProfileRoot(RootInfo info, UserId selectedUser) {
728         if (info.isRecents()) {
729             openRoot(mProviders.getRecentsRoot(selectedUser));
730             return;
731         }
732         new LoadRootTask<>(mActivity, mProviders, info.getUri(), selectedUser,
733                 new LoadCrossProfileRootCallback(info, selectedUser))
734                 .executeOnExecutor(mExecutors.lookup(info.getUri().getAuthority()));
735     }
736 
737     private class LoadCrossProfileRootCallback implements LoadRootTask.LoadRootCallback {
738         private final RootInfo mOriginalRoot;
739         private final UserId mSelectedUserId;
740 
LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser)741         LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser) {
742             mOriginalRoot = rootInfo;
743             mSelectedUserId = selectedUser;
744         }
745 
746         @Override
onRootLoaded(@ullable RootInfo root)747         public void onRootLoaded(@Nullable RootInfo root) {
748             if (root == null) {
749                 // There is no such root in the other profile. Maybe the provider is missing on
750                 // the other profile. Create a placeholder root and open it to show error message.
751                 root = RootInfo.copyRootInfo(mOriginalRoot);
752                 root.userId = mSelectedUserId;
753             }
754             openRoot(root);
755         }
756     }
757 
758     @Override
loadFirstRoot(Uri uri)759     public final void loadFirstRoot(Uri uri) {
760         new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded)
761                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
762     }
763 
764     @Override
loadDocumentsForCurrentStack()765     public void loadDocumentsForCurrentStack() {
766         // mState.stack may be empty when we cannot load the root document.
767         // However, we still want to restart loader because we may need to perform search in a
768         // cross-profile scenario.
769         // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op.
770         // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null.
771 
772         // Only allow restartLoader when the previous loader is finished or reset. Allowing
773         // multiple consecutive calls to restartLoader() / onCreateLoader() will probably create
774         // multiple active loaders, because restartLoader() does not interrupt previous loaders'
775         // loading, therefore may block the UI thread and cause ANR.
776         if (mLoaderSemaphore.tryAcquire()) {
777             mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings);
778         }
779     }
780 
launchToDocument(Uri uri)781     protected final boolean launchToDocument(Uri uri) {
782         // We don't support launching to a document in an archive.
783         if (!Providers.isArchiveUri(uri)) {
784             loadDocument(uri, UserId.DEFAULT_USER, this::onStackLoaded);
785             return true;
786         }
787 
788         return false;
789     }
790 
onStackLoaded(@ullable DocumentStack stack)791     private void onStackLoaded(@Nullable DocumentStack stack) {
792         if (stack != null) {
793             if (!stack.peek().isDirectory()) {
794                 // Requested document is not a directory. Pop it so that we can launch into its
795                 // parent.
796                 stack.pop();
797             }
798             mState.stack.reset(stack);
799             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
800 
801             Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri());
802         } else {
803             Log.w(TAG, "Failed to launch into the given uri. Launch to default location.");
804             launchToDefaultLocation();
805 
806             Metrics.logLaunchAtLocation(mState, null);
807         }
808     }
809 
onRootLoaded(@ullable RootInfo root)810     private void onRootLoaded(@Nullable RootInfo root) {
811         boolean invalidRootForAction =
812                 (root != null
813                 && !root.supportsChildren()
814                 && mState.action == State.ACTION_OPEN_TREE);
815 
816         if (invalidRootForAction) {
817             loadDeviceRoot();
818         } else if (root != null) {
819             mActivity.onRootPicked(root);
820         } else {
821             launchToDefaultLocation();
822         }
823     }
824 
launchToDefaultLocation()825     protected abstract void launchToDefaultLocation();
826 
restoreRootAndDirectory()827     protected void restoreRootAndDirectory() {
828         if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) {
829             mActivity.onRootPicked(mState.stack.getRoot());
830         } else {
831             mActivity.restoreRootAndDirectory();
832         }
833     }
834 
loadDeviceRoot()835     protected final void loadDeviceRoot() {
836         loadRoot(DocumentsContract.buildRootUri(Providers.AUTHORITY_STORAGE,
837                 Providers.ROOT_ID_DEVICE), UserId.DEFAULT_USER);
838     }
839 
loadHomeDir()840     protected final void loadHomeDir() {
841         loadRoot(Shared.getDefaultRootUri(mActivity), UserId.DEFAULT_USER);
842     }
843 
loadRecent()844     protected final void loadRecent() {
845         mState.stack.changeRoot(mProviders.getRecentsRoot(UserId.DEFAULT_USER));
846         mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
847     }
848 
getStableSelection()849     protected MutableSelection<String> getStableSelection() {
850         MutableSelection<String> selection = new MutableSelection<>();
851         mSelectionMgr.copySelection(selection);
852         return selection;
853     }
854 
855     @Override
reset(ContentLock reloadLock)856     public ActionHandler reset(ContentLock reloadLock) {
857         mContentLock = reloadLock;
858         mActivity.getLoaderManager().destroyLoader(LOADER_ID);
859         return this;
860     }
861 
862     private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
863 
864         @Override
onCreateLoader(int id, Bundle args)865         public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
866             Context context = mActivity;
867 
868             if (mState.stack.isRecents()) {
869                 final LockingContentObserver observer = new LockingContentObserver(
870                         mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack);
871                 MultiRootDocumentsLoader loader;
872 
873                 if (mSearchMgr.isSearching()) {
874                     if (DEBUG) {
875                         Log.d(TAG, "Creating new GlobalSearchLoader.");
876                     }
877                     loader = new GlobalSearchLoader(
878                             context,
879                             mProviders,
880                             mState,
881                             mExecutors,
882                             mInjector.fileTypeLookup,
883                             mSearchMgr.buildQueryArgs(),
884                             mState.stack.getRoot().userId);
885                 } else {
886                     if (DEBUG) {
887                         Log.d(TAG, "Creating new loader recents.");
888                     }
889                     loader = new RecentsLoader(
890                             context,
891                             mProviders,
892                             mState,
893                             mExecutors,
894                             mInjector.fileTypeLookup,
895                             mState.stack.getRoot().userId);
896                 }
897                 loader.setObserver(observer);
898                 return loader;
899             } else {
900                 // There maybe no root docInfo
901                 DocumentInfo rootDoc = mState.stack.peek();
902 
903                 String authority = rootDoc == null
904                         ? mState.stack.getRoot().authority
905                         : rootDoc.authority;
906                 String documentId = rootDoc == null
907                         ? mState.stack.getRoot().documentId
908                         : rootDoc.documentId;
909 
910                 Uri contentsUri = mSearchMgr.isSearching()
911                         ? DocumentsContract.buildSearchDocumentsUri(
912                         mState.stack.getRoot().authority,
913                         mState.stack.getRoot().rootId,
914                         mSearchMgr.getCurrentSearch())
915                         : DocumentsContract.buildChildDocumentsUri(
916                                 authority,
917                                 documentId);
918 
919                 final Bundle queryArgs = mSearchMgr.isSearching()
920                         ? mSearchMgr.buildQueryArgs()
921                         : null;
922 
923                 if (mInjector.config.managedModeEnabled(mState.stack)) {
924                     contentsUri = DocumentsContract.setManageMode(contentsUri);
925                 }
926 
927                 if (DEBUG) {
928                     Log.d(TAG,
929                         "Creating new directory loader for: "
930                             + DocumentInfo.debugString(mState.stack.peek()));
931                 }
932 
933                 return new DirectoryLoader(
934                         mInjector.features,
935                         context,
936                         mState,
937                         contentsUri,
938                         mInjector.fileTypeLookup,
939                         mContentLock,
940                         queryArgs);
941             }
942         }
943 
944         @Override
onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)945         public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
946             if (DEBUG) {
947                 Log.d(TAG, "Loader has finished for: "
948                     + DocumentInfo.debugString(mState.stack.peek()));
949             }
950             assert(result != null);
951 
952             mInjector.getModel().update(result);
953             mLoaderSemaphore.release();
954         }
955 
956         @Override
onLoaderReset(Loader<DirectoryResult> loader)957         public void onLoaderReset(Loader<DirectoryResult> loader) {
958             mLoaderSemaphore.release();
959         }
960     }
961     /**
962      * A class primarily for the support of isolating our tests
963      * from our concrete activity implementations.
964      */
965     public interface CommonAddons {
restoreRootAndDirectory()966         void restoreRootAndDirectory();
refreshCurrentRootAndDirectory(@nimationType int anim)967         void refreshCurrentRootAndDirectory(@AnimationType int anim);
onRootPicked(RootInfo root)968         void onRootPicked(RootInfo root);
969         // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
onDocumentsPicked(List<DocumentInfo> docs)970         void onDocumentsPicked(List<DocumentInfo> docs);
onDocumentPicked(DocumentInfo doc)971         void onDocumentPicked(DocumentInfo doc);
getCurrentRoot()972         RootInfo getCurrentRoot();
getCurrentDirectory()973         DocumentInfo getCurrentDirectory();
getSelectedUser()974         UserId getSelectedUser();
975         /**
976          * Check whether current directory is root of recent.
977          */
isInRecents()978         boolean isInRecents();
setRootsDrawerOpen(boolean open)979         void setRootsDrawerOpen(boolean open);
980 
981         /**
982          * Set the locked status of the DrawerController.
983          */
setRootsDrawerLocked(boolean locked)984         void setRootsDrawerLocked(boolean locked);
985 
986         // TODO: Let navigator listens to State
updateNavigator()987         void updateNavigator();
988 
989         @VisibleForTesting
notifyDirectoryNavigated(Uri docUri)990         void notifyDirectoryNavigated(Uri docUri);
991     }
992 }
993