| /* |
| * 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 android.app.Activity; |
| import android.content.ClipData; |
| import android.content.Intent; |
| import android.content.pm.ResolveInfo; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Parcelable; |
| import android.provider.DocumentsContract; |
| import android.support.annotation.VisibleForTesting; |
| import android.util.Log; |
| |
| import com.android.documentsui.AbstractActionHandler.CommonAddons; |
| import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; |
| import com.android.documentsui.archives.ArchivesProvider; |
| 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.RootInfo; |
| import com.android.documentsui.base.Shared; |
| import com.android.documentsui.base.State; |
| import com.android.documentsui.dirlist.AnimationView.AnimationType; |
| import com.android.documentsui.dirlist.AnimationView; |
| import com.android.documentsui.dirlist.DocumentDetails; |
| import com.android.documentsui.dirlist.DocumentsAdapter; |
| import com.android.documentsui.dirlist.FocusHandler; |
| import com.android.documentsui.dirlist.Model; |
| import com.android.documentsui.files.LauncherActivity; |
| import com.android.documentsui.queries.SearchViewManager; |
| import com.android.documentsui.roots.LoadRootTask; |
| import com.android.documentsui.roots.RootsAccess; |
| import com.android.documentsui.selection.Selection; |
| import com.android.documentsui.selection.SelectionManager; |
| import com.android.documentsui.sidebar.EjectRootTask; |
| |
| 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 (viewDocument etc.) to the host activity. |
| */ |
| public abstract class AbstractActionHandler<T extends Activity & CommonAddons> |
| implements ActionHandler { |
| |
| private static final String TAG = "AbstractActionHandler"; |
| private static final int REFRESH_SPINNER_TIMEOUT = 500; |
| |
| protected final T mActivity; |
| protected final State mState; |
| protected final RootsAccess mRoots; |
| protected final DocumentsAccess mDocs; |
| protected final FocusHandler mFocusHandler; |
| protected final SelectionManager mSelectionMgr; |
| protected final SearchViewManager mSearchMgr; |
| protected final Lookup<String, Executor> mExecutors; |
| protected final Injector mInjector; |
| |
| private Runnable mDisplayStateChangedListener; |
| |
| @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, |
| RootsAccess roots, |
| DocumentsAccess docs, |
| SearchViewManager searchMgr, |
| Lookup<String, Executor> executors, |
| Injector injector) { |
| |
| assert(activity != null); |
| assert(state != null); |
| assert(roots != null); |
| assert(searchMgr != null); |
| assert(docs != null); |
| assert(injector != null); |
| |
| mActivity = activity; |
| mState = state; |
| mRoots = roots; |
| mDocs = docs; |
| mFocusHandler = injector.focusManager; |
| mSelectionMgr = injector.selectionMgr; |
| mSearchMgr = searchMgr; |
| mExecutors = executors; |
| mInjector = injector; |
| } |
| |
| @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 refreshDocument(DocumentInfo doc, BooleanConsumer callback) { |
| RefreshTask task = new RefreshTask(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(mActivity, Metrics.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 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(ClipData data, 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 selectAllFiles() { |
| Metrics.logUserAction(mActivity, Metrics.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 |
| @Nullable |
| public DocumentInfo renameDocument(String name, DocumentInfo document) { |
| throw new UnsupportedOperationException("Can't rename documents."); |
| } |
| |
| @Override |
| public boolean viewDocument(DocumentDetails doc) { |
| throw new UnsupportedOperationException("Direct view not supported!"); |
| } |
| |
| @Override |
| public boolean previewDocument(DocumentDetails doc) { |
| throw new UnsupportedOperationException("Preview not supported!"); |
| } |
| |
| @Override |
| public void showChooserForDoc(DocumentInfo doc) { |
| throw new UnsupportedOperationException("Show chooser for doc not supported!"); |
| } |
| |
| @Override |
| public void openContainerDocument(DocumentInfo doc) { |
| assert(doc.isContainer()); |
| |
| if (mSearchMgr.isSearching()) { |
| loadDocument( |
| doc.derivedUri, |
| (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); |
| } else { |
| openChildContainer(doc); |
| } |
| } |
| |
| 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()); |
| } |
| |
| 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); |
| } |
| |
| assert(currentDoc != null); |
| 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 cutToClipboard() { |
| throw new UnsupportedOperationException("Cut not supported!"); |
| } |
| |
| @Override |
| public void copyToClipboard() { |
| throw new UnsupportedOperationException("Copy not supported!"); |
| } |
| |
| @Override |
| public void deleteSelectedDocuments() { |
| throw new UnsupportedOperationException("Delete not supported!"); |
| } |
| |
| @Override |
| public void shareSelectedDocuments() { |
| throw new UnsupportedOperationException("Share not supported!"); |
| } |
| |
| protected final void loadDocument(Uri uri, LoadDocStackCallback callback) { |
| new LoadDocStackTask( |
| mActivity, |
| mRoots, |
| mDocs, |
| callback |
| ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); |
| } |
| |
| @Override |
| public final void loadRoot(Uri uri) { |
| new LoadRootTask<>(mActivity, mRoots, mState, uri) |
| .executeOnExecutor(mExecutors.lookup(uri.getAuthority())); |
| } |
| |
| protected final boolean launchToDocument(Uri uri) { |
| // We don't support launching to a document in an archive. |
| if (!ArchivesProvider.AUTHORITY.equals(uri.getAuthority())) { |
| loadDocument(uri, 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(mActivity, mState, stack.getRoot().getUri()); |
| } else { |
| Log.w(TAG, "Failed to launch into the given uri. Launch to default location."); |
| launchToDefaultLocation(); |
| |
| Metrics.logLaunchAtLocation(mActivity, mState, null); |
| } |
| } |
| |
| protected abstract void launchToDefaultLocation(); |
| |
| protected final void loadHomeDir() { |
| loadRoot(Shared.getDefaultRootUri(mActivity)); |
| } |
| |
| protected Selection getStableSelection() { |
| return mSelectionMgr.getSelection(new Selection()); |
| } |
| |
| /** |
| * A class primarily for the support of isolating our tests |
| * from our concrete activity implementations. |
| */ |
| public interface CommonAddons { |
| 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(); |
| void setRootsDrawerOpen(boolean open); |
| |
| // TODO: Let navigator listens to State |
| void updateNavigator(); |
| |
| @VisibleForTesting |
| void notifyDirectoryNavigated(Uri docUri); |
| } |
| } |