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