Home | History | Annotate | Download | only in picker
      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.picker;
     18 
     19 import static com.android.documentsui.base.SharedMinimal.DEBUG;
     20 import static com.android.documentsui.base.State.ACTION_CREATE;
     21 import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
     22 import static com.android.documentsui.base.State.ACTION_OPEN;
     23 import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
     24 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
     25 
     26 import android.app.Activity;
     27 import android.app.FragmentManager;
     28 import android.content.ClipData;
     29 import android.content.ComponentName;
     30 import android.content.Intent;
     31 import android.content.pm.ResolveInfo;
     32 import android.net.Uri;
     33 import android.os.AsyncTask;
     34 import android.os.Parcelable;
     35 import android.provider.DocumentsContract;
     36 import android.provider.Settings;
     37 import android.util.Log;
     38 
     39 import com.android.documentsui.AbstractActionHandler;
     40 import com.android.documentsui.ActivityConfig;
     41 import com.android.documentsui.DocumentsAccess;
     42 import com.android.documentsui.Injector;
     43 import com.android.documentsui.Metrics;
     44 import com.android.documentsui.Model;
     45 import com.android.documentsui.base.BooleanConsumer;
     46 import com.android.documentsui.base.DocumentInfo;
     47 import com.android.documentsui.base.DocumentStack;
     48 import com.android.documentsui.base.Features;
     49 import com.android.documentsui.base.Lookup;
     50 import com.android.documentsui.base.RootInfo;
     51 import com.android.documentsui.base.Shared;
     52 import com.android.documentsui.base.State;
     53 import com.android.documentsui.dirlist.AnimationView;
     54 import com.android.documentsui.picker.ActionHandler.Addons;
     55 import com.android.documentsui.queries.SearchViewManager;
     56 import com.android.documentsui.roots.ProvidersAccess;
     57 import com.android.documentsui.selection.ItemDetailsLookup.ItemDetails;
     58 import com.android.documentsui.services.FileOperationService;
     59 import com.android.internal.annotations.VisibleForTesting;
     60 
     61 import java.util.Arrays;
     62 import java.util.concurrent.Executor;
     63 
     64 import javax.annotation.Nullable;
     65 
     66 /**
     67  * Provides {@link PickActivity} action specializations to fragments.
     68  */
     69 class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T> {
     70 
     71     private static final String TAG = "PickerActionHandler";
     72 
     73     private final Features mFeatures;
     74     private final ActivityConfig mConfig;
     75     private final Model mModel;
     76     private final LastAccessedStorage mLastAccessed;
     77 
     78     ActionHandler(
     79             T activity,
     80             State state,
     81             ProvidersAccess providers,
     82             DocumentsAccess docs,
     83             SearchViewManager searchMgr,
     84             Lookup<String, Executor> executors,
     85             Injector injector,
     86             LastAccessedStorage lastAccessed) {
     87 
     88         super(activity, state, providers, docs, searchMgr, executors, injector);
     89 
     90         mConfig = injector.config;
     91         mFeatures = injector.features;
     92         mModel = injector.getModel();
     93         mLastAccessed = lastAccessed;
     94     }
     95 
     96     @Override
     97     public void initLocation(Intent intent) {
     98         assert(intent != null);
     99 
    100         // stack is initialized if it's restored from bundle, which means we're restoring a
    101         // previously stored state.
    102         if (mState.stack.isInitialized()) {
    103             if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
    104             restoreRootAndDirectory();
    105             return;
    106         }
    107 
    108         // We set the activity title in AsyncTask.onPostExecute().
    109         // To prevent talkback from reading aloud the default title, we clear it here.
    110         mActivity.setTitle("");
    111 
    112         if (launchHomeForCopyDestination(intent)) {
    113             if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination.");
    114             return;
    115         }
    116 
    117         if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) {
    118             if (DEBUG) Log.d(TAG, "Launched to a document.");
    119             return;
    120         }
    121 
    122         if (DEBUG) Log.d(TAG, "Load last accessed stack.");
    123         loadLastAccessedStack();
    124     }
    125 
    126     @Override
    127     protected void launchToDefaultLocation() {
    128         loadLastAccessedStack();
    129     }
    130 
    131     private boolean launchHomeForCopyDestination(Intent intent) {
    132         // As a matter of policy we don't load the last used stack for the copy
    133         // destination picker (user is already in Files app).
    134         // Consensus was that the experice was too confusing.
    135         // In all other cases, where the user is visiting us from another app
    136         // we restore the stack as last used from that app.
    137         if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
    138             loadHomeDir();
    139             return true;
    140         }
    141 
    142         return false;
    143     }
    144 
    145     private boolean launchToDocument(Intent intent) {
    146         Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
    147         if (uri != null) {
    148             return launchToDocument(uri);
    149         }
    150 
    151         return false;
    152     }
    153 
    154     private void loadLastAccessedStack() {
    155         if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
    156         new LoadLastAccessedStackTask<>(
    157                 mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded)
    158                 .execute();
    159     }
    160 
    161     private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) {
    162         if (stack == null) {
    163             loadDefaultLocation();
    164         } else {
    165             mState.stack.reset(stack);
    166             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
    167         }
    168     }
    169 
    170     private void loadDefaultLocation() {
    171         switch (mState.action) {
    172             case ACTION_CREATE:
    173                 loadHomeDir();
    174                 break;
    175             case ACTION_GET_CONTENT:
    176             case ACTION_OPEN:
    177             case ACTION_OPEN_TREE:
    178                 mState.stack.changeRoot(mProviders.getRecentsRoot());
    179                 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
    180                 break;
    181             default:
    182                 throw new UnsupportedOperationException("Unexpected action type: " + mState.action);
    183         }
    184     }
    185 
    186     @Override
    187     public void showAppDetails(ResolveInfo info) {
    188         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    189         intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null));
    190         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
    191         mActivity.startActivity(intent);
    192     }
    193 
    194     @Override
    195     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    196         if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode);
    197 
    198         // Only relay back results when not canceled; otherwise stick around to
    199         // let the user pick another app/backend.
    200         switch (requestCode) {
    201             case CODE_FORWARD:
    202                 onExternalAppResult(resultCode, data);
    203                 break;
    204             default:
    205                 super.onActivityResult(requestCode, resultCode, data);
    206         }
    207     }
    208 
    209     private void onExternalAppResult(int resultCode, Intent data) {
    210         if (resultCode != Activity.RESULT_CANCELED) {
    211             // Remember that we last picked via external app
    212             mLastAccessed.setLastAccessedToExternalApp(mActivity);
    213 
    214             // Pass back result to original caller
    215             mActivity.setResult(resultCode, data, 0);
    216             mActivity.finish();
    217         }
    218     }
    219 
    220     @Override
    221     public void openInNewWindow(DocumentStack path) {
    222         // Open new window support only depends on vanilla Activity, so it is
    223         // implemented in our parent class. But we don't support that in
    224         // picking. So as a matter of defensiveness, we override that here.
    225         throw new UnsupportedOperationException("Can't open in new window");
    226     }
    227 
    228     @Override
    229     public void openRoot(RootInfo root) {
    230         Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root);
    231         mActivity.onRootPicked(root);
    232     }
    233 
    234     @Override
    235     public void openRoot(ResolveInfo info) {
    236         Metrics.logAppVisited(mActivity, info);
    237         final Intent intent = new Intent(mActivity.getIntent());
    238         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
    239         intent.setComponent(new ComponentName(
    240                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
    241         mActivity.startActivityForResult(intent, CODE_FORWARD);
    242     }
    243 
    244     @Override
    245     public void springOpenDirectory(DocumentInfo doc) {
    246     }
    247 
    248     @Override
    249     public boolean openItem(ItemDetails details, @ViewType int type,
    250             @ViewType int fallback) {
    251         DocumentInfo doc = mModel.getDocument(details.getStableId());
    252         if (doc == null) {
    253             Log.w(TAG,
    254                     "Can't view item. No Document available for modeId: " + details.getStableId());
    255             return false;
    256         }
    257 
    258         if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
    259             mActivity.onDocumentPicked(doc);
    260             mSelectionMgr.clearSelection();
    261             return true;
    262         }
    263         return false;
    264     }
    265 
    266     void pickDocument(DocumentInfo pickTarget) {
    267         assert(pickTarget != null);
    268         Uri result;
    269         switch (mState.action) {
    270             case ACTION_OPEN_TREE:
    271                 result = DocumentsContract.buildTreeDocumentUri(
    272                         pickTarget.authority, pickTarget.documentId);
    273                 break;
    274             case ACTION_PICK_COPY_DESTINATION:
    275                 result = pickTarget.derivedUri;
    276                 break;
    277             default:
    278                 // Should not be reached
    279                 throw new IllegalStateException("Invalid mState.action");
    280         }
    281         finishPicking(result);
    282     }
    283 
    284     void saveDocument(
    285             String mimeType, String displayName, BooleanConsumer inProgressStateListener) {
    286         assert(mState.action == ACTION_CREATE);
    287         new CreatePickedDocumentTask(
    288                 mActivity,
    289                 mDocs,
    290                 mLastAccessed,
    291                 mState.stack,
    292                 mimeType,
    293                 displayName,
    294                 inProgressStateListener,
    295                 this::onPickFinished)
    296                 .executeOnExecutor(getExecutorForCurrentDirectory());
    297     }
    298 
    299     // User requested to overwrite a target. If confirmed by user #finishPicking() will be
    300     // called.
    301     void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) {
    302         assert(mState.action == ACTION_CREATE);
    303         assert(replaceTarget != null);
    304 
    305         // Adding a confirmation dialog breaks an inherited CTS test (testCreateExisting), so we
    306         // need to add a feature flag to bypass this feature in ARC++ environment.
    307         if (mFeatures.isOverwriteConfirmationEnabled()) {
    308             mInjector.dialogs.confirmOverwrite(fm, replaceTarget);
    309         } else {
    310             finishPicking(replaceTarget.derivedUri);
    311         }
    312     }
    313 
    314     void finishPicking(Uri... docs) {
    315         new SetLastAccessedStackTask(
    316                 mActivity,
    317                 mLastAccessed,
    318                 mState.stack,
    319                 () -> {
    320                     onPickFinished(docs);
    321                 }
    322         ) .executeOnExecutor(getExecutorForCurrentDirectory());
    323     }
    324 
    325     private void onPickFinished(Uri... uris) {
    326         if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
    327 
    328         final Intent intent = new Intent();
    329         if (uris.length == 1) {
    330             intent.setData(uris[0]);
    331         } else if (uris.length > 1) {
    332             final ClipData clipData = new ClipData(
    333                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
    334             for (int i = 1; i < uris.length; i++) {
    335                 clipData.addItem(new ClipData.Item(uris[i]));
    336             }
    337             intent.setClipData(clipData);
    338         }
    339 
    340         // TODO: Separate this piece of logic per action.
    341         // We don't instantiate different objects for different actions at the first place, so it's
    342         // not a easy task to separate this logic cleanly.
    343         // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
    344         // inheritance structure.
    345         if (mState.action == ACTION_GET_CONTENT) {
    346             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    347         } else if (mState.action == ACTION_OPEN_TREE) {
    348             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
    349                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    350                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    351                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
    352         } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
    353             // Picking a copy destination is only used internally by us, so we
    354             // don't need to extend permissions to the caller.
    355             intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
    356             intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
    357         } else {
    358             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
    359                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    360                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    361         }
    362 
    363         mActivity.setResult(Activity.RESULT_OK, intent, 0);
    364         mActivity.finish();
    365     }
    366 
    367     private Executor getExecutorForCurrentDirectory() {
    368         final DocumentInfo cwd = mState.stack.peek();
    369         if (cwd != null && cwd.authority != null) {
    370             return mExecutors.lookup(cwd.authority);
    371         } else {
    372             return AsyncTask.THREAD_POOL_EXECUTOR;
    373         }
    374     }
    375 
    376     public interface Addons extends CommonAddons {
    377         @Override
    378         void onDocumentPicked(DocumentInfo doc);
    379 
    380         /**
    381          * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept
    382          * this method call in test environment.
    383          */
    384         @VisibleForTesting
    385         void setResult(int resultCode, Intent result, int notUsed);
    386     }
    387 }
    388