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.files;
18 
19 import static android.content.ContentResolver.wrap;
20 
21 import static com.android.documentsui.base.SharedMinimal.DEBUG;
22 
23 import android.app.DownloadManager;
24 import android.content.ActivityNotFoundException;
25 import android.content.ClipData;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.os.FileUtils;
31 import android.provider.DocumentsContract;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.DragEvent;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.fragment.app.FragmentActivity;
38 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
39 import androidx.recyclerview.selection.MutableSelection;
40 import androidx.recyclerview.selection.Selection;
41 
42 import com.android.documentsui.AbstractActionHandler;
43 import com.android.documentsui.ActionModeAddons;
44 import com.android.documentsui.ActivityConfig;
45 import com.android.documentsui.DocumentsAccess;
46 import com.android.documentsui.DocumentsApplication;
47 import com.android.documentsui.DragAndDropManager;
48 import com.android.documentsui.Injector;
49 import com.android.documentsui.MetricConsts;
50 import com.android.documentsui.Metrics;
51 import com.android.documentsui.Model;
52 import com.android.documentsui.R;
53 import com.android.documentsui.TimeoutTask;
54 import com.android.documentsui.base.ConfirmationCallback;
55 import com.android.documentsui.base.DebugFlags;
56 import com.android.documentsui.base.DocumentFilters;
57 import com.android.documentsui.base.DocumentInfo;
58 import com.android.documentsui.base.DocumentStack;
59 import com.android.documentsui.base.Features;
60 import com.android.documentsui.base.Lookup;
61 import com.android.documentsui.base.MimeTypes;
62 import com.android.documentsui.base.Providers;
63 import com.android.documentsui.base.RootInfo;
64 import com.android.documentsui.base.Shared;
65 import com.android.documentsui.base.State;
66 import com.android.documentsui.clipping.ClipStore;
67 import com.android.documentsui.clipping.DocumentClipper;
68 import com.android.documentsui.clipping.UrisSupplier;
69 import com.android.documentsui.dirlist.AnimationView;
70 import com.android.documentsui.files.ActionHandler.Addons;
71 import com.android.documentsui.inspector.InspectorActivity;
72 import com.android.documentsui.queries.SearchViewManager;
73 import com.android.documentsui.roots.ProvidersAccess;
74 import com.android.documentsui.services.FileOperation;
75 import com.android.documentsui.services.FileOperationService;
76 import com.android.documentsui.services.FileOperations;
77 import com.android.documentsui.ui.DialogController;
78 
79 import java.util.ArrayList;
80 import java.util.List;
81 import java.util.concurrent.Executor;
82 
83 import javax.annotation.Nullable;
84 
85 /**
86  * Provides {@link FilesActivity} action specializations to fragments.
87  */
88 public class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionHandler<T> {
89 
90     private static final String TAG = "ManagerActionHandler";
91 
92     private final ActionModeAddons mActionModeAddons;
93     private final Features mFeatures;
94     private final ActivityConfig mConfig;
95     private final DialogController mDialogs;
96     private final DocumentClipper mClipper;
97     private final ClipStore mClipStore;
98     private final DragAndDropManager mDragAndDropManager;
99     private final Model mModel;
100 
ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, ActionModeAddons actionModeAddons, DocumentClipper clipper, ClipStore clipStore, DragAndDropManager dragAndDropManager, Injector injector)101     ActionHandler(
102             T activity,
103             State state,
104             ProvidersAccess providers,
105             DocumentsAccess docs,
106             SearchViewManager searchMgr,
107             Lookup<String, Executor> executors,
108             ActionModeAddons actionModeAddons,
109             DocumentClipper clipper,
110             ClipStore clipStore,
111             DragAndDropManager dragAndDropManager,
112             Injector injector) {
113 
114         super(activity, state, providers, docs, searchMgr, executors, injector);
115 
116         mActionModeAddons = actionModeAddons;
117         mFeatures = injector.features;
118         mConfig = injector.config;
119         mDialogs = injector.dialogs;
120         mClipper = clipper;
121         mClipStore = clipStore;
122         mDragAndDropManager = dragAndDropManager;
123         mModel = injector.getModel();
124     }
125 
126     @Override
dropOn(DragEvent event, RootInfo root)127     public boolean dropOn(DragEvent event, RootInfo root) {
128         if (!root.supportsCreate() || root.isLibrary()) {
129             return false;
130         }
131 
132         // DragEvent gets recycled, so it is possible that by the time the callback is called,
133         // event.getLocalState() and event.getClipData() returns null. Thus, we want to save
134         // references to ensure they are non null.
135         final ClipData clipData = event.getClipData();
136         final Object localState = event.getLocalState();
137 
138         return mDragAndDropManager.drop(
139                 clipData, localState, root, this, mDialogs::showFileOperationStatus);
140     }
141 
142     @Override
openSelectedInNewWindow()143     public void openSelectedInNewWindow() {
144         Selection<String> selection = getStableSelection();
145         assert(selection.size() == 1);
146         DocumentInfo doc = mModel.getDocument(selection.iterator().next());
147         assert(doc != null);
148         openInNewWindow(new DocumentStack(mState.stack, doc));
149     }
150 
151     @Override
openSettings(RootInfo root)152     public void openSettings(RootInfo root) {
153         Metrics.logUserAction(MetricConsts.USER_ACTION_SETTINGS);
154         final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
155         intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
156         mActivity.startActivity(intent);
157     }
158 
159     @Override
pasteIntoFolder(RootInfo root)160     public void pasteIntoFolder(RootInfo root) {
161         this.getRootDocument(
162                 root,
163                 TimeoutTask.DEFAULT_TIMEOUT,
164                 (DocumentInfo doc) -> pasteIntoFolder(root, doc));
165     }
166 
pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc)167     private void pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc) {
168         DocumentStack stack = new DocumentStack(root, doc);
169         mClipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
170     }
171 
172     @Override
renameDocument(String name, DocumentInfo document)173     public @Nullable DocumentInfo renameDocument(String name, DocumentInfo document) {
174         ContentResolver resolver = mActivity.getContentResolver();
175         ContentProviderClient client = null;
176 
177         try {
178             client = DocumentsApplication.acquireUnstableProviderOrThrow(
179                     resolver, document.derivedUri.getAuthority());
180             Uri newUri = DocumentsContract.renameDocument(
181                     wrap(client), document.derivedUri, name);
182             return DocumentInfo.fromUri(resolver, newUri);
183         } catch (Exception e) {
184             Log.w(TAG, "Failed to rename file", e);
185             return null;
186         } finally {
187             FileUtils.closeQuietly(client);
188         }
189     }
190 
191     @Override
openRoot(RootInfo root)192     public void openRoot(RootInfo root) {
193         Metrics.logRootVisited(MetricConsts.FILES_SCOPE, root);
194         mActivity.onRootPicked(root);
195     }
196 
197     @Override
openItem(ItemDetails<String> details, @ViewType int type, @ViewType int fallback)198     public boolean openItem(ItemDetails<String> details, @ViewType int type,
199             @ViewType int fallback) {
200         DocumentInfo doc = mModel.getDocument(details.getSelectionKey());
201         if (doc == null) {
202             Log.w(TAG, "Can't view item. No Document available for modeId: "
203                     + details.getSelectionKey());
204             return false;
205         }
206         mInjector.searchManager.recordHistory();
207 
208         return openDocument(doc, type, fallback);
209     }
210 
211     // TODO: Make this private and make tests call openDocument(DocumentDetails, int, int) instead.
212     @VisibleForTesting
openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback)213     public boolean openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
214         if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
215             onDocumentPicked(doc, type, fallback);
216             mSelectionMgr.clearSelection();
217             return !doc.isContainer();
218         }
219         return false;
220     }
221 
222     @Override
springOpenDirectory(DocumentInfo doc)223     public void springOpenDirectory(DocumentInfo doc) {
224         assert(doc.isDirectory());
225         mActionModeAddons.finishActionMode();
226         openContainerDocument(doc);
227     }
228 
getSelectedOrFocused()229     private Selection<String> getSelectedOrFocused() {
230         final MutableSelection<String> selection = this.getStableSelection();
231         if (selection.isEmpty()) {
232             String focusModelId = mFocusHandler.getFocusModelId();
233             if (focusModelId != null) {
234                 selection.add(focusModelId);
235             }
236         }
237 
238         return selection;
239     }
240 
241     @Override
cutToClipboard()242     public void cutToClipboard() {
243         Metrics.logUserAction(MetricConsts.USER_ACTION_CUT_CLIPBOARD);
244         Selection<String> selection = getSelectedOrFocused();
245 
246         if (selection.isEmpty()) {
247             return;
248         }
249 
250         if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
251             mDialogs.showOperationUnsupported();
252             return;
253         }
254 
255         mSelectionMgr.clearSelection();
256 
257         mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
258 
259         mDialogs.showDocumentsClipped(selection.size());
260     }
261 
262     @Override
copyToClipboard()263     public void copyToClipboard() {
264         Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_CLIPBOARD);
265         Selection<String> selection = getSelectedOrFocused();
266 
267         if (selection.isEmpty()) {
268             return;
269         }
270         mSelectionMgr.clearSelection();
271 
272         mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
273 
274         mDialogs.showDocumentsClipped(selection.size());
275     }
276 
277     @Override
viewInOwner()278     public void viewInOwner() {
279         Metrics.logUserAction(MetricConsts.USER_ACTION_VIEW_IN_APPLICATION);
280         Selection<String> selection = getSelectedOrFocused();
281 
282         if (selection.isEmpty() || selection.size() > 1) {
283             return;
284         }
285         DocumentInfo doc = mModel.getDocument(selection.iterator().next());
286         Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
287         intent.setPackage(mProviders.getPackageName(doc.authority));
288         intent.addCategory(Intent.CATEGORY_DEFAULT);
289         intent.setData(doc.derivedUri);
290         try {
291             mActivity.startActivity(intent);
292         } catch (ActivityNotFoundException e) {
293             Log.e(TAG, "Failed to view settings in application for " + doc.derivedUri, e);
294             mDialogs.showNoApplicationFound();
295         }
296     }
297 
298 
299     @Override
deleteSelectedDocuments()300     public void deleteSelectedDocuments() {
301         Metrics.logUserAction(MetricConsts.USER_ACTION_DELETE);
302         Selection selection = getSelectedOrFocused();
303 
304         if (selection.isEmpty()) {
305             return;
306         }
307 
308         final @Nullable DocumentInfo srcParent = mState.stack.peek();
309 
310         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
311         List<DocumentInfo> docs = mModel.getDocuments(selection);
312 
313         ConfirmationCallback result = (@ConfirmationCallback.Result int code) -> {
314             // share the news with our caller, be it good or bad.
315             mActionModeAddons.finishOnConfirmed(code);
316 
317             if (code != ConfirmationCallback.CONFIRM) {
318                 return;
319             }
320 
321             UrisSupplier srcs;
322             try {
323                 srcs = UrisSupplier.create(
324                         selection,
325                         mModel::getItemUri,
326                         mClipStore);
327             } catch (Exception e) {
328                 Log.e(TAG,"Failed to delete a file because we were unable to get item URIs.", e);
329                 mDialogs.showFileOperationStatus(
330                         FileOperations.Callback.STATUS_FAILED,
331                         FileOperationService.OPERATION_DELETE,
332                         selection.size());
333                 return;
334             }
335 
336             FileOperation operation = new FileOperation.Builder()
337                     .withOpType(FileOperationService.OPERATION_DELETE)
338                     .withDestination(mState.stack)
339                     .withSrcs(srcs)
340                     .withSrcParent(srcParent == null ? null : srcParent.derivedUri)
341                     .build();
342 
343             FileOperations.start(mActivity, operation, mDialogs::showFileOperationStatus,
344                     FileOperations.createJobId());
345         };
346 
347         mDialogs.confirmDelete(docs, result);
348     }
349 
350     @Override
shareSelectedDocuments()351     public void shareSelectedDocuments() {
352         Metrics.logUserAction(MetricConsts.USER_ACTION_SHARE);
353 
354         Selection<String> selection = getStableSelection();
355 
356         assert(!selection.isEmpty());
357 
358         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
359         List<DocumentInfo> docs = mModel.loadDocuments(
360                 selection, DocumentFilters.sharable(mFeatures));
361 
362         Intent intent;
363 
364         if (docs.size() == 1) {
365             intent = new Intent(Intent.ACTION_SEND);
366             DocumentInfo doc = docs.get(0);
367             intent.setType(doc.mimeType);
368             intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
369 
370         } else if (docs.size() > 1) {
371             intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
372 
373             final ArrayList<String> mimeTypes = new ArrayList<>();
374             final ArrayList<Uri> uris = new ArrayList<>();
375             for (DocumentInfo doc : docs) {
376                 mimeTypes.add(doc.mimeType);
377                 uris.add(doc.derivedUri);
378             }
379 
380             intent.setType(MimeTypes.findCommonMimeType(mimeTypes));
381             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
382 
383         } else {
384             // Everything filtered out, nothing to share.
385             return;
386         }
387 
388         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
389         intent.addCategory(Intent.CATEGORY_DEFAULT);
390 
391         if (mFeatures.isVirtualFilesSharingEnabled()
392                 && mModel.hasDocuments(selection, DocumentFilters.VIRTUAL)) {
393             intent.addCategory(Intent.CATEGORY_TYPED_OPENABLE);
394         }
395 
396         Intent chooserIntent = Intent.createChooser(
397                 intent, mActivity.getResources().getText(R.string.share_via));
398 
399         mActivity.startActivity(chooserIntent);
400     }
401 
402     @Override
loadDocumentsForCurrentStack()403     public void loadDocumentsForCurrentStack() {
404         super.loadDocumentsForCurrentStack();
405     }
406 
407     @Override
initLocation(Intent intent)408     public void initLocation(Intent intent) {
409         assert(intent != null);
410 
411         // stack is initialized if it's restored from bundle, which means we're restoring a
412         // previously stored state.
413         if (mState.stack.isInitialized()) {
414             if (DEBUG) {
415                 Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
416             }
417             restoreRootAndDirectory();
418             return;
419         }
420 
421         if (launchToStackLocation(intent)) {
422             if (DEBUG) {
423                 Log.d(TAG, "Launched to location from stack.");
424             }
425             return;
426         }
427 
428         if (launchToRoot(intent)) {
429             if (DEBUG) {
430                 Log.d(TAG, "Launched to root for browsing.");
431             }
432             return;
433         }
434 
435         if (launchToDocument(intent)) {
436             if (DEBUG) {
437                 Log.d(TAG, "Launched to a document.");
438             }
439             return;
440         }
441 
442         if (launchToDownloads(intent)) {
443             if (DEBUG) {
444                 Log.d(TAG, "Launched to a downloads.");
445             }
446             return;
447         }
448 
449         if (DEBUG) {
450             Log.d(TAG, "Launching directly into Home directory.");
451         }
452         launchToDefaultLocation();
453     }
454 
455     @Override
launchToDefaultLocation()456     protected void launchToDefaultLocation() {
457         if (mFeatures.isDefaultRootInBrowseEnabled()) {
458             loadHomeDir();
459         } else {
460             loadRecent();
461         }
462     }
463 
464     // If EXTRA_STACK is not null in intent, we'll skip other means of loading
465     // or restoring the stack (like URI).
466     //
467     // When restoring from a stack, if a URI is present, it should only ever be:
468     // -- a launch URI: Launch URIs support sensible activity management,
469     //    but don't specify a real content target)
470     // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
471     //
472     // Any other URI is *sorta* unexpected...except when browsing an archive
473     // in downloads.
launchToStackLocation(Intent intent)474     private boolean launchToStackLocation(Intent intent) {
475         DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
476         if (stack == null || stack.getRoot() == null) {
477             return false;
478         }
479 
480         mState.stack.reset(stack);
481         if (mState.stack.isEmpty()) {
482             mActivity.onRootPicked(mState.stack.getRoot());
483         } else {
484             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
485         }
486 
487         return true;
488     }
489 
launchToRoot(Intent intent)490     private boolean launchToRoot(Intent intent) {
491         String action = intent.getAction();
492         if (Intent.ACTION_VIEW.equals(action)) {
493             Uri uri = intent.getData();
494             if (DocumentsContract.isRootUri(mActivity, uri)) {
495                 if (DEBUG) {
496                     Log.d(TAG, "Launching with root URI.");
497                 }
498                 // If we've got a specific root to display, restore that root using a dedicated
499                 // authority. That way a misbehaving provider won't result in an ANR.
500                 loadRoot(uri);
501                 return true;
502             } else if (DocumentsContract.isRootsUri(mActivity, uri)) {
503                 if (DEBUG) {
504                     Log.d(TAG, "Launching first root with roots URI.");
505                 }
506                 // TODO: b/116760996 Let the user can disambiguate between roots if there are
507                 // multiple from DocumentsProvider instead of launching the first root in default
508                 loadFirstRoot(uri);
509                 return true;
510             }
511         }
512         return false;
513     }
514 
launchToDocument(Intent intent)515     private boolean launchToDocument(Intent intent) {
516         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
517             Uri uri = intent.getData();
518             if (DocumentsContract.isDocumentUri(mActivity, uri)) {
519                 return launchToDocument(intent.getData());
520             }
521         }
522 
523         return false;
524     }
525 
launchToDownloads(Intent intent)526     private boolean launchToDownloads(Intent intent) {
527         if (DownloadManager.ACTION_VIEW_DOWNLOADS.equals(intent.getAction())) {
528             Uri uri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
529                     Providers.ROOT_ID_DOWNLOADS);
530             loadRoot(uri);
531             return true;
532         }
533 
534         return false;
535     }
536 
537     @Override
showChooserForDoc(DocumentInfo doc)538     public void showChooserForDoc(DocumentInfo doc) {
539         assert(!doc.isDirectory());
540 
541         if (manageDocument(doc)) {
542             Log.w(TAG, "Open with is not yet supported for managed doc.");
543             return;
544         }
545 
546         Intent intent = Intent.createChooser(buildViewIntent(doc), null);
547         intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
548         try {
549             mActivity.startActivity(intent);
550         } catch (ActivityNotFoundException e) {
551             mDialogs.showNoApplicationFound();
552         }
553     }
554 
onDocumentPicked(DocumentInfo doc, @ViewType int type, @ViewType int fallback)555     private void onDocumentPicked(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
556         if (doc.isContainer()) {
557             openContainerDocument(doc);
558             return;
559         }
560 
561         if (manageDocument(doc)) {
562             return;
563         }
564 
565         // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow
566         // PackageManager to install it.  This allows users to install APKs from any root.
567         // The Downloads special case is handled above in #manageDocument.
568         if (MimeTypes.isApkType(doc.mimeType)) {
569             viewDocument(doc);
570             return;
571         }
572 
573         switch (type) {
574           case VIEW_TYPE_REGULAR:
575             if (viewDocument(doc)) {
576                 return;
577             }
578             break;
579 
580           case VIEW_TYPE_PREVIEW:
581             if (previewDocument(doc)) {
582                 return;
583             }
584             break;
585 
586           default:
587             throw new IllegalArgumentException("Illegal view type.");
588         }
589 
590         switch (fallback) {
591           case VIEW_TYPE_REGULAR:
592             if (viewDocument(doc)) {
593                 return;
594             }
595             break;
596 
597           case VIEW_TYPE_PREVIEW:
598             if (previewDocument(doc)) {
599                 return;
600             }
601             break;
602 
603           case VIEW_TYPE_NONE:
604             break;
605 
606           default:
607             throw new IllegalArgumentException("Illegal fallback view type.");
608         }
609 
610         // Failed to view including fallback, and it's in an archive.
611         if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) {
612             mDialogs.showViewInArchivesUnsupported();
613         }
614     }
615 
viewDocument(DocumentInfo doc)616     private boolean viewDocument(DocumentInfo doc) {
617         if (doc.isPartial()) {
618             Log.w(TAG, "Can't view partial file.");
619             return false;
620         }
621 
622         if (doc.isInArchive()) {
623             Log.w(TAG, "Can't view files in archives.");
624             return false;
625         }
626 
627         if (doc.isDirectory()) {
628             Log.w(TAG, "Can't view directories.");
629             return true;
630         }
631 
632         Intent intent = buildViewIntent(doc);
633         if (DEBUG && intent.getClipData() != null) {
634             Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
635         }
636 
637         try {
638             mActivity.startActivity(intent);
639             return true;
640         } catch (ActivityNotFoundException e) {
641             mDialogs.showNoApplicationFound();
642         }
643         return false;
644     }
645 
previewDocument(DocumentInfo doc)646     private boolean previewDocument(DocumentInfo doc) {
647         if (doc.isPartial()) {
648             Log.w(TAG, "Can't view partial file.");
649             return false;
650         }
651 
652         Intent intent = new QuickViewIntentBuilder(
653                 mActivity.getPackageManager(),
654                 mActivity.getResources(),
655                 doc,
656                 mModel,
657                 false /* fromPicker */).build();
658 
659         if (intent != null) {
660             // TODO: un-work around issue b/24963914. Should be fixed soon.
661             try {
662                 mActivity.startActivity(intent);
663                 return true;
664             } catch (SecurityException e) {
665                 // Carry on to regular view mode.
666                 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
667             }
668         }
669 
670         return false;
671     }
672 
manageDocument(DocumentInfo doc)673     private boolean manageDocument(DocumentInfo doc) {
674         if (isManagedDownload(doc)) {
675             // First try managing the document; we expect manager to filter
676             // based on authority, so we don't grant.
677             Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
678             manage.setData(doc.derivedUri);
679             try {
680                 mActivity.startActivity(manage);
681                 return true;
682             } catch (ActivityNotFoundException ex) {
683                 // Fall back to regular handling.
684             }
685         }
686 
687         return false;
688     }
689 
isManagedDownload(DocumentInfo doc)690     private boolean isManagedDownload(DocumentInfo doc) {
691         // Anything on downloads goes through the back through downloads manager
692         // (that's the MANAGE_DOCUMENT bit).
693         // This is done for two reasons:
694         // 1) The file in question might be a failed/queued or otherwise have some
695         //    specialized download handling.
696         // 2) For APKs, the download manager will add on some important security stuff
697         //    like origin URL.
698         // 3) For partial files, the download manager will offer to restart/retry downloads.
699 
700         // All other files not on downloads, event APKs, would get no benefit from this
701         // treatment, thusly the "isDownloads" check.
702 
703         // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
704         // files in archives or in child folders. Also, if the activity is already browsing
705         // a ZIP from downloads, then skip MANAGE_DOCUMENTS.
706         if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
707                 && mState.stack.size() > 1) {
708             // viewing the contents of an archive.
709             return false;
710         }
711 
712         // management is only supported in Downloads root or downloaded files show in Recent root.
713         if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) {
714             // only on APKs or partial files.
715             return MimeTypes.isApkType(doc.mimeType) || doc.isPartial();
716         }
717 
718         return false;
719     }
720 
buildViewIntent(DocumentInfo doc)721     private Intent buildViewIntent(DocumentInfo doc) {
722         Intent intent = new Intent(Intent.ACTION_VIEW);
723         intent.setDataAndType(doc.derivedUri, doc.mimeType);
724 
725         // Downloads has traditionally added the WRITE permission
726         // in the TrampolineActivity. Since this behavior is long
727         // established, we set the same permission for non-managed files
728         // This ensures consistent behavior between the Downloads root
729         // and other roots.
730         int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
731         if (doc.isWriteSupported()) {
732             flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
733         }
734         intent.setFlags(flags);
735 
736         return intent;
737     }
738 
739     @Override
showInspector(DocumentInfo doc)740     public void showInspector(DocumentInfo doc) {
741         Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR);
742         Intent intent = new Intent(mActivity, InspectorActivity.class);
743         intent.setData(doc.derivedUri);
744 
745         // permit the display of debug info about the file.
746         intent.putExtra(
747                 Shared.EXTRA_SHOW_DEBUG,
748                 mFeatures.isDebugSupportEnabled() &&
749                         (DEBUG || DebugFlags.getDocumentDetailsEnabled()));
750 
751         // The "root document" (top level folder in a root) don't usually have a
752         // human friendly display name. That's because we've never shown the root
753         // folder's name to anyone.
754         // For that reason when the doc being inspected is the root folder,
755         // we override the displayName of the doc w/ the Root's name instead.
756         // The Root's name is shown to the user in the sidebar.
757         if (doc.isDirectory() && mState.stack.size() == 1 && mState.stack.get(0).equals(doc)) {
758             RootInfo root = mActivity.getCurrentRoot();
759             // Recents root title isn't defined, but inspector is disabled for recents root folder.
760             assert !TextUtils.isEmpty(root.title);
761             intent.putExtra(Intent.EXTRA_TITLE, root.title);
762         }
763         mActivity.startActivity(intent);
764     }
765 
766     public interface Addons extends CommonAddons {
767     }
768 }
769