| /* |
| * 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.picker; |
| |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; |
| import static com.android.documentsui.base.State.ACTION_CREATE; |
| import static com.android.documentsui.base.State.ACTION_GET_CONTENT; |
| import static com.android.documentsui.base.State.ACTION_OPEN; |
| import static com.android.documentsui.base.State.ACTION_OPEN_TREE; |
| import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; |
| |
| import android.app.Activity; |
| import android.app.FragmentManager; |
| import android.content.ClipData; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.content.pm.ResolveInfo; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Parcelable; |
| import android.provider.DocumentsContract; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.documentsui.AbstractActionHandler; |
| import com.android.documentsui.ActivityConfig; |
| import com.android.documentsui.DocumentsAccess; |
| import com.android.documentsui.Injector; |
| import com.android.documentsui.Metrics; |
| import com.android.documentsui.Model; |
| import com.android.documentsui.base.BooleanConsumer; |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.DocumentStack; |
| import com.android.documentsui.base.Features; |
| 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; |
| import com.android.documentsui.picker.ActionHandler.Addons; |
| import com.android.documentsui.queries.SearchViewManager; |
| import com.android.documentsui.roots.ProvidersAccess; |
| import com.android.documentsui.selection.ItemDetailsLookup.ItemDetails; |
| import com.android.documentsui.services.FileOperationService; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.Arrays; |
| import java.util.concurrent.Executor; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Provides {@link PickActivity} action specializations to fragments. |
| */ |
| class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T> { |
| |
| private static final String TAG = "PickerActionHandler"; |
| |
| private final Features mFeatures; |
| private final ActivityConfig mConfig; |
| private final Model mModel; |
| private final LastAccessedStorage mLastAccessed; |
| |
| ActionHandler( |
| T activity, |
| State state, |
| ProvidersAccess providers, |
| DocumentsAccess docs, |
| SearchViewManager searchMgr, |
| Lookup<String, Executor> executors, |
| Injector injector, |
| LastAccessedStorage lastAccessed) { |
| |
| super(activity, state, providers, docs, searchMgr, executors, injector); |
| |
| mConfig = injector.config; |
| mFeatures = injector.features; |
| mModel = injector.getModel(); |
| mLastAccessed = lastAccessed; |
| } |
| |
| @Override |
| public void initLocation(Intent intent) { |
| assert(intent != null); |
| |
| // stack is initialized if it's restored from bundle, which means we're restoring a |
| // previously stored state. |
| if (mState.stack.isInitialized()) { |
| if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); |
| restoreRootAndDirectory(); |
| return; |
| } |
| |
| // We set the activity title in AsyncTask.onPostExecute(). |
| // To prevent talkback from reading aloud the default title, we clear it here. |
| mActivity.setTitle(""); |
| |
| if (launchHomeForCopyDestination(intent)) { |
| if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination."); |
| return; |
| } |
| |
| if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) { |
| if (DEBUG) Log.d(TAG, "Launched to a document."); |
| return; |
| } |
| |
| if (DEBUG) Log.d(TAG, "Load last accessed stack."); |
| loadLastAccessedStack(); |
| } |
| |
| @Override |
| protected void launchToDefaultLocation() { |
| loadLastAccessedStack(); |
| } |
| |
| private boolean launchHomeForCopyDestination(Intent intent) { |
| // As a matter of policy we don't load the last used stack for the copy |
| // destination picker (user is already in Files app). |
| // Consensus was that the experice was too confusing. |
| // In all other cases, where the user is visiting us from another app |
| // we restore the stack as last used from that app. |
| if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) { |
| loadHomeDir(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean launchToDocument(Intent intent) { |
| Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI); |
| if (uri != null) { |
| return launchToDocument(uri); |
| } |
| |
| return false; |
| } |
| |
| private void loadLastAccessedStack() { |
| if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); |
| new LoadLastAccessedStackTask<>( |
| mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded) |
| .execute(); |
| } |
| |
| private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { |
| if (stack == null) { |
| loadDefaultLocation(); |
| } else { |
| mState.stack.reset(stack); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| } |
| } |
| |
| private void loadDefaultLocation() { |
| switch (mState.action) { |
| case ACTION_CREATE: |
| loadHomeDir(); |
| break; |
| case ACTION_GET_CONTENT: |
| case ACTION_OPEN: |
| case ACTION_OPEN_TREE: |
| mState.stack.changeRoot(mProviders.getRecentsRoot()); |
| mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); |
| break; |
| default: |
| throw new UnsupportedOperationException("Unexpected action type: " + mState.action); |
| } |
| } |
| |
| @Override |
| public void showAppDetails(ResolveInfo info) { |
| final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); |
| intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null)); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); |
| mActivity.startActivity(intent); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode); |
| |
| // Only relay back results when not canceled; otherwise stick around to |
| // let the user pick another app/backend. |
| switch (requestCode) { |
| case CODE_FORWARD: |
| onExternalAppResult(resultCode, data); |
| break; |
| default: |
| super.onActivityResult(requestCode, resultCode, data); |
| } |
| } |
| |
| private void onExternalAppResult(int resultCode, Intent data) { |
| if (resultCode != Activity.RESULT_CANCELED) { |
| // Remember that we last picked via external app |
| mLastAccessed.setLastAccessedToExternalApp(mActivity); |
| |
| // Pass back result to original caller |
| mActivity.setResult(resultCode, data, 0); |
| mActivity.finish(); |
| } |
| } |
| |
| @Override |
| public void openInNewWindow(DocumentStack path) { |
| // Open new window support only depends on vanilla Activity, so it is |
| // implemented in our parent class. But we don't support that in |
| // picking. So as a matter of defensiveness, we override that here. |
| throw new UnsupportedOperationException("Can't open in new window"); |
| } |
| |
| @Override |
| public void openRoot(RootInfo root) { |
| Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root); |
| mActivity.onRootPicked(root); |
| } |
| |
| @Override |
| public void openRoot(ResolveInfo info) { |
| Metrics.logAppVisited(mActivity, info); |
| final Intent intent = new Intent(mActivity.getIntent()); |
| intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); |
| intent.setComponent(new ComponentName( |
| info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); |
| mActivity.startActivityForResult(intent, CODE_FORWARD); |
| } |
| |
| @Override |
| public void springOpenDirectory(DocumentInfo doc) { |
| } |
| |
| @Override |
| public boolean openItem(ItemDetails details, @ViewType int type, |
| @ViewType int fallback) { |
| DocumentInfo doc = mModel.getDocument(details.getStableId()); |
| if (doc == null) { |
| Log.w(TAG, |
| "Can't view item. No Document available for modeId: " + details.getStableId()); |
| return false; |
| } |
| |
| if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) { |
| mActivity.onDocumentPicked(doc); |
| mSelectionMgr.clearSelection(); |
| return true; |
| } |
| return false; |
| } |
| |
| void pickDocument(DocumentInfo pickTarget) { |
| assert(pickTarget != null); |
| Uri result; |
| switch (mState.action) { |
| case ACTION_OPEN_TREE: |
| result = DocumentsContract.buildTreeDocumentUri( |
| pickTarget.authority, pickTarget.documentId); |
| break; |
| case ACTION_PICK_COPY_DESTINATION: |
| result = pickTarget.derivedUri; |
| break; |
| default: |
| // Should not be reached |
| throw new IllegalStateException("Invalid mState.action"); |
| } |
| finishPicking(result); |
| } |
| |
| void saveDocument( |
| String mimeType, String displayName, BooleanConsumer inProgressStateListener) { |
| assert(mState.action == ACTION_CREATE); |
| new CreatePickedDocumentTask( |
| mActivity, |
| mDocs, |
| mLastAccessed, |
| mState.stack, |
| mimeType, |
| displayName, |
| inProgressStateListener, |
| this::onPickFinished) |
| .executeOnExecutor(getExecutorForCurrentDirectory()); |
| } |
| |
| // User requested to overwrite a target. If confirmed by user #finishPicking() will be |
| // called. |
| void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) { |
| assert(mState.action == ACTION_CREATE); |
| assert(replaceTarget != null); |
| |
| // Adding a confirmation dialog breaks an inherited CTS test (testCreateExisting), so we |
| // need to add a feature flag to bypass this feature in ARC++ environment. |
| if (mFeatures.isOverwriteConfirmationEnabled()) { |
| mInjector.dialogs.confirmOverwrite(fm, replaceTarget); |
| } else { |
| finishPicking(replaceTarget.derivedUri); |
| } |
| } |
| |
| void finishPicking(Uri... docs) { |
| new SetLastAccessedStackTask( |
| mActivity, |
| mLastAccessed, |
| mState.stack, |
| () -> { |
| onPickFinished(docs); |
| } |
| ) .executeOnExecutor(getExecutorForCurrentDirectory()); |
| } |
| |
| private void onPickFinished(Uri... uris) { |
| if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris)); |
| |
| final Intent intent = new Intent(); |
| if (uris.length == 1) { |
| intent.setData(uris[0]); |
| } else if (uris.length > 1) { |
| final ClipData clipData = new ClipData( |
| null, mState.acceptMimes, new ClipData.Item(uris[0])); |
| for (int i = 1; i < uris.length; i++) { |
| clipData.addItem(new ClipData.Item(uris[i])); |
| } |
| intent.setClipData(clipData); |
| } |
| |
| // TODO: Separate this piece of logic per action. |
| // We don't instantiate different objects for different actions at the first place, so it's |
| // not a easy task to separate this logic cleanly. |
| // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its |
| // inheritance structure. |
| if (mState.action == ACTION_GET_CONTENT) { |
| intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| } else if (mState.action == ACTION_OPEN_TREE) { |
| intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
| | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
| | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
| | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); |
| } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { |
| // Picking a copy destination is only used internally by us, so we |
| // don't need to extend permissions to the caller. |
| intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); |
| intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); |
| } else { |
| intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
| | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
| | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); |
| } |
| |
| mActivity.setResult(Activity.RESULT_OK, intent, 0); |
| mActivity.finish(); |
| } |
| |
| private Executor getExecutorForCurrentDirectory() { |
| final DocumentInfo cwd = mState.stack.peek(); |
| if (cwd != null && cwd.authority != null) { |
| return mExecutors.lookup(cwd.authority); |
| } else { |
| return AsyncTask.THREAD_POOL_EXECUTOR; |
| } |
| } |
| |
| public interface Addons extends CommonAddons { |
| @Override |
| void onDocumentPicked(DocumentInfo doc); |
| |
| /** |
| * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept |
| * this method call in test environment. |
| */ |
| @VisibleForTesting |
| void setResult(int resultCode, Intent result, int notUsed); |
| } |
| } |