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 
ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, ActionModeAddons actionModeAddons, DocumentClipper clipper, ClipStore clipStore, Injector injector)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
dropOn(DragEvent event, RootInfo root)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 
dropOnCallback( ClipData clipData, Object localState, DocumentInfo rootDoc, RootInfo root)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
openSelectedInNewWindow()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
openSettings(RootInfo root)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
pasteIntoFolder(RootInfo root)159     public void pasteIntoFolder(RootInfo root) {
160         this.getRootDocument(
161                 root,
162                 TimeoutTask.DEFAULT_TIMEOUT,
163                 (DocumentInfo doc) -> pasteIntoFolder(root, doc));
164     }
165 
pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc)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
renameDocument(String name, DocumentInfo document)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
openRoot(RootInfo root)191     public void openRoot(RootInfo root) {
192         Metrics.logRootVisited(mActivity, Metrics.FILES_SCOPE, root);
193         mActivity.onRootPicked(root);
194     }
195 
196     @Override
openDocument(DocumentDetails details, @ViewType int type, @ViewType int fallback)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
openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback)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
springOpenDirectory(DocumentInfo doc)221     public void springOpenDirectory(DocumentInfo doc) {
222         assert(doc.isDirectory());
223         mActionModeAddons.finishActionMode();
224         openContainerDocument(doc);
225     }
226 
getSelectedOrFocused()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
cutToClipboard()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
copyToClipboard()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
viewInOwner()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
deleteSelectedDocuments()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
shareSelectedDocuments()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
initLocation(Intent intent)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
launchToDefaultLocation()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.
launchToStackLocation(Intent intent)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 
launchToRoot(Intent intent)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 
launchToDocument(Intent intent)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
showChooserForDoc(DocumentInfo doc)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 
onDocumentPicked(DocumentInfo doc, @ViewType int type, @ViewType int fallback)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 
viewDocument(DocumentInfo doc)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 
previewDocument(DocumentInfo doc)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 
manageDocument(DocumentInfo doc)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 
isManagedDownload(DocumentInfo doc)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 
buildViewIntent(DocumentInfo doc)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