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.Context; 25 import android.content.Intent; 26 import android.content.IntentSender; 27 import android.content.pm.ResolveInfo; 28 import android.database.Cursor; 29 import android.graphics.drawable.ColorDrawable; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.Parcelable; 33 import android.provider.DocumentsContract; 34 import android.util.Log; 35 import android.util.Pair; 36 import android.view.DragEvent; 37 38 import androidx.annotation.VisibleForTesting; 39 import androidx.fragment.app.FragmentActivity; 40 import androidx.loader.app.LoaderManager.LoaderCallbacks; 41 import androidx.loader.content.Loader; 42 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 43 import androidx.recyclerview.selection.MutableSelection; 44 import androidx.recyclerview.selection.SelectionTracker; 45 46 import com.android.documentsui.AbstractActionHandler.CommonAddons; 47 import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; 48 import com.android.documentsui.base.BooleanConsumer; 49 import com.android.documentsui.base.DocumentInfo; 50 import com.android.documentsui.base.DocumentStack; 51 import com.android.documentsui.base.Lookup; 52 import com.android.documentsui.base.Providers; 53 import com.android.documentsui.base.RootInfo; 54 import com.android.documentsui.base.Shared; 55 import com.android.documentsui.base.State; 56 import com.android.documentsui.dirlist.AnimationView; 57 import com.android.documentsui.dirlist.AnimationView.AnimationType; 58 import com.android.documentsui.dirlist.FocusHandler; 59 import com.android.documentsui.files.LauncherActivity; 60 import com.android.documentsui.queries.SearchViewManager; 61 import com.android.documentsui.roots.GetRootDocumentTask; 62 import com.android.documentsui.roots.LoadFirstRootTask; 63 import com.android.documentsui.roots.LoadRootTask; 64 import com.android.documentsui.roots.ProvidersAccess; 65 import com.android.documentsui.sidebar.EjectRootTask; 66 import com.android.documentsui.sorting.SortListFragment; 67 import com.android.documentsui.ui.Snackbars; 68 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.Objects; 72 import java.util.concurrent.Executor; 73 import java.util.function.Consumer; 74 75 import javax.annotation.Nullable; 76 77 /** 78 * Provides support for specializing the actions (openDocument etc.) to the host activity. 79 */ 80 public abstract class AbstractActionHandler<T extends FragmentActivity & CommonAddons> 81 implements ActionHandler { 82 83 @VisibleForTesting 84 public static final int CODE_FORWARD = 42; 85 public static final int CODE_AUTHENTICATION = 43; 86 87 @VisibleForTesting 88 static final int LOADER_ID = 42; 89 90 private static final String TAG = "AbstractActionHandler"; 91 private static final int REFRESH_SPINNER_TIMEOUT = 500; 92 93 protected final T mActivity; 94 protected final State mState; 95 protected final ProvidersAccess mProviders; 96 protected final DocumentsAccess mDocs; 97 protected final FocusHandler mFocusHandler; 98 protected final SelectionTracker<String> mSelectionMgr; 99 protected final SearchViewManager mSearchMgr; 100 protected final Lookup<String, Executor> mExecutors; 101 protected final Injector<?> mInjector; 102 103 private final LoaderBindings mBindings; 104 105 private Runnable mDisplayStateChangedListener; 106 107 private ContentLock mContentLock; 108 109 @Override registerDisplayStateChangedListener(Runnable l)110 public void registerDisplayStateChangedListener(Runnable l) { 111 mDisplayStateChangedListener = l; 112 } 113 @Override unregisterDisplayStateChangedListener(Runnable l)114 public void unregisterDisplayStateChangedListener(Runnable l) { 115 if (mDisplayStateChangedListener == l) { 116 mDisplayStateChangedListener = null; 117 } 118 } 119 AbstractActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector<?> injector)120 public AbstractActionHandler( 121 T activity, 122 State state, 123 ProvidersAccess providers, 124 DocumentsAccess docs, 125 SearchViewManager searchMgr, 126 Lookup<String, Executor> executors, 127 Injector<?> injector) { 128 129 assert(activity != null); 130 assert(state != null); 131 assert(providers != null); 132 assert(searchMgr != null); 133 assert(docs != null); 134 assert(injector != null); 135 136 mActivity = activity; 137 mState = state; 138 mProviders = providers; 139 mDocs = docs; 140 mFocusHandler = injector.focusManager; 141 mSelectionMgr = injector.selectionMgr; 142 mSearchMgr = searchMgr; 143 mExecutors = executors; 144 mInjector = injector; 145 146 mBindings = new LoaderBindings(); 147 } 148 149 @Override ejectRoot(RootInfo root, BooleanConsumer listener)150 public void ejectRoot(RootInfo root, BooleanConsumer listener) { 151 new EjectRootTask( 152 mActivity.getContentResolver(), 153 root.authority, 154 root.rootId, 155 listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority)); 156 } 157 158 @Override startAuthentication(PendingIntent intent)159 public void startAuthentication(PendingIntent intent) { 160 try { 161 mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION, 162 null, 0, 0, 0); 163 } catch (IntentSender.SendIntentException cancelled) { 164 Log.d(TAG, "Authentication Pending Intent either canceled or ignored."); 165 } 166 } 167 168 @Override onActivityResult(int requestCode, int resultCode, Intent data)169 public void onActivityResult(int requestCode, int resultCode, Intent data) { 170 switch (requestCode) { 171 case CODE_AUTHENTICATION: 172 onAuthenticationResult(resultCode); 173 break; 174 } 175 } 176 onAuthenticationResult(int resultCode)177 private void onAuthenticationResult(int resultCode) { 178 if (resultCode == FragmentActivity.RESULT_OK) { 179 Log.v(TAG, "Authentication was successful. Refreshing directory now."); 180 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 181 } 182 } 183 184 @Override getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback)185 public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) { 186 GetRootDocumentTask task = new GetRootDocumentTask( 187 root, 188 mActivity, 189 timeout, 190 mDocs, 191 callback); 192 193 task.executeOnExecutor(mExecutors.lookup(root.authority)); 194 } 195 196 @Override refreshDocument(DocumentInfo doc, BooleanConsumer callback)197 public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) { 198 RefreshTask task = new RefreshTask( 199 mInjector.features, 200 mState, 201 doc, 202 REFRESH_SPINNER_TIMEOUT, 203 mActivity.getApplicationContext(), 204 mActivity::isDestroyed, 205 callback); 206 task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority)); 207 } 208 209 @Override openSelectedInNewWindow()210 public void openSelectedInNewWindow() { 211 throw new UnsupportedOperationException("Can't open in new window."); 212 } 213 214 @Override openInNewWindow(DocumentStack path)215 public void openInNewWindow(DocumentStack path) { 216 Metrics.logUserAction(MetricConsts.USER_ACTION_NEW_WINDOW); 217 218 Intent intent = LauncherActivity.createLaunchIntent(mActivity); 219 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path); 220 221 // Multi-window necessitates we pick how we are launched. 222 // By default we'd be launched in-place above the existing app. 223 // By setting launch-to-side ActivityManager will open us to side. 224 if (mActivity.isInMultiWindowMode()) { 225 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); 226 } 227 228 mActivity.startActivity(intent); 229 } 230 231 @Override openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback)232 public boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) { 233 throw new UnsupportedOperationException("Can't open document."); 234 } 235 236 @Override showInspector(DocumentInfo doc)237 public void showInspector(DocumentInfo doc) { 238 throw new UnsupportedOperationException("Can't open properties."); 239 } 240 241 @Override springOpenDirectory(DocumentInfo doc)242 public void springOpenDirectory(DocumentInfo doc) { 243 throw new UnsupportedOperationException("Can't spring open directories."); 244 } 245 246 @Override openSettings(RootInfo root)247 public void openSettings(RootInfo root) { 248 throw new UnsupportedOperationException("Can't open settings."); 249 } 250 251 @Override openRoot(ResolveInfo app)252 public void openRoot(ResolveInfo app) { 253 throw new UnsupportedOperationException("Can't open an app."); 254 } 255 256 @Override showAppDetails(ResolveInfo info)257 public void showAppDetails(ResolveInfo info) { 258 throw new UnsupportedOperationException("Can't show app details."); 259 } 260 261 @Override dropOn(DragEvent event, RootInfo root)262 public boolean dropOn(DragEvent event, RootInfo root) { 263 throw new UnsupportedOperationException("Can't open an app."); 264 } 265 266 @Override pasteIntoFolder(RootInfo root)267 public void pasteIntoFolder(RootInfo root) { 268 throw new UnsupportedOperationException("Can't paste into folder."); 269 } 270 271 @Override viewInOwner()272 public void viewInOwner() { 273 throw new UnsupportedOperationException("Can't view in application."); 274 } 275 276 @Override selectAllFiles()277 public void selectAllFiles() { 278 Metrics.logUserAction(MetricConsts.USER_ACTION_SELECT_ALL); 279 Model model = mInjector.getModel(); 280 281 // Exclude disabled files 282 List<String> enabled = new ArrayList<>(); 283 for (String id : model.getModelIds()) { 284 Cursor cursor = model.getItem(id); 285 if (cursor == null) { 286 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id); 287 continue; 288 } 289 String docMimeType = getCursorString( 290 cursor, DocumentsContract.Document.COLUMN_MIME_TYPE); 291 int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS); 292 if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) { 293 enabled.add(id); 294 } 295 } 296 297 // Only select things currently visible in the adapter. 298 boolean changed = mSelectionMgr.setItemsSelected(enabled, true); 299 if (changed) { 300 mDisplayStateChangedListener.run(); 301 } 302 } 303 304 @Override showCreateDirectoryDialog()305 public void showCreateDirectoryDialog() { 306 Metrics.logUserAction(MetricConsts.USER_ACTION_CREATE_DIR); 307 308 CreateDirectoryFragment.show(mActivity.getSupportFragmentManager()); 309 } 310 311 @Override showSortDialog()312 public void showSortDialog() { 313 SortListFragment.show(mActivity.getSupportFragmentManager(), mState.sortModel); 314 } 315 316 @Override 317 @Nullable renameDocument(String name, DocumentInfo document)318 public DocumentInfo renameDocument(String name, DocumentInfo document) { 319 throw new UnsupportedOperationException("Can't rename documents."); 320 } 321 322 @Override showChooserForDoc(DocumentInfo doc)323 public void showChooserForDoc(DocumentInfo doc) { 324 throw new UnsupportedOperationException("Show chooser for doc not supported!"); 325 } 326 327 @Override openRootDocument(@ullable DocumentInfo rootDoc)328 public void openRootDocument(@Nullable DocumentInfo rootDoc) { 329 if (rootDoc == null) { 330 // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root 331 // document. Either case we should call refreshCurrentRootAndDirectory() to let 332 // DirectoryFragment update UI. 333 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 334 } else { 335 openContainerDocument(rootDoc); 336 } 337 } 338 339 @Override openContainerDocument(DocumentInfo doc)340 public void openContainerDocument(DocumentInfo doc) { 341 assert(doc.isContainer()); 342 343 if (mSearchMgr.isSearching()) { 344 loadDocument( 345 doc.derivedUri, 346 (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); 347 } else { 348 openChildContainer(doc); 349 } 350 } 351 352 @Override previewItem(ItemDetails<String> doc)353 public boolean previewItem(ItemDetails<String> doc) { 354 throw new UnsupportedOperationException("Can't handle preview."); 355 } 356 openFolderInSearchResult(@ullable DocumentStack stack, DocumentInfo doc)357 private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) { 358 if (stack == null) { 359 mState.stack.popToRootDocument(); 360 361 // Update navigator to give horizontal breadcrumb a chance to update documents. It 362 // doesn't update its content if the size of document stack doesn't change. 363 // TODO: update breadcrumb to take range update. 364 mActivity.updateNavigator(); 365 366 mState.stack.push(doc); 367 } else { 368 if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) { 369 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected " 370 + mState.stack.getRoot()); 371 } 372 373 final DocumentInfo top = stack.peek(); 374 if (top.isArchive()) { 375 // Swap the zip file in original provider and the one provided by ArchiveProvider. 376 stack.pop(); 377 stack.push(mDocs.getArchiveDocument(top.derivedUri)); 378 } 379 380 mState.stack.reset(); 381 // Update navigator to give horizontal breadcrumb a chance to update documents. It 382 // doesn't update its content if the size of document stack doesn't change. 383 // TODO: update breadcrumb to take range update. 384 mActivity.updateNavigator(); 385 386 mState.stack.reset(stack); 387 } 388 389 // Show an opening animation only if pressing "back" would get us back to the 390 // previous directory. Especially after opening a root document, pressing 391 // back, wouldn't go to the previous root, but close the activity. 392 final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) 393 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 394 mActivity.refreshCurrentRootAndDirectory(anim); 395 } 396 openChildContainer(DocumentInfo doc)397 private void openChildContainer(DocumentInfo doc) { 398 DocumentInfo currentDoc = null; 399 400 if (doc.isDirectory()) { 401 // Regular directory. 402 currentDoc = doc; 403 } else if (doc.isArchive()) { 404 // Archive. 405 currentDoc = mDocs.getArchiveDocument(doc.derivedUri); 406 } 407 408 assert(currentDoc != null); 409 mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); 410 411 mState.stack.push(currentDoc); 412 // Show an opening animation only if pressing "back" would get us back to the 413 // previous directory. Especially after opening a root document, pressing 414 // back, wouldn't go to the previous root, but close the activity. 415 final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) 416 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 417 mActivity.refreshCurrentRootAndDirectory(anim); 418 } 419 420 @Override setDebugMode(boolean enabled)421 public void setDebugMode(boolean enabled) { 422 if (!mInjector.features.isDebugSupportEnabled()) { 423 return; 424 } 425 426 mState.debugMode = enabled; 427 mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled); 428 mInjector.features.forceFeature(R.bool.feature_inspector, enabled); 429 mActivity.invalidateOptionsMenu(); 430 431 if (enabled) { 432 showDebugMessage(); 433 } else { 434 mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable( 435 mActivity.getResources().getColor(R.color.primary))); 436 mActivity.getWindow().setStatusBarColor( 437 mActivity.getResources().getColor(android.R.color.background_dark)); 438 } 439 } 440 441 @Override showDebugMessage()442 public void showDebugMessage() { 443 assert (mInjector.features.isDebugSupportEnabled()); 444 445 int[] colors = mInjector.debugHelper.getNextColors(); 446 Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage(); 447 448 Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second); 449 450 mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable(colors[0])); 451 mActivity.getWindow().setStatusBarColor(colors[1]); 452 } 453 454 @Override cutToClipboard()455 public void cutToClipboard() { 456 throw new UnsupportedOperationException("Cut not supported!"); 457 } 458 459 @Override copyToClipboard()460 public void copyToClipboard() { 461 throw new UnsupportedOperationException("Copy not supported!"); 462 } 463 464 @Override deleteSelectedDocuments()465 public void deleteSelectedDocuments() { 466 throw new UnsupportedOperationException("Delete not supported!"); 467 } 468 469 @Override shareSelectedDocuments()470 public void shareSelectedDocuments() { 471 throw new UnsupportedOperationException("Share not supported!"); 472 } 473 loadDocument(Uri uri, LoadDocStackCallback callback)474 protected final void loadDocument(Uri uri, LoadDocStackCallback callback) { 475 new LoadDocStackTask( 476 mActivity, 477 mProviders, 478 mDocs, 479 callback 480 ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); 481 } 482 483 @Override loadRoot(Uri uri)484 public final void loadRoot(Uri uri) { 485 new LoadRootTask<>(mActivity, mProviders, mState, uri) 486 .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); 487 } 488 489 @Override loadFirstRoot(Uri uri)490 public final void loadFirstRoot(Uri uri) { 491 new LoadFirstRootTask<>(mActivity, mProviders, mState, uri) 492 .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); 493 } 494 495 @Override loadDocumentsForCurrentStack()496 public void loadDocumentsForCurrentStack() { 497 DocumentStack stack = mState.stack; 498 if (!stack.isRecents() && stack.isEmpty()) { 499 DirectoryResult result = new DirectoryResult(); 500 501 // TODO (b/35996595): Consider plumbing through the actual exception, though it might 502 // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()). 503 result.exception = new IllegalStateException("Failed to load root document."); 504 mInjector.getModel().update(result); 505 return; 506 } 507 508 mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); 509 } 510 launchToDocument(Uri uri)511 protected final boolean launchToDocument(Uri uri) { 512 // We don't support launching to a document in an archive. 513 if (!Providers.isArchiveUri(uri)) { 514 loadDocument(uri, this::onStackLoaded); 515 return true; 516 } 517 518 return false; 519 } 520 onStackLoaded(@ullable DocumentStack stack)521 private void onStackLoaded(@Nullable DocumentStack stack) { 522 if (stack != null) { 523 if (!stack.peek().isDirectory()) { 524 // Requested document is not a directory. Pop it so that we can launch into its 525 // parent. 526 stack.pop(); 527 } 528 mState.stack.reset(stack); 529 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 530 531 Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); 532 } else { 533 Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); 534 launchToDefaultLocation(); 535 536 Metrics.logLaunchAtLocation(mState, null); 537 } 538 } 539 launchToDefaultLocation()540 protected abstract void launchToDefaultLocation(); 541 restoreRootAndDirectory()542 protected void restoreRootAndDirectory() { 543 if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) { 544 mActivity.onRootPicked(mState.stack.getRoot()); 545 } else { 546 mActivity.restoreRootAndDirectory(); 547 } 548 } 549 loadHomeDir()550 protected final void loadHomeDir() { 551 loadRoot(Shared.getDefaultRootUri(mActivity)); 552 } 553 loadRecent()554 protected final void loadRecent() { 555 mState.stack.changeRoot(mProviders.getRecentsRoot()); 556 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 557 } 558 getStableSelection()559 protected MutableSelection<String> getStableSelection() { 560 MutableSelection<String> selection = new MutableSelection<>(); 561 mSelectionMgr.copySelection(selection); 562 return selection; 563 } 564 565 @Override reset(ContentLock reloadLock)566 public ActionHandler reset(ContentLock reloadLock) { 567 mContentLock = reloadLock; 568 mActivity.getLoaderManager().destroyLoader(LOADER_ID); 569 return this; 570 } 571 572 private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { 573 574 @Override onCreateLoader(int id, Bundle args)575 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { 576 Context context = mActivity; 577 578 if (mState.stack.isRecents()) { 579 final LockingContentObserver observer = new LockingContentObserver( 580 mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); 581 MultiRootDocumentsLoader loader; 582 583 if (mSearchMgr.isSearching()) { 584 if (DEBUG) { 585 Log.d(TAG, "Creating new GlobalSearchLoader."); 586 } 587 loader = new GlobalSearchLoader( 588 context, 589 mProviders, 590 mState, 591 mExecutors, 592 mInjector.fileTypeLookup, 593 mSearchMgr.buildQueryArgs()); 594 } else { 595 if (DEBUG) { 596 Log.d(TAG, "Creating new loader recents."); 597 } 598 loader = new RecentsLoader( 599 context, 600 mProviders, 601 mState, 602 mExecutors, 603 mInjector.fileTypeLookup); 604 } 605 loader.setObserver(observer); 606 return loader; 607 } else { 608 Uri contentsUri = mSearchMgr.isSearching() 609 ? DocumentsContract.buildSearchDocumentsUri( 610 mState.stack.getRoot().authority, 611 mState.stack.getRoot().rootId, 612 mSearchMgr.getCurrentSearch()) 613 : DocumentsContract.buildChildDocumentsUri( 614 mState.stack.peek().authority, 615 mState.stack.peek().documentId); 616 617 final Bundle queryArgs = mSearchMgr.isSearching() 618 ? mSearchMgr.buildQueryArgs() 619 : null; 620 621 if (mInjector.config.managedModeEnabled(mState.stack)) { 622 contentsUri = DocumentsContract.setManageMode(contentsUri); 623 } 624 625 if (DEBUG) { 626 Log.d(TAG, 627 "Creating new directory loader for: " 628 + DocumentInfo.debugString(mState.stack.peek())); 629 } 630 631 return new DirectoryLoader( 632 mInjector.features, 633 context, 634 mState, 635 contentsUri, 636 mInjector.fileTypeLookup, 637 mContentLock, 638 queryArgs); 639 } 640 } 641 642 @Override onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)643 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { 644 if (DEBUG) { 645 Log.d(TAG, "Loader has finished for: " 646 + DocumentInfo.debugString(mState.stack.peek())); 647 } 648 assert(result != null); 649 650 mInjector.getModel().update(result); 651 } 652 653 @Override onLoaderReset(Loader<DirectoryResult> loader)654 public void onLoaderReset(Loader<DirectoryResult> loader) {} 655 } 656 /** 657 * A class primarily for the support of isolating our tests 658 * from our concrete activity implementations. 659 */ 660 public interface CommonAddons { restoreRootAndDirectory()661 void restoreRootAndDirectory(); refreshCurrentRootAndDirectory(@nimationType int anim)662 void refreshCurrentRootAndDirectory(@AnimationType int anim); onRootPicked(RootInfo root)663 void onRootPicked(RootInfo root); 664 // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity. onDocumentsPicked(List<DocumentInfo> docs)665 void onDocumentsPicked(List<DocumentInfo> docs); onDocumentPicked(DocumentInfo doc)666 void onDocumentPicked(DocumentInfo doc); getCurrentRoot()667 RootInfo getCurrentRoot(); getCurrentDirectory()668 DocumentInfo getCurrentDirectory(); 669 /** 670 * Check whether current directory is root of recent. 671 */ isInRecents()672 boolean isInRecents(); setRootsDrawerOpen(boolean open)673 void setRootsDrawerOpen(boolean open); 674 675 // TODO: Let navigator listens to State updateNavigator()676 void updateNavigator(); 677 678 @VisibleForTesting notifyDirectoryNavigated(Uri docUri)679 void notifyDirectoryNavigated(Uri docUri); 680 } 681 } 682