| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui; |
| |
| import static com.android.documentsui.base.DocumentInfo.getCursorInt; |
| import static com.android.documentsui.base.DocumentInfo.getCursorString; |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; |
| |
| import android.app.PendingIntent; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Parcelable; |
| import android.provider.DocumentsContract; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.DragEvent; |
| |
| import androidx.annotation.VisibleForTesting; |
| import androidx.fragment.app.FragmentActivity; |
| import androidx.loader.app.LoaderManager.LoaderCallbacks; |
| import androidx.loader.content.Loader; |
| import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; |
| import androidx.recyclerview.selection.MutableSelection; |
| import androidx.recyclerview.selection.SelectionTracker; |
| |
| import com.android.documentsui.AbstractActionHandler.CommonAddons; |
| import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; |
| import com.android.documentsui.base.BooleanConsumer; |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.DocumentStack; |
| import com.android.documentsui.base.Lookup; |
| import com.android.documentsui.base.Providers; |
| import com.android.documentsui.base.RootInfo; |
| import com.android.documentsui.base.Shared; |
| import com.android.documentsui.base.State; |
| import com.android.documentsui.base.UserId; |
| import com.android.documentsui.dirlist.AnimationView; |
| import com.android.documentsui.dirlist.AnimationView.AnimationType; |
| import com.android.documentsui.dirlist.FocusHandler; |
| import com.android.documentsui.files.LauncherActivity; |
| import com.android.documentsui.queries.SearchViewManager; |
| import com.android.documentsui.roots.GetRootDocumentTask; |
| import com.android.documentsui.roots.LoadFirstRootTask; |
| import com.android.documentsui.roots.LoadRootTask; |
| import com.android.documentsui.roots.ProvidersAccess; |
| import com.android.documentsui.sidebar.EjectRootTask; |
| import com.android.documentsui.sorting.SortListFragment; |
| import com.android.documentsui.ui.Snackbars; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Provides support for specializing the actions (openDocument etc.) to the host activity. |
| */ |
| public abstract class AbstractActionHandler<T extends FragmentActivity & CommonAddons> |
| implements ActionHandler { |
| |
| @VisibleForTesting |
| public static final int CODE_FORWARD = 42; |
| public static final int CODE_AUTHENTICATION = 43; |
| |
| @VisibleForTesting |
| static final int LOADER_ID = 42; |
| |
| private static final String TAG = "AbstractActionHandler"; |
| private static final int REFRESH_SPINNER_TIMEOUT = 500; |
| |
| protected final T mActivity; |
| protected final State mState; |
| protected final ProvidersAccess mProviders; |
| protected final DocumentsAccess mDocs; |
| protected final FocusHandler mFocusHandler; |
| protected final SelectionTracker<String> mSelectionMgr; |
| protected final SearchViewManager mSearchMgr; |
| protected final Lookup<String, Executor> mExecutors; |
| protected final Injector<?> mInjector; |
| |
| private final LoaderBindings mBindings; |
| |
| private Runnable mDisplayStateChangedListener; |
| |
| private ContentLock mContentLock; |
| |
| @Override |
| public void registerDisplayStateChangedListener(Runnable l) { |
| mDisplayStateChangedListener = l; |
| } |
| @Override |
| public void unregisterDisplayStateChangedListener(Runnable l) { |
| if (mDisplayStateChangedListener == l) { |
| mDisplayStateChangedListener = null; |
| } |
| } |
| |
| public AbstractActionHandler( |
| T activity, |
| State state, |
| ProvidersAccess providers, |
| DocumentsAccess docs, |
| SearchViewManager searchMgr, |
| Lookup<String, Executor> executors, |
| Injector<?> injector) { |
| |
| assert(activity != null); |
| assert(state != null); |
| assert(providers != null); |
| assert(searchMgr != null); |
| assert(docs != null); |
| assert(injector != null); |
| |
| mActivity = activity; |
| mState = state; |
| mProviders = providers; |
| mDocs = docs; |
| mFocusHandler = injector.focusManager; |
| mSelectionMgr = injector.selectionMgr; |
| mSearchMgr = searchMgr; |
| mExecutors = executors; |
| mInjector = injector; |
| |
| mBindings = new LoaderBindings(); |
| } |
| |
| @Override |
| public void ejectRoot(RootInfo root, BooleanConsumer listener) { |
| new EjectRootTask( |
| mActivity.getContentResolver(), |
| root.authority, |
| root.rootId, |
| listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority)); |
| } |
| |
| @Override |
| public void startAuthentication(PendingIntent intent) { |
| try { |
| mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION, |
| null, 0, 0, 0); |
| } catch (IntentSender.SendIntentException cancelled) { |
| Log.d(TAG, "Authentication Pending Intent either canceled or ignored."); |
| } |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| switch (requestCode) { |
| case CODE_AUTHENTICATION: |
| onAuthenticationResult(resultCode); |
| break; |
| } |
| } |
| |
| private void onAuthenticationResult(int resultCode) { |
| if (resultCode == FragmentActivity.RESULT_OK) { |
| Log.v(TAG, "Authentication was successful. Refreshing directory now."); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } |
| } |
| |
| @Override |
| public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) { |
| GetRootDocumentTask task = new GetRootDocumentTask( |
| root, |
| mActivity, |
| timeout, |
| mDocs, |
| callback); |
| |
| task.executeOnExecutor(mExecutors.lookup(root.authority)); |
| } |
| |
| @Override |
| public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) { |
| RefreshTask task = new RefreshTask( |
| mInjector.features, |
| mState, |
| doc, |
| REFRESH_SPINNER_TIMEOUT, |
| mActivity.getApplicationContext(), |
| mActivity::isDestroyed, |
| callback); |
| task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority)); |
| } |
| |
| @Override |
| public void openSelectedInNewWindow() { |
| throw new UnsupportedOperationException("Can't open in new window."); |
| } |
| |
| @Override |
| public void openInNewWindow(DocumentStack path) { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_NEW_WINDOW); |
| |
| Intent intent = LauncherActivity.createLaunchIntent(mActivity); |
| intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path); |
| |
| // Multi-window necessitates we pick how we are launched. |
| // By default we'd be launched in-place above the existing app. |
| // By setting launch-to-side ActivityManager will open us to side. |
| if (mActivity.isInMultiWindowMode()) { |
| intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); |
| } |
| |
| mActivity.startActivity(intent); |
| } |
| |
| @Override |
| public boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback) { |
| throw new UnsupportedOperationException("Can't open document."); |
| } |
| |
| @Override |
| public void showInspector(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Can't open properties."); |
| } |
| |
| @Override |
| public void springOpenDirectory(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Can't spring open directories."); |
| } |
| |
| @Override |
| public void openSettings(RootInfo root) { |
| throw new UnsupportedOperationException("Can't open settings."); |
| } |
| |
| @Override |
| public void openRoot(ResolveInfo app) { |
| throw new UnsupportedOperationException("Can't open an app."); |
| } |
| |
| @Override |
| public void showAppDetails(ResolveInfo info) { |
| throw new UnsupportedOperationException("Can't show app details."); |
| } |
| |
| @Override |
| public boolean dropOn(DragEvent event, RootInfo root) { |
| throw new UnsupportedOperationException("Can't open an app."); |
| } |
| |
| @Override |
| public void pasteIntoFolder(RootInfo root) { |
| throw new UnsupportedOperationException("Can't paste into folder."); |
| } |
| |
| @Override |
| public void viewInOwner() { |
| throw new UnsupportedOperationException("Can't view in application."); |
| } |
| |
| @Override |
| public void selectAllFiles() { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_SELECT_ALL); |
| Model model = mInjector.getModel(); |
| |
| // Exclude disabled files |
| List<String> enabled = new ArrayList<>(); |
| for (String id : model.getModelIds()) { |
| Cursor cursor = model.getItem(id); |
| if (cursor == null) { |
| Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id); |
| continue; |
| } |
| String docMimeType = getCursorString( |
| cursor, DocumentsContract.Document.COLUMN_MIME_TYPE); |
| int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS); |
| if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) { |
| enabled.add(id); |
| } |
| } |
| |
| // Only select things currently visible in the adapter. |
| boolean changed = mSelectionMgr.setItemsSelected(enabled, true); |
| if (changed) { |
| mDisplayStateChangedListener.run(); |
| } |
| } |
| |
| @Override |
| public void deselectAllFiles() { |
| mSelectionMgr.clearSelection(); |
| } |
| |
| @Override |
| public void showCreateDirectoryDialog() { |
| Metrics.logUserAction(MetricConsts.USER_ACTION_CREATE_DIR); |
| |
| CreateDirectoryFragment.show(mActivity.getSupportFragmentManager()); |
| } |
| |
| @Override |
| public void showSortDialog() { |
| SortListFragment.show(mActivity.getSupportFragmentManager(), mState.sortModel); |
| } |
| |
| @Override |
| @Nullable |
| public DocumentInfo renameDocument(String name, DocumentInfo document) { |
| throw new UnsupportedOperationException("Can't rename documents."); |
| } |
| |
| @Override |
| public void showChooserForDoc(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Show chooser for doc not supported!"); |
| } |
| |
| @Override |
| public void openRootDocument(@Nullable DocumentInfo rootDoc) { |
| if (rootDoc == null) { |
| // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root |
| // document. Either case we should call refreshCurrentRootAndDirectory() to let |
| // DirectoryFragment update UI. |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } else { |
| openContainerDocument(rootDoc); |
| } |
| } |
| |
| @Override |
| public void openContainerDocument(DocumentInfo doc) { |
| assert(doc.isContainer()); |
| |
| if (mSearchMgr.isSearching()) { |
| loadDocument( |
| doc.derivedUri, |
| doc.userId, |
| (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); |
| } else { |
| openChildContainer(doc); |
| } |
| } |
| |
| @Override |
| public boolean previewItem(ItemDetails<String> doc) { |
| throw new UnsupportedOperationException("Can't handle preview."); |
| } |
| |
| private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) { |
| if (stack == null) { |
| mState.stack.popToRootDocument(); |
| |
| // Update navigator to give horizontal breadcrumb a chance to update documents. It |
| // doesn't update its content if the size of document stack doesn't change. |
| // TODO: update breadcrumb to take range update. |
| mActivity.updateNavigator(); |
| |
| mState.stack.push(doc); |
| } else { |
| if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) { |
| Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected " |
| + mState.stack.getRoot()); |
| } |
| |
| final DocumentInfo top = stack.peek(); |
| if (top.isArchive()) { |
| // Swap the zip file in original provider and the one provided by ArchiveProvider. |
| stack.pop(); |
| stack.push(mDocs.getArchiveDocument(top.derivedUri, top.userId)); |
| } |
| |
| mState.stack.reset(); |
| // Update navigator to give horizontal breadcrumb a chance to update documents. It |
| // doesn't update its content if the size of document stack doesn't change. |
| // TODO: update breadcrumb to take range update. |
| mActivity.updateNavigator(); |
| |
| mState.stack.reset(stack); |
| } |
| |
| // Show an opening animation only if pressing "back" would get us back to the |
| // previous directory. Especially after opening a root document, pressing |
| // back, wouldn't go to the previous root, but close the activity. |
| final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) |
| ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; |
| mActivity.refreshCurrentRootAndDirectory(anim); |
| } |
| |
| private void openChildContainer(DocumentInfo doc) { |
| DocumentInfo currentDoc = null; |
| |
| if (doc.isDirectory()) { |
| // Regular directory. |
| currentDoc = doc; |
| } else if (doc.isArchive()) { |
| // Archive. |
| currentDoc = mDocs.getArchiveDocument(doc.derivedUri, doc.userId); |
| } |
| |
| assert(currentDoc != null); |
| if (currentDoc.equals(mState.stack.peek())) { |
| Log.w(TAG, "This DocumentInfo is already in current DocumentsStack"); |
| return; |
| } |
| |
| mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); |
| |
| mState.stack.push(currentDoc); |
| // Show an opening animation only if pressing "back" would get us back to the |
| // previous directory. Especially after opening a root document, pressing |
| // back, wouldn't go to the previous root, but close the activity. |
| final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1) |
| ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; |
| mActivity.refreshCurrentRootAndDirectory(anim); |
| } |
| |
| @Override |
| public void setDebugMode(boolean enabled) { |
| if (!mInjector.features.isDebugSupportEnabled()) { |
| return; |
| } |
| |
| mState.debugMode = enabled; |
| mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled); |
| mInjector.features.forceFeature(R.bool.feature_inspector, enabled); |
| mActivity.invalidateOptionsMenu(); |
| |
| if (enabled) { |
| showDebugMessage(); |
| } else { |
| mActivity.getWindow().setStatusBarColor( |
| mActivity.getResources().getColor(R.color.app_background_color)); |
| } |
| } |
| |
| @Override |
| public void showDebugMessage() { |
| assert (mInjector.features.isDebugSupportEnabled()); |
| |
| int[] colors = mInjector.debugHelper.getNextColors(); |
| Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage(); |
| |
| Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second); |
| |
| mActivity.getWindow().setStatusBarColor(colors[1]); |
| } |
| |
| @Override |
| public void switchLauncherIcon() { |
| PackageManager pm = mActivity.getPackageManager(); |
| if (pm != null) { |
| final boolean enalbled = Shared.isLauncherEnabled(mActivity); |
| ComponentName component = new ComponentName( |
| mActivity.getPackageName(), Shared.LAUNCHER_TARGET_CLASS); |
| pm.setComponentEnabledSetting(component, enalbled |
| ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED |
| : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, |
| PackageManager.DONT_KILL_APP); |
| } |
| } |
| |
| @Override |
| public void cutToClipboard() { |
| throw new UnsupportedOperationException("Cut not supported!"); |
| } |
| |
| @Override |
| public void copyToClipboard() { |
| throw new UnsupportedOperationException("Copy not supported!"); |
| } |
| |
| @Override |
| public void showDeleteDialog() { |
| throw new UnsupportedOperationException("Delete not supported!"); |
| } |
| |
| @Override |
| public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) { |
| throw new UnsupportedOperationException("Delete not supported!"); |
| } |
| |
| @Override |
| public void shareSelectedDocuments() { |
| throw new UnsupportedOperationException("Share not supported!"); |
| } |
| |
| protected final void loadDocument(Uri uri, UserId userId, LoadDocStackCallback callback) { |
| new LoadDocStackTask( |
| mActivity, |
| mProviders, |
| mDocs, |
| userId, |
| callback |
| ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); |
| } |
| |
| @Override |
| public final void loadRoot(Uri uri) { |
| new LoadRootTask<>(mActivity, mProviders, uri, this::onRootLoaded) |
| .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); |
| } |
| |
| @Override |
| public final void loadFirstRoot(Uri uri) { |
| new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded) |
| .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); |
| } |
| |
| @Override |
| public void loadDocumentsForCurrentStack() { |
| DocumentStack stack = mState.stack; |
| if (!stack.isRecents() && stack.isEmpty()) { |
| DirectoryResult result = new DirectoryResult(); |
| |
| // TODO (b/35996595): Consider plumbing through the actual exception, though it might |
| // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()). |
| result.exception = new IllegalStateException("Failed to load root document."); |
| mInjector.getModel().update(result); |
| return; |
| } |
| |
| mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); |
| } |
| |
| protected final boolean launchToDocument(Uri uri) { |
| // We don't support launching to a document in an archive. |
| if (!Providers.isArchiveUri(uri)) { |
| loadDocument(uri, UserId.DEFAULT_USER, this::onStackLoaded); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void onStackLoaded(@Nullable DocumentStack stack) { |
| if (stack != null) { |
| if (!stack.peek().isDirectory()) { |
| // Requested document is not a directory. Pop it so that we can launch into its |
| // parent. |
| stack.pop(); |
| } |
| mState.stack.reset(stack); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| |
| Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); |
| } else { |
| Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); |
| launchToDefaultLocation(); |
| |
| Metrics.logLaunchAtLocation(mState, null); |
| } |
| } |
| |
| private void onRootLoaded(@Nullable RootInfo root) { |
| boolean invalidRootForAction = |
| (root != null |
| && !root.supportsChildren() |
| && mState.action == State.ACTION_OPEN_TREE); |
| |
| if (invalidRootForAction) { |
| loadDeviceRoot(); |
| } else if (root != null) { |
| mActivity.onRootPicked(root); |
| } else { |
| launchToDefaultLocation(); |
| } |
| } |
| |
| protected abstract void launchToDefaultLocation(); |
| |
| protected void restoreRootAndDirectory() { |
| if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) { |
| mActivity.onRootPicked(mState.stack.getRoot()); |
| } else { |
| mActivity.restoreRootAndDirectory(); |
| } |
| } |
| |
| protected final void loadDeviceRoot() { |
| mActivity.onRootPicked( |
| mProviders.getRootOneshot(UserId.DEFAULT_USER, Providers.AUTHORITY_STORAGE, |
| Providers.ROOT_ID_DEVICE)); |
| } |
| |
| protected final void loadHomeDir() { |
| loadRoot(Shared.getDefaultRootUri(mActivity)); |
| } |
| |
| protected final void loadRecent() { |
| mState.stack.changeRoot(mProviders.getRecentsRoot()); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } |
| |
| protected MutableSelection<String> getStableSelection() { |
| MutableSelection<String> selection = new MutableSelection<>(); |
| mSelectionMgr.copySelection(selection); |
| return selection; |
| } |
| |
| @Override |
| public ActionHandler reset(ContentLock reloadLock) { |
| mContentLock = reloadLock; |
| mActivity.getLoaderManager().destroyLoader(LOADER_ID); |
| return this; |
| } |
| |
| private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { |
| |
| @Override |
| public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { |
| Context context = mActivity; |
| |
| if (mState.stack.isRecents()) { |
| final LockingContentObserver observer = new LockingContentObserver( |
| mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); |
| MultiRootDocumentsLoader loader; |
| |
| if (mSearchMgr.isSearching()) { |
| if (DEBUG) { |
| Log.d(TAG, "Creating new GlobalSearchLoader."); |
| } |
| loader = new GlobalSearchLoader( |
| context, |
| mProviders, |
| mState, |
| mExecutors, |
| mInjector.fileTypeLookup, |
| mSearchMgr.buildQueryArgs()); |
| } else { |
| if (DEBUG) { |
| Log.d(TAG, "Creating new loader recents."); |
| } |
| loader = new RecentsLoader( |
| context, |
| mProviders, |
| mState, |
| mExecutors, |
| mInjector.fileTypeLookup); |
| } |
| loader.setObserver(observer); |
| return loader; |
| } else { |
| Uri contentsUri = mSearchMgr.isSearching() |
| ? DocumentsContract.buildSearchDocumentsUri( |
| mState.stack.getRoot().authority, |
| mState.stack.getRoot().rootId, |
| mSearchMgr.getCurrentSearch()) |
| : DocumentsContract.buildChildDocumentsUri( |
| mState.stack.peek().authority, |
| mState.stack.peek().documentId); |
| |
| final Bundle queryArgs = mSearchMgr.isSearching() |
| ? mSearchMgr.buildQueryArgs() |
| : null; |
| |
| if (mInjector.config.managedModeEnabled(mState.stack)) { |
| contentsUri = DocumentsContract.setManageMode(contentsUri); |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, |
| "Creating new directory loader for: " |
| + DocumentInfo.debugString(mState.stack.peek())); |
| } |
| |
| return new DirectoryLoader( |
| mInjector.features, |
| context, |
| mState, |
| contentsUri, |
| mInjector.fileTypeLookup, |
| mContentLock, |
| queryArgs); |
| } |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { |
| if (DEBUG) { |
| Log.d(TAG, "Loader has finished for: " |
| + DocumentInfo.debugString(mState.stack.peek())); |
| } |
| assert(result != null); |
| |
| mInjector.getModel().update(result); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<DirectoryResult> loader) {} |
| } |
| /** |
| * A class primarily for the support of isolating our tests |
| * from our concrete activity implementations. |
| */ |
| public interface CommonAddons { |
| void restoreRootAndDirectory(); |
| void refreshCurrentRootAndDirectory(@AnimationType int anim); |
| void onRootPicked(RootInfo root); |
| // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity. |
| void onDocumentsPicked(List<DocumentInfo> docs); |
| void onDocumentPicked(DocumentInfo doc); |
| RootInfo getCurrentRoot(); |
| DocumentInfo getCurrentDirectory(); |
| /** |
| * Check whether current directory is root of recent. |
| */ |
| boolean isInRecents(); |
| void setRootsDrawerOpen(boolean open); |
| |
| // TODO: Let navigator listens to State |
| void updateNavigator(); |
| |
| @VisibleForTesting |
| void notifyDirectoryNavigated(Uri docUri); |
| } |
| } |