/* * 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.ActivityNotFoundException; 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.MimeTypes; 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.files.QuickViewIntentBuilder; 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.DialogController; 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.concurrent.Semaphore; 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 implements ActionHandler { @VisibleForTesting 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; private final Semaphore mLoaderSemaphore = new Semaphore(1); protected final T mActivity; protected final State mState; protected final ProvidersAccess mProviders; protected final DocumentsAccess mDocs; protected final FocusHandler mFocusHandler; protected final SelectionTracker mSelectionMgr; protected final SearchViewManager mSearchMgr; protected final Lookup mExecutors; protected final DialogController mDialogs; protected final Model mModel; 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 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; mDialogs = injector.dialogs; mModel = injector.getModel(); 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 requestQuietModeDisabled(RootInfo info, UserId userId) { new RequestQuietModeDisabledTask(mActivity, userId).execute(); } @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 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 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, UserId userId) { throw new UnsupportedOperationException("Can't open an app."); } @Override public void showAppDetails(ResolveInfo info, UserId userId) { 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 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); } } // TODO: Make this private and make tests call interface method instead. /** * Behavior when a document is opened. */ @VisibleForTesting public void onDocumentOpened(DocumentInfo doc, @ViewType int type, @ViewType int fallback, boolean fromPicker) { // In picker mode, don't access archive container to avoid pick file in archive files. if (doc.isContainer() && !fromPicker) { openContainerDocument(doc); return; } if (manageDocument(doc)) { return; } // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow // PackageManager to install it. This allows users to install APKs from any root. // The Downloads special case is handled above in #manageDocument. if (MimeTypes.isApkType(doc.mimeType)) { viewDocument(doc); return; } switch (type) { case VIEW_TYPE_REGULAR: if (viewDocument(doc)) { return; } break; case VIEW_TYPE_PREVIEW: if (previewDocument(doc, fromPicker)) { return; } break; default: throw new IllegalArgumentException("Illegal view type."); } switch (fallback) { case VIEW_TYPE_REGULAR: if (viewDocument(doc)) { return; } break; case VIEW_TYPE_PREVIEW: if (previewDocument(doc, fromPicker)) { return; } break; case VIEW_TYPE_NONE: break; default: throw new IllegalArgumentException("Illegal fallback view type."); } // Failed to view including fallback, and it's in an archive. if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) { mDialogs.showViewInArchivesUnsupported(); } } private boolean viewDocument(DocumentInfo doc) { if (doc.isPartial()) { Log.w(TAG, "Can't view partial file."); return false; } if (doc.isInArchive()) { Log.w(TAG, "Can't view files in archives."); return false; } if (doc.isDirectory()) { Log.w(TAG, "Can't view directories."); return true; } Intent intent = buildViewIntent(doc); if (DEBUG && intent.getClipData() != null) { Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData()); } try { doc.userId.startActivityAsUser(mActivity, intent); return true; } catch (ActivityNotFoundException e) { mDialogs.showNoApplicationFound(); } return false; } private boolean previewDocument(DocumentInfo doc, boolean fromPicker) { if (doc.isPartial()) { Log.w(TAG, "Can't view partial file."); return false; } Intent intent = new QuickViewIntentBuilder( mActivity, mActivity.getResources(), doc, mModel, fromPicker).build(); if (intent != null) { // TODO: un-work around issue b/24963914. Should be fixed soon. try { doc.userId.startActivityAsUser(mActivity, intent); return true; } catch (SecurityException e) { // Carry on to regular view mode. Log.e(TAG, "Caught security error: " + e.getLocalizedMessage()); } } return false; } protected boolean manageDocument(DocumentInfo doc) { if (isManagedDownload(doc)) { // First try managing the document; we expect manager to filter // based on authority, so we don't grant. Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); manage.setData(doc.getDocumentUri()); try { doc.userId.startActivityAsUser(mActivity, manage); return true; } catch (ActivityNotFoundException ex) { // Fall back to regular handling. } } return false; } private boolean isManagedDownload(DocumentInfo doc) { // Anything on downloads goes through the back through downloads manager // (that's the MANAGE_DOCUMENT bit). // This is done for two reasons: // 1) The file in question might be a failed/queued or otherwise have some // specialized download handling. // 2) For APKs, the download manager will add on some important security stuff // like origin URL. // 3) For partial files, the download manager will offer to restart/retry downloads. // All other files not on downloads, event APKs, would get no benefit from this // treatment, thusly the "isDownloads" check. // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for // files in archives or in child folders. Also, if the activity is already browsing // a ZIP from downloads, then skip MANAGE_DOCUMENTS. if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction()) && mState.stack.size() > 1) { // viewing the contents of an archive. return false; } // management is only supported in Downloads root or downloaded files show in Recent root. if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) { // only on APKs or partial files. return MimeTypes.isApkType(doc.mimeType) || doc.isPartial(); } return false; } protected Intent buildViewIntent(DocumentInfo doc) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(doc.getDocumentUri(), doc.mimeType); // Downloads has traditionally added the WRITE permission // in the TrampolineActivity. Since this behavior is long // established, we set the same permission for non-managed files // This ensures consistent behavior between the Downloads root // and other roots. int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP; if (doc.isWriteSupported()) { flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } intent.setFlags(flags); return intent; } @Override public boolean previewItem(ItemDetails 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())) { // It is now possible when opening cross-profile folder. 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 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 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, UserId userId) { new LoadRootTask<>(mActivity, mProviders, uri, userId, this::onRootLoaded) .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); } @Override public final void loadCrossProfileRoot(RootInfo info, UserId selectedUser) { if (info.isRecents()) { openRoot(mProviders.getRecentsRoot(selectedUser)); return; } new LoadRootTask<>(mActivity, mProviders, info.getUri(), selectedUser, new LoadCrossProfileRootCallback(info, selectedUser)) .executeOnExecutor(mExecutors.lookup(info.getUri().getAuthority())); } private class LoadCrossProfileRootCallback implements LoadRootTask.LoadRootCallback { private final RootInfo mOriginalRoot; private final UserId mSelectedUserId; LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser) { mOriginalRoot = rootInfo; mSelectedUserId = selectedUser; } @Override public void onRootLoaded(@Nullable RootInfo root) { if (root == null) { // There is no such root in the other profile. Maybe the provider is missing on // the other profile. Create a placeholder root and open it to show error message. root = RootInfo.copyRootInfo(mOriginalRoot); root.userId = mSelectedUserId; } openRoot(root); } } @Override public final void loadFirstRoot(Uri uri) { new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded) .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); } @Override public void loadDocumentsForCurrentStack() { // mState.stack may be empty when we cannot load the root document. // However, we still want to restart loader because we may need to perform search in a // cross-profile scenario. // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op. // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null. // Only allow restartLoader when the previous loader is finished or reset. Allowing // multiple consecutive calls to restartLoader() / onCreateLoader() will probably create // multiple active loaders, because restartLoader() does not interrupt previous loaders' // loading, therefore may block the UI thread and cause ANR. if (mLoaderSemaphore.tryAcquire()) { mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings); } } protected final boolean launchToDocument(Uri uri) { if (DEBUG) { Log.d(TAG, "launchToDocument() uri=" + uri); } // We don't support launching to a document in an archive. if (Providers.isArchiveUri(uri)) { return false; } loadDocument(uri, UserId.DEFAULT_USER, this::onStackToLaunchToLoaded); return true; } /** * Invoked only once, when the initial stack (that is the stack we are going to * "launch to") is loaded. * * @see #launchToDocument(Uri) */ private void onStackToLaunchToLoaded(@Nullable DocumentStack stack) { if (DEBUG) { Log.d(TAG, "onLaunchStackLoaded() stack=" + stack); } if (stack == null) { Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); launchToDefaultLocation(); Metrics.logLaunchAtLocation(mState, null); return; } // Make sure the document at the top of the stack is a directory (if it isn't - just pop // one off). if (!stack.peek().isDirectory()) { stack.pop(); } mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); Metrics.logLaunchAtLocation(mState, stack.getRoot().getUri()); } 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() { loadRoot(DocumentsContract.buildRootUri(Providers.AUTHORITY_STORAGE, Providers.ROOT_ID_DEVICE), UserId.DEFAULT_USER); } protected final void loadHomeDir() { loadRoot(Shared.getDefaultRootUri(mActivity), UserId.DEFAULT_USER); } protected final void loadRecent() { mState.stack.changeRoot(mProviders.getRecentsRoot(UserId.DEFAULT_USER)); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); } protected MutableSelection getStableSelection() { MutableSelection 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 { @Override public Loader 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(), mState.stack.getRoot().userId); } else { if (DEBUG) { Log.d(TAG, "Creating new loader recents."); } loader = new RecentsLoader( context, mProviders, mState, mExecutors, mInjector.fileTypeLookup, mState.stack.getRoot().userId); } loader.setObserver(observer); return loader; } else { // There maybe no root docInfo DocumentInfo rootDoc = mState.stack.peek(); String authority = rootDoc == null ? mState.stack.getRoot().authority : rootDoc.authority; String documentId = rootDoc == null ? mState.stack.getRoot().documentId : rootDoc.documentId; Uri contentsUri = mSearchMgr.isSearching() ? DocumentsContract.buildSearchDocumentsUri( mState.stack.getRoot().authority, mState.stack.getRoot().rootId, mSearchMgr.getCurrentSearch()) : DocumentsContract.buildChildDocumentsUri( authority, 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 loader, DirectoryResult result) { if (DEBUG) { Log.d(TAG, "Loader has finished for: " + DocumentInfo.debugString(mState.stack.peek())); } assert (result != null); mInjector.getModel().update(result); mLoaderSemaphore.release(); } @Override public void onLoaderReset(Loader loader) { mLoaderSemaphore.release(); } } /** * 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 docs); void onDocumentPicked(DocumentInfo doc); RootInfo getCurrentRoot(); DocumentInfo getCurrentDirectory(); UserId getSelectedUser(); /** * Check whether current directory is root of recent. */ boolean isInRecents(); void setRootsDrawerOpen(boolean open); /** * Set the locked status of the DrawerController. */ void setRootsDrawerLocked(boolean locked); // TODO: Let navigator listens to State void updateNavigator(); @VisibleForTesting void notifyDirectoryNavigated(Uri docUri); } }