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