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