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             return;
105         }
106 
107         // We set the activity title in AsyncTask.onPostExecute().
108         // To prevent talkback from reading aloud the default title, we clear it here.
109         mActivity.setTitle("");
110 
111         if (launchHomeForCopyDestination(intent)) {
112             if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination.");
113             return;
114         }
115 
116         if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) {
117             if (DEBUG) Log.d(TAG, "Launched to a document.");
118             return;
119         }
120 
121         if (DEBUG) Log.d(TAG, "Load last accessed stack.");
122         loadLastAccessedStack();
123     }
124 
125     @Override
launchToDefaultLocation()126     protected void launchToDefaultLocation() {
127         loadLastAccessedStack();
128     }
129 
launchHomeForCopyDestination(Intent intent)130     private boolean launchHomeForCopyDestination(Intent intent) {
131         // As a matter of policy we don't load the last used stack for the copy
132         // destination picker (user is already in Files app).
133         // Consensus was that the experice was too confusing.
134         // In all other cases, where the user is visiting us from another app
135         // we restore the stack as last used from that app.
136         if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
137             loadHomeDir();
138             return true;
139         }
140 
141         return false;
142     }
143 
launchToDocument(Intent intent)144     private boolean launchToDocument(Intent intent) {
145         Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
146         if (uri != null) {
147             return launchToDocument(uri);
148         }
149 
150         return false;
151     }
152 
loadLastAccessedStack()153     private void loadLastAccessedStack() {
154         if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
155         new LoadLastAccessedStackTask<>(
156                 mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded)
157                 .execute();
158     }
159 
onLastAccessedStackLoaded(@ullable DocumentStack stack)160     private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) {
161         if (stack == null) {
162             loadDefaultLocation();
163         } else {
164             mState.stack.reset(stack);
165             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
166         }
167     }
168 
loadDefaultLocation()169     private void loadDefaultLocation() {
170         switch (mState.action) {
171             case ACTION_CREATE:
172                 loadHomeDir();
173                 break;
174             case ACTION_GET_CONTENT:
175             case ACTION_OPEN:
176             case ACTION_OPEN_TREE:
177                 mState.stack.changeRoot(mProviders.getRecentsRoot());
178                 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
179                 break;
180             default:
181                 throw new UnsupportedOperationException("Unexpected action type: " + mState.action);
182         }
183     }
184 
185     @Override
showAppDetails(ResolveInfo info)186     public void showAppDetails(ResolveInfo info) {
187         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
188         intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null));
189         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
190         mActivity.startActivity(intent);
191     }
192 
193     @Override
onActivityResult(int requestCode, int resultCode, Intent data)194     public void onActivityResult(int requestCode, int resultCode, Intent data) {
195         if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode);
196 
197         // Only relay back results when not canceled; otherwise stick around to
198         // let the user pick another app/backend.
199         switch (requestCode) {
200             case CODE_FORWARD:
201                 onExternalAppResult(resultCode, data);
202                 break;
203             default:
204                 super.onActivityResult(requestCode, resultCode, data);
205         }
206     }
207 
onExternalAppResult(int resultCode, Intent data)208     private void onExternalAppResult(int resultCode, Intent data) {
209         if (resultCode != Activity.RESULT_CANCELED) {
210             // Remember that we last picked via external app
211             mLastAccessed.setLastAccessedToExternalApp(mActivity);
212 
213             // Pass back result to original caller
214             mActivity.setResult(resultCode, data, 0);
215             mActivity.finish();
216         }
217     }
218 
219     @Override
openInNewWindow(DocumentStack path)220     public void openInNewWindow(DocumentStack path) {
221         // Open new window support only depends on vanilla Activity, so it is
222         // implemented in our parent class. But we don't support that in
223         // picking. So as a matter of defensiveness, we override that here.
224         throw new UnsupportedOperationException("Can't open in new window");
225     }
226 
227     @Override
openRoot(RootInfo root)228     public void openRoot(RootInfo root) {
229         Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root);
230         mActivity.onRootPicked(root);
231     }
232 
233     @Override
openRoot(ResolveInfo info)234     public void openRoot(ResolveInfo info) {
235         Metrics.logAppVisited(mActivity, info);
236         final Intent intent = new Intent(mActivity.getIntent());
237         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
238         intent.setComponent(new ComponentName(
239                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
240         mActivity.startActivityForResult(intent, CODE_FORWARD);
241     }
242 
243     @Override
springOpenDirectory(DocumentInfo doc)244     public void springOpenDirectory(DocumentInfo doc) {
245     }
246 
247     @Override
openDocument(DocumentDetails details, @ViewType int type, @ViewType int fallback)248     public boolean openDocument(DocumentDetails details, @ViewType int type,
249             @ViewType int fallback) {
250         DocumentInfo doc = mModel.getDocument(details.getModelId());
251         if (doc == null) {
252             Log.w(TAG,
253                     "Can't view item. No Document available for modeId: " + details.getModelId());
254             return false;
255         }
256 
257         if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
258             mActivity.onDocumentPicked(doc);
259             mSelectionMgr.clearSelection();
260             return true;
261         }
262         return false;
263     }
264 
pickDocument(DocumentInfo pickTarget)265     void pickDocument(DocumentInfo pickTarget) {
266         assert(pickTarget != null);
267         Uri result;
268         switch (mState.action) {
269             case ACTION_OPEN_TREE:
270                 result = DocumentsContract.buildTreeDocumentUri(
271                         pickTarget.authority, pickTarget.documentId);
272                 break;
273             case ACTION_PICK_COPY_DESTINATION:
274                 result = pickTarget.derivedUri;
275                 break;
276             default:
277                 // Should not be reached
278                 throw new IllegalStateException("Invalid mState.action");
279         }
280         finishPicking(result);
281     }
282 
saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener)283     void saveDocument(
284             String mimeType, String displayName, BooleanConsumer inProgressStateListener) {
285         assert(mState.action == ACTION_CREATE);
286         new CreatePickedDocumentTask(
287                 mActivity,
288                 mDocs,
289                 mLastAccessed,
290                 mState.stack,
291                 mimeType,
292                 displayName,
293                 inProgressStateListener,
294                 this::onPickFinished)
295                 .executeOnExecutor(getExecutorForCurrentDirectory());
296     }
297 
298     // User requested to overwrite a target. If confirmed by user #finishPicking() will be
299     // called.
saveDocument(FragmentManager fm, DocumentInfo replaceTarget)300     void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) {
301         assert(mState.action == ACTION_CREATE);
302         assert(replaceTarget != null);
303 
304         mInjector.dialogs.confirmOverwrite(fm, replaceTarget);
305     }
306 
finishPicking(Uri... docs)307     void finishPicking(Uri... docs) {
308         new SetLastAccessedStackTask(
309                 mActivity,
310                 mLastAccessed,
311                 mState.stack,
312                 () -> {
313                     onPickFinished(docs);
314                 }
315         ) .executeOnExecutor(getExecutorForCurrentDirectory());
316     }
317 
onPickFinished(Uri... uris)318     private void onPickFinished(Uri... uris) {
319         if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
320 
321         final Intent intent = new Intent();
322         if (uris.length == 1) {
323             intent.setData(uris[0]);
324         } else if (uris.length > 1) {
325             final ClipData clipData = new ClipData(
326                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
327             for (int i = 1; i < uris.length; i++) {
328                 clipData.addItem(new ClipData.Item(uris[i]));
329             }
330             intent.setClipData(clipData);
331         }
332 
333         // TODO: Separate this piece of logic per action.
334         // We don't instantiate different objects for different actions at the first place, so it's
335         // not a easy task to separate this logic cleanly.
336         // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
337         // inheritance structure.
338         if (mState.action == ACTION_GET_CONTENT) {
339             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
340         } else if (mState.action == ACTION_OPEN_TREE) {
341             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
342                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
343                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
344                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
345         } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
346             // Picking a copy destination is only used internally by us, so we
347             // don't need to extend permissions to the caller.
348             intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
349             intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
350         } else {
351             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
352                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
353                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
354         }
355 
356         mActivity.setResult(Activity.RESULT_OK, intent, 0);
357         mActivity.finish();
358     }
359 
getExecutorForCurrentDirectory()360     private Executor getExecutorForCurrentDirectory() {
361         final DocumentInfo cwd = mState.stack.peek();
362         if (cwd != null && cwd.authority != null) {
363             return mExecutors.lookup(cwd.authority);
364         } else {
365             return AsyncTask.THREAD_POOL_EXECUTOR;
366         }
367     }
368 
369     public interface Addons extends CommonAddons {
onDocumentPicked(DocumentInfo doc)370         void onDocumentPicked(DocumentInfo doc);
371 
372         /**
373          * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept
374          * this method call in test environment.
375          */
376         @VisibleForTesting
setResult(int resultCode, Intent result, int notUsed)377         void setResult(int resultCode, Intent result, int notUsed);
378     }
379 }
380