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