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