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