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