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