blob: 4407a62b2a42e912dc89784acf3329020ed77758 [file] [log] [blame]
/*
* 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<T extends FragmentActivity & CommonAddons>
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<String> mSelectionMgr;
protected final SearchViewManager mSearchMgr;
protected final Lookup<String, Executor> 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<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;
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<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, 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<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);
}
}
// 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<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())) {
// 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<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, 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 <b>only</b> 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<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(),
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<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);
mLoaderSemaphore.release();
}
@Override
public void onLoaderReset(Loader<DirectoryResult> 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<DocumentInfo> 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);
}
}