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; 18 19 import static com.android.documentsui.base.DocumentInfo.getCursorInt; 20 import static com.android.documentsui.base.DocumentInfo.getCursorString; 21 import static com.android.documentsui.base.SharedMinimal.DEBUG; 22 23 import android.app.PendingIntent; 24 import android.content.ActivityNotFoundException; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentSender; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Parcelable; 35 import android.provider.DocumentsContract; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.DragEvent; 39 40 import androidx.annotation.VisibleForTesting; 41 import androidx.fragment.app.FragmentActivity; 42 import androidx.loader.app.LoaderManager.LoaderCallbacks; 43 import androidx.loader.content.Loader; 44 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 45 import androidx.recyclerview.selection.MutableSelection; 46 import androidx.recyclerview.selection.SelectionTracker; 47 48 import com.android.documentsui.AbstractActionHandler.CommonAddons; 49 import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; 50 import com.android.documentsui.base.BooleanConsumer; 51 import com.android.documentsui.base.DocumentInfo; 52 import com.android.documentsui.base.DocumentStack; 53 import com.android.documentsui.base.Lookup; 54 import com.android.documentsui.base.MimeTypes; 55 import com.android.documentsui.base.Providers; 56 import com.android.documentsui.base.RootInfo; 57 import com.android.documentsui.base.Shared; 58 import com.android.documentsui.base.State; 59 import com.android.documentsui.base.UserId; 60 import com.android.documentsui.dirlist.AnimationView; 61 import com.android.documentsui.dirlist.AnimationView.AnimationType; 62 import com.android.documentsui.dirlist.FocusHandler; 63 import com.android.documentsui.files.LauncherActivity; 64 import com.android.documentsui.files.QuickViewIntentBuilder; 65 import com.android.documentsui.queries.SearchViewManager; 66 import com.android.documentsui.roots.GetRootDocumentTask; 67 import com.android.documentsui.roots.LoadFirstRootTask; 68 import com.android.documentsui.roots.LoadRootTask; 69 import com.android.documentsui.roots.ProvidersAccess; 70 import com.android.documentsui.sidebar.EjectRootTask; 71 import com.android.documentsui.sorting.SortListFragment; 72 import com.android.documentsui.ui.DialogController; 73 import com.android.documentsui.ui.Snackbars; 74 75 import java.util.ArrayList; 76 import java.util.List; 77 import java.util.Objects; 78 import java.util.concurrent.Executor; 79 import java.util.concurrent.Semaphore; 80 import java.util.function.Consumer; 81 82 import javax.annotation.Nullable; 83 84 /** 85 * Provides support for specializing the actions (openDocument etc.) to the host activity. 86 */ 87 public abstract class AbstractActionHandler<T extends FragmentActivity & CommonAddons> 88 implements ActionHandler { 89 90 @VisibleForTesting 91 public static final int CODE_AUTHENTICATION = 43; 92 93 @VisibleForTesting 94 static final int LOADER_ID = 42; 95 96 private static final String TAG = "AbstractActionHandler"; 97 private static final int REFRESH_SPINNER_TIMEOUT = 500; 98 private final Semaphore mLoaderSemaphore = new Semaphore(1); 99 100 protected final T mActivity; 101 protected final State mState; 102 protected final ProvidersAccess mProviders; 103 protected final DocumentsAccess mDocs; 104 protected final FocusHandler mFocusHandler; 105 protected final SelectionTracker<String> mSelectionMgr; 106 protected final SearchViewManager mSearchMgr; 107 protected final Lookup<String, Executor> mExecutors; 108 protected final DialogController mDialogs; 109 protected final Model mModel; 110 protected final Injector<?> mInjector; 111 112 private final LoaderBindings mBindings; 113 114 private Runnable mDisplayStateChangedListener; 115 116 private ContentLock mContentLock; 117 118 @Override registerDisplayStateChangedListener(Runnable l)119 public void registerDisplayStateChangedListener(Runnable l) { 120 mDisplayStateChangedListener = l; 121 } 122 123 @Override unregisterDisplayStateChangedListener(Runnable l)124 public void unregisterDisplayStateChangedListener(Runnable l) { 125 if (mDisplayStateChangedListener == l) { 126 mDisplayStateChangedListener = null; 127 } 128 } 129 AbstractActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector<?> injector)130 public AbstractActionHandler( 131 T activity, 132 State state, 133 ProvidersAccess providers, 134 DocumentsAccess docs, 135 SearchViewManager searchMgr, 136 Lookup<String, Executor> executors, 137 Injector<?> injector) { 138 139 assert (activity != null); 140 assert (state != null); 141 assert (providers != null); 142 assert (searchMgr != null); 143 assert (docs != null); 144 assert (injector != null); 145 146 mActivity = activity; 147 mState = state; 148 mProviders = providers; 149 mDocs = docs; 150 mFocusHandler = injector.focusManager; 151 mSelectionMgr = injector.selectionMgr; 152 mSearchMgr = searchMgr; 153 mExecutors = executors; 154 mDialogs = injector.dialogs; 155 mModel = injector.getModel(); 156 mInjector = injector; 157 158 mBindings = new LoaderBindings(); 159 } 160 161 @Override ejectRoot(RootInfo root, BooleanConsumer listener)162 public void ejectRoot(RootInfo root, BooleanConsumer listener) { 163 new EjectRootTask( 164 mActivity.getContentResolver(), 165 root.authority, 166 root.rootId, 167 listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority)); 168 } 169 170 @Override startAuthentication(PendingIntent intent)171 public void startAuthentication(PendingIntent intent) { 172 try { 173 mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION, 174 null, 0, 0, 0); 175 } catch (IntentSender.SendIntentException cancelled) { 176 Log.d(TAG, "Authentication Pending Intent either canceled or ignored."); 177 } 178 } 179 180 @Override requestQuietModeDisabled(RootInfo info, UserId userId)181 public void requestQuietModeDisabled(RootInfo info, UserId userId) { 182 new RequestQuietModeDisabledTask(mActivity, userId).execute(); 183 } 184 185 @Override onActivityResult(int requestCode, int resultCode, Intent data)186 public void onActivityResult(int requestCode, int resultCode, Intent data) { 187 switch (requestCode) { 188 case CODE_AUTHENTICATION: 189 onAuthenticationResult(resultCode); 190 break; 191 } 192 } 193 onAuthenticationResult(int resultCode)194 private void onAuthenticationResult(int resultCode) { 195 if (resultCode == FragmentActivity.RESULT_OK) { 196 Log.v(TAG, "Authentication was successful. Refreshing directory now."); 197 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 198 } 199 } 200 201 @Override getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback)202 public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) { 203 GetRootDocumentTask task = new GetRootDocumentTask( 204 root, 205 mActivity, 206 timeout, 207 mDocs, 208 callback); 209 210 task.executeOnExecutor(mExecutors.lookup(root.authority)); 211 } 212 213 @Override refreshDocument(DocumentInfo doc, BooleanConsumer callback)214 public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) { 215 RefreshTask task = new RefreshTask( 216 mInjector.features, 217 mState, 218 doc, 219 REFRESH_SPINNER_TIMEOUT, 220 mActivity.getApplicationContext(), 221 mActivity::isDestroyed, 222 callback); 223 task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority)); 224 } 225 226 @Override openSelectedInNewWindow()227 public void openSelectedInNewWindow() { 228 throw new UnsupportedOperationException("Can't open in new window."); 229 } 230 231 @Override openInNewWindow(DocumentStack path)232 public void openInNewWindow(DocumentStack path) { 233 Metrics.logUserAction(MetricConsts.USER_ACTION_NEW_WINDOW); 234 235 Intent intent = LauncherActivity.createLaunchIntent(mActivity); 236 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path); 237 238 // Multi-window necessitates we pick how we are launched. 239 // By default we'd be launched in-place above the existing app. 240 // By setting launch-to-side ActivityManager will open us to side. 241 if (mActivity.isInMultiWindowMode()) { 242 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); 243 } 244 245 mActivity.startActivity(intent); 246 } 247 248 @Override openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback)249 public boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) { 250 throw new UnsupportedOperationException("Can't open document."); 251 } 252 253 @Override showInspector(DocumentInfo doc)254 public void showInspector(DocumentInfo doc) { 255 throw new UnsupportedOperationException("Can't open properties."); 256 } 257 258 @Override springOpenDirectory(DocumentInfo doc)259 public void springOpenDirectory(DocumentInfo doc) { 260 throw new UnsupportedOperationException("Can't spring open directories."); 261 } 262 263 @Override openSettings(RootInfo root)264 public void openSettings(RootInfo root) { 265 throw new UnsupportedOperationException("Can't open settings."); 266 } 267 268 @Override openRoot(ResolveInfo app, UserId userId)269 public void openRoot(ResolveInfo app, UserId userId) { 270 throw new UnsupportedOperationException("Can't open an app."); 271 } 272 273 @Override showAppDetails(ResolveInfo info, UserId userId)274 public void showAppDetails(ResolveInfo info, UserId userId) { 275 throw new UnsupportedOperationException("Can't show app details."); 276 } 277 278 @Override dropOn(DragEvent event, RootInfo root)279 public boolean dropOn(DragEvent event, RootInfo root) { 280 throw new UnsupportedOperationException("Can't open an app."); 281 } 282 283 @Override pasteIntoFolder(RootInfo root)284 public void pasteIntoFolder(RootInfo root) { 285 throw new UnsupportedOperationException("Can't paste into folder."); 286 } 287 288 @Override viewInOwner()289 public void viewInOwner() { 290 throw new UnsupportedOperationException("Can't view in application."); 291 } 292 293 @Override selectAllFiles()294 public void selectAllFiles() { 295 Metrics.logUserAction(MetricConsts.USER_ACTION_SELECT_ALL); 296 Model model = mInjector.getModel(); 297 298 // Exclude disabled files 299 List<String> enabled = new ArrayList<>(); 300 for (String id : model.getModelIds()) { 301 Cursor cursor = model.getItem(id); 302 if (cursor == null) { 303 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id); 304 continue; 305 } 306 String docMimeType = getCursorString( 307 cursor, DocumentsContract.Document.COLUMN_MIME_TYPE); 308 int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS); 309 if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) { 310 enabled.add(id); 311 } 312 } 313 314 // Only select things currently visible in the adapter. 315 boolean changed = mSelectionMgr.setItemsSelected(enabled, true); 316 if (changed) { 317 mDisplayStateChangedListener.run(); 318 } 319 } 320 321 @Override deselectAllFiles()322 public void deselectAllFiles() { 323 mSelectionMgr.clearSelection(); 324 } 325 326 @Override showCreateDirectoryDialog()327 public void showCreateDirectoryDialog() { 328 Metrics.logUserAction(MetricConsts.USER_ACTION_CREATE_DIR); 329 330 CreateDirectoryFragment.show(mActivity.getSupportFragmentManager()); 331 } 332 333 @Override showSortDialog()334 public void showSortDialog() { 335 SortListFragment.show(mActivity.getSupportFragmentManager(), mState.sortModel); 336 } 337 338 @Override 339 @Nullable renameDocument(String name, DocumentInfo document)340 public DocumentInfo renameDocument(String name, DocumentInfo document) { 341 throw new UnsupportedOperationException("Can't rename documents."); 342 } 343 344 @Override showChooserForDoc(DocumentInfo doc)345 public void showChooserForDoc(DocumentInfo doc) { 346 throw new UnsupportedOperationException("Show chooser for doc not supported!"); 347 } 348 349 @Override openRootDocument(@ullable DocumentInfo rootDoc)350 public void openRootDocument(@Nullable DocumentInfo rootDoc) { 351 if (rootDoc == null) { 352 // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root 353 // document. Either case we should call refreshCurrentRootAndDirectory() to let 354 // DirectoryFragment update UI. 355 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 356 } else { 357 openContainerDocument(rootDoc); 358 } 359 } 360 361 @Override openContainerDocument(DocumentInfo doc)362 public void openContainerDocument(DocumentInfo doc) { 363 assert (doc.isContainer()); 364 365 if (mSearchMgr.isSearching()) { 366 loadDocument( 367 doc.derivedUri, 368 doc.userId, 369 (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); 370 } else { 371 openChildContainer(doc); 372 } 373 } 374 375 // TODO: Make this private and make tests call interface method instead. 376 377 /** 378 * Behavior when a document is opened. 379 */ 380 @VisibleForTesting onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback, boolean fromPicker)381 public void onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback, 382 boolean fromPicker) { 383 // In picker mode, don't access archive container to avoid pick file in archive files. 384 if (doc.isContainer() && !fromPicker) { 385 openContainerDocument(doc); 386 return; 387 } 388 389 if (manageDocument(doc)) { 390 return; 391 } 392 393 // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow 394 // PackageManager to install it. This allows users to install APKs from any root. 395 // The Downloads special case is handled above in #manageDocument. 396 if (MimeTypes.isApkType(doc.mimeType)) { 397 viewDocument(doc); 398 return; 399 } 400 401 switch (type) { 402 case VIEW_TYPE_REGULAR: 403 if (viewDocument(doc)) { 404 return; 405 } 406 break; 407 408 case VIEW_TYPE_PREVIEW: 409 if (previewDocument(doc, fromPicker)) { 410 return; 411 } 412 break; 413 414 default: 415 throw new IllegalArgumentException("Illegal view type."); 416 } 417 418 switch (fallback) { 419 case VIEW_TYPE_REGULAR: 420 if (viewDocument(doc)) { 421 return; 422 } 423 break; 424 425 case VIEW_TYPE_PREVIEW: 426 if (previewDocument(doc, fromPicker)) { 427 return; 428 } 429 break; 430 431 case VIEW_TYPE_NONE: 432 break; 433 434 default: 435 throw new IllegalArgumentException("Illegal fallback view type."); 436 } 437 438 // Failed to view including fallback, and it's in an archive. 439 if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) { 440 mDialogs.showViewInArchivesUnsupported(); 441 } 442 } 443 viewDocument(DocumentInfo doc)444 private boolean viewDocument(DocumentInfo doc) { 445 if (doc.isPartial()) { 446 Log.w(TAG, "Can't view partial file."); 447 return false; 448 } 449 450 if (doc.isInArchive()) { 451 Log.w(TAG, "Can't view files in archives."); 452 return false; 453 } 454 455 if (doc.isDirectory()) { 456 Log.w(TAG, "Can't view directories."); 457 return true; 458 } 459 460 Intent intent = buildViewIntent(doc); 461 if (DEBUG && intent.getClipData() != null) { 462 Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData()); 463 } 464 465 try { 466 doc.userId.startActivityAsUser(mActivity, intent); 467 return true; 468 } catch (ActivityNotFoundException e) { 469 mDialogs.showNoApplicationFound(); 470 } 471 return false; 472 } 473 previewDocument(DocumentInfo doc, boolean fromPicker)474 private boolean previewDocument(DocumentInfo doc, boolean fromPicker) { 475 if (doc.isPartial()) { 476 Log.w(TAG, "Can't view partial file."); 477 return false; 478 } 479 480 Intent intent = new QuickViewIntentBuilder( 481 mActivity, 482 mActivity.getResources(), 483 doc, 484 mModel, 485 fromPicker).build(); 486 487 if (intent != null) { 488 // TODO: un-work around issue b/24963914. Should be fixed soon. 489 try { 490 doc.userId.startActivityAsUser(mActivity, intent); 491 return true; 492 } catch (SecurityException e) { 493 // Carry on to regular view mode. 494 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage()); 495 } 496 } 497 498 return false; 499 } 500 501 manageDocument(DocumentInfo doc)502 protected boolean manageDocument(DocumentInfo doc) { 503 if (isManagedDownload(doc)) { 504 // First try managing the document; we expect manager to filter 505 // based on authority, so we don't grant. 506 Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); 507 manage.setData(doc.getDocumentUri()); 508 try { 509 doc.userId.startActivityAsUser(mActivity, manage); 510 return true; 511 } catch (ActivityNotFoundException ex) { 512 // Fall back to regular handling. 513 } 514 } 515 516 return false; 517 } 518 isManagedDownload(DocumentInfo doc)519 private boolean isManagedDownload(DocumentInfo doc) { 520 // Anything on downloads goes through the back through downloads manager 521 // (that's the MANAGE_DOCUMENT bit). 522 // This is done for two reasons: 523 // 1) The file in question might be a failed/queued or otherwise have some 524 // specialized download handling. 525 // 2) For APKs, the download manager will add on some important security stuff 526 // like origin URL. 527 // 3) For partial files, the download manager will offer to restart/retry downloads. 528 529 // All other files not on downloads, event APKs, would get no benefit from this 530 // treatment, thusly the "isDownloads" check. 531 532 // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for 533 // files in archives or in child folders. Also, if the activity is already browsing 534 // a ZIP from downloads, then skip MANAGE_DOCUMENTS. 535 if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction()) 536 && mState.stack.size() > 1) { 537 // viewing the contents of an archive. 538 return false; 539 } 540 541 // management is only supported in Downloads root or downloaded files show in Recent root. 542 if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) { 543 // only on APKs or partial files. 544 return MimeTypes.isApkType(doc.mimeType) || doc.isPartial(); 545 } 546 547 return false; 548 } 549 buildViewIntent(DocumentInfo doc)550 protected Intent buildViewIntent(DocumentInfo doc) { 551 Intent intent = new Intent(Intent.ACTION_VIEW); 552 intent.setDataAndType(doc.getDocumentUri(), doc.mimeType); 553 554 // Downloads has traditionally added the WRITE permission 555 // in the TrampolineActivity. Since this behavior is long 556 // established, we set the same permission for non-managed files 557 // This ensures consistent behavior between the Downloads root 558 // and other roots. 559 int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP; 560 if (doc.isWriteSupported()) { 561 flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 562 } 563 intent.setFlags(flags); 564 565 return intent; 566 } 567 568 @Override previewItem(ItemDetails<String> doc)569 public boolean previewItem(ItemDetails<String> doc) { 570 throw new UnsupportedOperationException("Can't handle preview."); 571 } 572 openFolderInSearchResult(@ullable DocumentStack stack, DocumentInfo doc)573 private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) { 574 if (stack == null) { 575 mState.stack.popToRootDocument(); 576 577 // Update navigator to give horizontal breadcrumb a chance to update documents. It 578 // doesn't update its content if the size of document stack doesn't change. 579 // TODO: update breadcrumb to take range update. 580 mActivity.updateNavigator(); 581 582 mState.stack.push(doc); 583 } else { 584 if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) { 585 // It is now possible when opening cross-profile folder. 586 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected " 587 + mState.stack.getRoot()); 588 } 589 590 final DocumentInfo top = stack.peek(); 591 if (top.isArchive()) { 592 // Swap the zip file in original provider and the one provided by ArchiveProvider. 593 stack.pop(); 594 stack.push(mDocs.getArchiveDocument(top.derivedUri, top.userId)); 595 } 596 597 mState.stack.reset(); 598 // Update navigator to give horizontal breadcrumb a chance to update documents. It 599 // doesn't update its content if the size of document stack doesn't change. 600 // TODO: update breadcrumb to take range update. 601 mActivity.updateNavigator(); 602 603 mState.stack.reset(stack); 604 } 605 606 // Show an opening animation only if pressing "back" would get us back to the 607 // previous directory. Especially after opening a root document, pressing 608 // back, wouldn't go to the previous root, but close the activity. 609 final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) 610 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 611 mActivity.refreshCurrentRootAndDirectory(anim); 612 } 613 openChildContainer(DocumentInfo doc)614 private void openChildContainer(DocumentInfo doc) { 615 DocumentInfo currentDoc = null; 616 617 if (doc.isDirectory()) { 618 // Regular directory. 619 currentDoc = doc; 620 } else if (doc.isArchive()) { 621 // Archive. 622 currentDoc = mDocs.getArchiveDocument(doc.derivedUri, doc.userId); 623 } 624 625 assert (currentDoc != null); 626 if (currentDoc.equals(mState.stack.peek())) { 627 Log.w(TAG, "This DocumentInfo is already in current DocumentsStack"); 628 return; 629 } 630 631 mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); 632 633 mState.stack.push(currentDoc); 634 // Show an opening animation only if pressing "back" would get us back to the 635 // previous directory. Especially after opening a root document, pressing 636 // back, wouldn't go to the previous root, but close the activity. 637 final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) 638 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 639 mActivity.refreshCurrentRootAndDirectory(anim); 640 } 641 642 @Override setDebugMode(boolean enabled)643 public void setDebugMode(boolean enabled) { 644 if (!mInjector.features.isDebugSupportEnabled()) { 645 return; 646 } 647 648 mState.debugMode = enabled; 649 mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled); 650 mInjector.features.forceFeature(R.bool.feature_inspector, enabled); 651 mActivity.invalidateOptionsMenu(); 652 653 if (enabled) { 654 showDebugMessage(); 655 } else { 656 mActivity.getWindow().setStatusBarColor( 657 mActivity.getResources().getColor(R.color.app_background_color)); 658 } 659 } 660 661 @Override showDebugMessage()662 public void showDebugMessage() { 663 assert (mInjector.features.isDebugSupportEnabled()); 664 665 int[] colors = mInjector.debugHelper.getNextColors(); 666 Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage(); 667 668 Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second); 669 670 mActivity.getWindow().setStatusBarColor(colors[1]); 671 } 672 673 @Override switchLauncherIcon()674 public void switchLauncherIcon() { 675 PackageManager pm = mActivity.getPackageManager(); 676 if (pm != null) { 677 final boolean enalbled = Shared.isLauncherEnabled(mActivity); 678 ComponentName component = new ComponentName( 679 mActivity.getPackageName(), Shared.LAUNCHER_TARGET_CLASS); 680 pm.setComponentEnabledSetting(component, enalbled 681 ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED 682 : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 683 PackageManager.DONT_KILL_APP); 684 } 685 } 686 687 @Override cutToClipboard()688 public void cutToClipboard() { 689 throw new UnsupportedOperationException("Cut not supported!"); 690 } 691 692 @Override copyToClipboard()693 public void copyToClipboard() { 694 throw new UnsupportedOperationException("Copy not supported!"); 695 } 696 697 @Override showDeleteDialog()698 public void showDeleteDialog() { 699 throw new UnsupportedOperationException("Delete not supported!"); 700 } 701 702 @Override deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent)703 public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) { 704 throw new UnsupportedOperationException("Delete not supported!"); 705 } 706 707 @Override shareSelectedDocuments()708 public void shareSelectedDocuments() { 709 throw new UnsupportedOperationException("Share not supported!"); 710 } 711 loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback)712 protected final void loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback) { 713 new LoadDocStackTask( 714 mActivity, 715 mProviders, 716 mDocs, 717 userId, 718 callback 719 ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); 720 } 721 722 @Override loadRoot(Uri uri, UserId userId)723 public final void loadRoot(Uri uri, UserId userId) { 724 new LoadRootTask<>(mActivity, mProviders, uri, userId, this::onRootLoaded) 725 .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); 726 } 727 728 @Override loadCrossProfileRoot(RootInfo info, UserId selectedUser)729 public final void loadCrossProfileRoot(RootInfo info, UserId selectedUser) { 730 if (info.isRecents()) { 731 openRoot(mProviders.getRecentsRoot(selectedUser)); 732 return; 733 } 734 new LoadRootTask<>(mActivity, mProviders, info.getUri(), selectedUser, 735 new LoadCrossProfileRootCallback(info, selectedUser)) 736 .executeOnExecutor(mExecutors.lookup(info.getUri().getAuthority())); 737 } 738 739 private class LoadCrossProfileRootCallback implements LoadRootTask.LoadRootCallback { 740 private final RootInfo mOriginalRoot; 741 private final UserId mSelectedUserId; 742 LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser)743 LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser) { 744 mOriginalRoot = rootInfo; 745 mSelectedUserId = selectedUser; 746 } 747 748 @Override onRootLoaded(@ullable RootInfo root)749 public void onRootLoaded(@Nullable RootInfo root) { 750 if (root == null) { 751 // There is no such root in the other profile. Maybe the provider is missing on 752 // the other profile. Create a placeholder root and open it to show error message. 753 root = RootInfo.copyRootInfo(mOriginalRoot); 754 root.userId = mSelectedUserId; 755 } 756 openRoot(root); 757 } 758 } 759 760 @Override loadFirstRoot(Uri uri)761 public final void loadFirstRoot(Uri uri) { 762 new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded) 763 .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); 764 } 765 766 @Override loadDocumentsForCurrentStack()767 public void loadDocumentsForCurrentStack() { 768 // mState.stack may be empty when we cannot load the root document. 769 // However, we still want to restart loader because we may need to perform search in a 770 // cross-profile scenario. 771 // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op. 772 // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null. 773 774 // Only allow restartLoader when the previous loader is finished or reset. Allowing 775 // multiple consecutive calls to restartLoader() / onCreateLoader() will probably create 776 // multiple active loaders, because restartLoader() does not interrupt previous loaders' 777 // loading, therefore may block the UI thread and cause ANR. 778 if (mLoaderSemaphore.tryAcquire()) { 779 mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); 780 } 781 } 782 launchToDocument(Uri uri)783 protected final boolean launchToDocument(Uri uri) { 784 if (DEBUG) { 785 Log.d(TAG, "launchToDocument() uri=" + uri); 786 } 787 788 // We don't support launching to a document in an archive. 789 if (Providers.isArchiveUri(uri)) { 790 return false; 791 } 792 793 loadDocument(uri, UserId.DEFAULT_USER, this::onStackToLaunchToLoaded); 794 return true; 795 } 796 797 /** 798 * Invoked <b>only</b> once, when the initial stack (that is the stack we are going to 799 * "launch to") is loaded. 800 * 801 * @see #launchToDocument(Uri) 802 */ onStackToLaunchToLoaded(@ullable DocumentStack stack)803 private void onStackToLaunchToLoaded(@Nullable DocumentStack stack) { 804 if (DEBUG) { 805 Log.d(TAG, "onLaunchStackLoaded() stack=" + stack); 806 } 807 808 if (stack == null) { 809 Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); 810 launchToDefaultLocation(); 811 812 Metrics.logLaunchAtLocation(mState, null); 813 return; 814 } 815 816 // Make sure the document at the top of the stack is a directory (if it isn't - just pop 817 // one off). 818 if (!stack.peek().isDirectory()) { 819 stack.pop(); 820 } 821 822 mState.stack.reset(stack); 823 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 824 825 Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); 826 } 827 onRootLoaded(@ullable RootInfo root)828 private void onRootLoaded(@Nullable RootInfo root) { 829 boolean invalidRootForAction = 830 (root != null 831 && !root.supportsChildren() 832 && mState.action == State.ACTION_OPEN_TREE); 833 834 if (invalidRootForAction) { 835 loadDeviceRoot(); 836 } else if (root != null) { 837 mActivity.onRootPicked(root); 838 } else { 839 launchToDefaultLocation(); 840 } 841 } 842 launchToDefaultLocation()843 protected abstract void launchToDefaultLocation(); 844 restoreRootAndDirectory()845 protected void restoreRootAndDirectory() { 846 if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) { 847 mActivity.onRootPicked(mState.stack.getRoot()); 848 } else { 849 mActivity.restoreRootAndDirectory(); 850 } 851 } 852 loadDeviceRoot()853 protected final void loadDeviceRoot() { 854 loadRoot(DocumentsContract.buildRootUri(Providers.AUTHORITY_STORAGE, 855 Providers.ROOT_ID_DEVICE), UserId.DEFAULT_USER); 856 } 857 loadHomeDir()858 protected final void loadHomeDir() { 859 loadRoot(Shared.getDefaultRootUri(mActivity), UserId.DEFAULT_USER); 860 } 861 loadRecent()862 protected final void loadRecent() { 863 mState.stack.changeRoot(mProviders.getRecentsRoot(UserId.DEFAULT_USER)); 864 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 865 } 866 getStableSelection()867 protected MutableSelection<String> getStableSelection() { 868 MutableSelection<String> selection = new MutableSelection<>(); 869 mSelectionMgr.copySelection(selection); 870 return selection; 871 } 872 873 @Override reset(ContentLock reloadLock)874 public ActionHandler reset(ContentLock reloadLock) { 875 mContentLock = reloadLock; 876 mActivity.getLoaderManager().destroyLoader(LOADER_ID); 877 return this; 878 } 879 880 private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { 881 882 @Override onCreateLoader(int id, Bundle args)883 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { 884 Context context = mActivity; 885 886 if (mState.stack.isRecents()) { 887 final LockingContentObserver observer = new LockingContentObserver( 888 mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); 889 MultiRootDocumentsLoader loader; 890 891 if (mSearchMgr.isSearching()) { 892 if (DEBUG) { 893 Log.d(TAG, "Creating new GlobalSearchLoader."); 894 } 895 loader = new GlobalSearchLoader( 896 context, 897 mProviders, 898 mState, 899 mExecutors, 900 mInjector.fileTypeLookup, 901 mSearchMgr.buildQueryArgs(), 902 mState.stack.getRoot().userId); 903 } else { 904 if (DEBUG) { 905 Log.d(TAG, "Creating new loader recents."); 906 } 907 loader = new RecentsLoader( 908 context, 909 mProviders, 910 mState, 911 mExecutors, 912 mInjector.fileTypeLookup, 913 mState.stack.getRoot().userId); 914 } 915 loader.setObserver(observer); 916 return loader; 917 } else { 918 // There maybe no root docInfo 919 DocumentInfo rootDoc = mState.stack.peek(); 920 921 String authority = rootDoc == null 922 ? mState.stack.getRoot().authority 923 : rootDoc.authority; 924 String documentId = rootDoc == null 925 ? mState.stack.getRoot().documentId 926 : rootDoc.documentId; 927 928 Uri contentsUri = mSearchMgr.isSearching() 929 ? DocumentsContract.buildSearchDocumentsUri( 930 mState.stack.getRoot().authority, 931 mState.stack.getRoot().rootId, 932 mSearchMgr.getCurrentSearch()) 933 : DocumentsContract.buildChildDocumentsUri( 934 authority, 935 documentId); 936 937 final Bundle queryArgs = mSearchMgr.isSearching() 938 ? mSearchMgr.buildQueryArgs() 939 : null; 940 941 if (mInjector.config.managedModeEnabled(mState.stack)) { 942 contentsUri = DocumentsContract.setManageMode(contentsUri); 943 } 944 945 if (DEBUG) { 946 Log.d(TAG, 947 "Creating new directory loader for: " 948 + DocumentInfo.debugString(mState.stack.peek())); 949 } 950 951 return new DirectoryLoader( 952 mInjector.features, 953 context, 954 mState, 955 contentsUri, 956 mInjector.fileTypeLookup, 957 mContentLock, 958 queryArgs); 959 } 960 } 961 962 @Override onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)963 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { 964 if (DEBUG) { 965 Log.d(TAG, "Loader has finished for: " 966 + DocumentInfo.debugString(mState.stack.peek())); 967 } 968 assert (result != null); 969 970 mInjector.getModel().update(result); 971 mLoaderSemaphore.release(); 972 } 973 974 @Override onLoaderReset(Loader<DirectoryResult> loader)975 public void onLoaderReset(Loader<DirectoryResult> loader) { 976 mLoaderSemaphore.release(); 977 } 978 } 979 980 /** 981 * A class primarily for the support of isolating our tests 982 * from our concrete activity implementations. 983 */ 984 public interface CommonAddons { restoreRootAndDirectory()985 void restoreRootAndDirectory(); 986 refreshCurrentRootAndDirectory(@nimationType int anim)987 void refreshCurrentRootAndDirectory(@AnimationType int anim); 988 onRootPicked(RootInfo root)989 void onRootPicked(RootInfo root); 990 991 // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity. onDocumentsPicked(List<DocumentInfo> docs)992 void onDocumentsPicked(List<DocumentInfo> docs); 993 onDocumentPicked(DocumentInfo doc)994 void onDocumentPicked(DocumentInfo doc); 995 getCurrentRoot()996 RootInfo getCurrentRoot(); 997 getCurrentDirectory()998 DocumentInfo getCurrentDirectory(); 999 getSelectedUser()1000 UserId getSelectedUser(); 1001 1002 /** 1003 * Check whether current directory is root of recent. 1004 */ isInRecents()1005 boolean isInRecents(); 1006 setRootsDrawerOpen(boolean open)1007 void setRootsDrawerOpen(boolean open); 1008 1009 /** 1010 * Set the locked status of the DrawerController. 1011 */ setRootsDrawerLocked(boolean locked)1012 void setRootsDrawerLocked(boolean locked); 1013 1014 // TODO: Let navigator listens to State updateNavigator()1015 void updateNavigator(); 1016 1017 @VisibleForTesting notifyDirectoryNavigated(Uri docUri)1018 void notifyDirectoryNavigated(Uri docUri); 1019 } 1020 } 1021