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