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