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