• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Shared.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.base.BooleanConsumer;
45 import com.android.documentsui.base.DocumentInfo;
46 import com.android.documentsui.base.DocumentStack;
47 import com.android.documentsui.base.Features;
48 import com.android.documentsui.base.Lookup;
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.DocumentDetails;
54 import com.android.documentsui.Model;
55 import com.android.documentsui.picker.ActionHandler.Addons;
56 import com.android.documentsui.queries.SearchViewManager;
57 import com.android.documentsui.roots.ProvidersAccess;
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 
ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector injector, LastAccessedStorage lastAccessed)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
initLocation(Intent intent)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
launchToDefaultLocation()127     protected void launchToDefaultLocation() {
128         loadLastAccessedStack();
129     }
130 
launchHomeForCopyDestination(Intent intent)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 
launchToDocument(Intent intent)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 
loadLastAccessedStack()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 
onLastAccessedStackLoaded(@ullable DocumentStack stack)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 
loadDefaultLocation()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
showAppDetails(ResolveInfo info)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
onActivityResult(int requestCode, int resultCode, Intent data)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 
onExternalAppResult(int resultCode, Intent data)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
openInNewWindow(DocumentStack path)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
openRoot(RootInfo root)229     public void openRoot(RootInfo root) {
230         Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root);
231         mActivity.onRootPicked(root);
232     }
233 
234     @Override
openRoot(ResolveInfo info)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
springOpenDirectory(DocumentInfo doc)245     public void springOpenDirectory(DocumentInfo doc) {
246     }
247 
248     @Override
openDocument(DocumentDetails details, @ViewType int type, @ViewType int fallback)249     public boolean openDocument(DocumentDetails details, @ViewType int type,
250             @ViewType int fallback) {
251         DocumentInfo doc = mModel.getDocument(details.getModelId());
252         if (doc == null) {
253             Log.w(TAG,
254                     "Can't view item. No Document available for modeId: " + details.getModelId());
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 
pickDocument(DocumentInfo pickTarget)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 
saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener)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.
saveDocument(FragmentManager fm, DocumentInfo replaceTarget)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 
finishPicking(Uri... docs)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 
onPickFinished(Uri... uris)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 
getExecutorForCurrentDirectory()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 {
onDocumentPicked(DocumentInfo doc)377         void onDocumentPicked(DocumentInfo doc);
378 
379         /**
380          * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept
381          * this method call in test environment.
382          */
383         @VisibleForTesting
setResult(int resultCode, Intent result, int notUsed)384         void setResult(int resultCode, Intent result, int notUsed);
385     }
386 }
387