• 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.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.content.ActivityNotFoundException;
27 import android.content.ClipData;
28 import android.content.ComponentName;
29 import android.content.Intent;
30 import android.content.pm.ResolveInfo;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Parcelable;
34 import android.provider.DocumentsContract;
35 import android.provider.Settings;
36 import android.util.Log;
37 
38 import androidx.annotation.VisibleForTesting;
39 import androidx.fragment.app.FragmentActivity;
40 import androidx.fragment.app.FragmentManager;
41 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
42 
43 import com.android.documentsui.AbstractActionHandler;
44 import com.android.documentsui.ActivityConfig;
45 import com.android.documentsui.DocumentsAccess;
46 import com.android.documentsui.Injector;
47 import com.android.documentsui.MetricConsts;
48 import com.android.documentsui.Metrics;
49 import com.android.documentsui.UserIdManager;
50 import com.android.documentsui.base.BooleanConsumer;
51 import com.android.documentsui.base.DocumentInfo;
52 import com.android.documentsui.base.DocumentStack;
53 import com.android.documentsui.base.Features;
54 import com.android.documentsui.base.Lookup;
55 import com.android.documentsui.base.RootInfo;
56 import com.android.documentsui.base.Shared;
57 import com.android.documentsui.base.State;
58 import com.android.documentsui.base.UserId;
59 import com.android.documentsui.dirlist.AnimationView;
60 import com.android.documentsui.picker.ActionHandler.Addons;
61 import com.android.documentsui.queries.SearchViewManager;
62 import com.android.documentsui.roots.ProvidersAccess;
63 import com.android.documentsui.services.FileOperationService;
64 
65 import com.android.documentsui.util.VersionUtils;
66 import java.util.Arrays;
67 import java.util.Locale;
68 import java.util.concurrent.Executor;
69 
70 import java.util.regex.Pattern;
71 import javax.annotation.Nullable;
72 
73 /**
74  * Provides {@link PickActivity} action specializations to fragments.
75  */
76 class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionHandler<T> {
77 
78     private static final String TAG = "PickerActionHandler";
79 
80     private final Features mFeatures;
81     private final ActivityConfig mConfig;
82     private final LastAccessedStorage mLastAccessed;
83     private final UserIdManager mUserIdManager;
84     private final static Pattern PATTERN_BLOCK_PATH = Pattern.compile(
85         ".*:android\\/(?:data|obb|sandbox)$");
86 
87     private UpdatePickResultTask mUpdatePickResultTask;
88 
ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector injector, LastAccessedStorage lastAccessed, UserIdManager userIdManager)89     ActionHandler(
90             T activity,
91             State state,
92             ProvidersAccess providers,
93             DocumentsAccess docs,
94             SearchViewManager searchMgr,
95             Lookup<String, Executor> executors,
96             Injector injector,
97             LastAccessedStorage lastAccessed,
98             UserIdManager userIdManager) {
99         super(activity, state, providers, docs, searchMgr, executors, injector);
100 
101         mConfig = injector.config;
102         mFeatures = injector.features;
103         mLastAccessed = lastAccessed;
104         mUpdatePickResultTask = new UpdatePickResultTask(
105             activity.getApplicationContext(), mInjector.pickResult);
106         mUserIdManager = userIdManager;
107     }
108 
109     @Override
initLocation(Intent intent)110     public void initLocation(Intent intent) {
111         assert(intent != null);
112 
113         // stack is initialized if it's restored from bundle, which means we're restoring a
114         // previously stored state.
115         if (mState.stack.isInitialized()) {
116             if (DEBUG) {
117                 Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
118             }
119             restoreRootAndDirectory();
120             return;
121         }
122 
123         if (launchHomeForCopyDestination(intent)) {
124             if (DEBUG) {
125                 Log.d(TAG, "Launching directly into Home directory for copy destination.");
126             }
127             return;
128         }
129 
130         if (mFeatures.isLaunchToDocumentEnabled() && launchToInitialUri(intent)) {
131             if (DEBUG) {
132                 Log.d(TAG, "Launched to initial uri.");
133             }
134             return;
135         }
136 
137         if (DEBUG) {
138             Log.d(TAG, "Load last accessed stack.");
139         }
140         initLoadLastAccessedStack();
141     }
142 
143     @Override
launchToDefaultLocation()144     protected void launchToDefaultLocation() {
145         loadLastAccessedStack();
146     }
147 
launchHomeForCopyDestination(Intent intent)148     private boolean launchHomeForCopyDestination(Intent intent) {
149         // As a matter of policy we don't load the last used stack for the copy
150         // destination picker (user is already in Files app).
151         // Consensus was that the experice was too confusing.
152         // In all other cases, where the user is visiting us from another app
153         // we restore the stack as last used from that app.
154         if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
155             loadHomeDir();
156             return true;
157         }
158 
159         return false;
160     }
161 
launchToInitialUri(Intent intent)162     private boolean launchToInitialUri(Intent intent) {
163         Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
164         if (uri != null) {
165             // In android S and above if path contains Android/data, Android/obb
166             // or Android/sandbox redirect to the root for which
167             // FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE is already set
168             if(Shared.shouldRestrictStorageAccessFramework(mActivity)
169                 && (PATTERN_BLOCK_PATH.matcher(uri.getPath().toLowerCase(Locale.ROOT)).matches())){
170                 loadDeviceRoot();
171                 return true;
172             }
173             if (DocumentsContract.isRootUri(mActivity, uri)) {
174                 loadRoot(uri, UserId.DEFAULT_USER);
175                 return true;
176             } else if (DocumentsContract.isDocumentUri(mActivity, uri)) {
177                 return launchToDocument(uri);
178             }
179         }
180 
181         return false;
182     }
183 
initLoadLastAccessedStack()184     private void initLoadLastAccessedStack() {
185         if (DEBUG) {
186             Log.d(TAG, "Attempting to load last used stack for calling package.");
187         }
188         // Block UI until stack is fully loaded, else there is an intermediate incomplete UI state.
189         onLastAccessedStackLoaded(mLastAccessed.getLastAccessed(mActivity, mProviders, mState));
190     }
191 
loadLastAccessedStack()192     private void loadLastAccessedStack() {
193         if (DEBUG) {
194             Log.d(TAG, "Attempting to load last used stack for calling package.");
195         }
196         new LoadLastAccessedStackTask<>(
197                 mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded)
198                 .execute();
199     }
200 
onLastAccessedStackLoaded(@ullable DocumentStack stack)201     private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) {
202         if (stack == null) {
203             loadDefaultLocation();
204         } else {
205             mState.stack.reset(stack);
206             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
207         }
208     }
209 
getUpdatePickResultTask()210     public UpdatePickResultTask getUpdatePickResultTask() {
211         return mUpdatePickResultTask;
212     }
213 
updatePickResult(Intent intent, boolean isSearching, int root)214     private void updatePickResult(Intent intent, boolean isSearching, int root) {
215         ClipData cdata = intent.getClipData();
216         int fileCount = 0;
217         Uri uri = null;
218 
219         // There are 2 cases that would be single-select:
220         // 1. getData() isn't null and getClipData() is null.
221         // 2. getClipData() isn't null and the item count of it is 1.
222         if (intent.getData() != null && cdata == null) {
223             fileCount = 1;
224             uri = intent.getData();
225         } else if (cdata != null) {
226             fileCount = cdata.getItemCount();
227             if (fileCount == 1) {
228                 uri = cdata.getItemAt(0).getUri();
229             }
230         }
231 
232         mInjector.pickResult.setFileCount(fileCount);
233         mInjector.pickResult.setIsSearching(isSearching);
234         mInjector.pickResult.setRoot(root);
235         mInjector.pickResult.setFileUri(uri);
236         getUpdatePickResultTask().safeExecute();
237     }
238 
loadDefaultLocation()239     private void loadDefaultLocation() {
240         switch (mState.action) {
241             case ACTION_CREATE:
242                 loadHomeDir();
243                 break;
244             case ACTION_OPEN_TREE:
245                 loadDeviceRoot();
246                 break;
247             case ACTION_GET_CONTENT:
248             case ACTION_OPEN:
249                 loadRecent();
250                 break;
251             default:
252                 throw new UnsupportedOperationException("Unexpected action type: " + mState.action);
253         }
254     }
255 
256     @Override
showAppDetails(ResolveInfo info, UserId userId)257     public void showAppDetails(ResolveInfo info, UserId userId) {
258         mInjector.pickResult.increaseActionCount();
259         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
260         intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null));
261         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
262         userId.startActivityAsUser(mActivity, intent);
263     }
264 
265     @Override
openInNewWindow(DocumentStack path)266     public void openInNewWindow(DocumentStack path) {
267         // Open new window support only depends on vanilla Activity, so it is
268         // implemented in our parent class. But we don't support that in
269         // picking. So as a matter of defensiveness, we override that here.
270         throw new UnsupportedOperationException("Can't open in new window");
271     }
272 
273     @Override
openRoot(RootInfo root)274     public void openRoot(RootInfo root) {
275         Metrics.logRootVisited(MetricConsts.PICKER_SCOPE, root);
276         mInjector.pickResult.increaseActionCount();
277         mActivity.onRootPicked(root);
278     }
279 
280     @Override
openRoot(ResolveInfo info, UserId userId)281     public void openRoot(ResolveInfo info, UserId userId) {
282         Metrics.logAppVisited(info);
283         mInjector.pickResult.increaseActionCount();
284 
285         // The App root item should not show if we cannot interact with the target user.
286         // But the user managed to get here, this is the final check of permission. We don't
287         // perform the check on activity result.
288         if (!mState.canInteractWith(userId)) {
289             mInjector.dialogs.showActionNotAllowed();
290             return;
291         }
292 
293         Intent intent = new Intent(mActivity.getIntent());
294         final int flagsRemoved = Intent.FLAG_GRANT_READ_URI_PERMISSION
295                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
296                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
297                 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
298         intent.setFlags(intent.getFlags() & ~flagsRemoved);
299         intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
300         intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
301         intent.setComponent(new ComponentName(
302                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
303         try {
304             boolean isCurrentUser = UserId.CURRENT_USER.equals(userId);
305             if (isCurrentUser) {
306                 mActivity.startActivity(intent);
307             } else {
308                 userId.startActivityAsUser(mActivity, intent);
309             }
310             Metrics.logLaunchOtherApp(!UserId.CURRENT_USER.equals(userId));
311             mActivity.finish();
312         } catch (SecurityException | ActivityNotFoundException e) {
313             Log.e(TAG, "Caught error: " + e.getLocalizedMessage());
314             mInjector.dialogs.showNoApplicationFound();
315         }
316     }
317 
318 
319     @Override
springOpenDirectory(DocumentInfo doc)320     public void springOpenDirectory(DocumentInfo doc) {
321     }
322 
323     @Override
openItem(ItemDetails<String> details, @ViewType int type, @ViewType int fallback)324     public boolean openItem(ItemDetails<String> details, @ViewType int type,
325             @ViewType int fallback) {
326         mInjector.pickResult.increaseActionCount();
327         DocumentInfo doc = mModel.getDocument(details.getSelectionKey());
328         if (doc == null) {
329             Log.w(TAG, "Can't view item. No Document available for modeId: "
330                     + details.getSelectionKey());
331             return false;
332         }
333 
334         if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
335             mActivity.onDocumentPicked(doc);
336             mSelectionMgr.clearSelection();
337             return !doc.isDirectory();
338         }
339         return false;
340     }
341 
342     @Override
previewItem(ItemDetails<String> details)343     public boolean previewItem(ItemDetails<String> details) {
344         mInjector.pickResult.increaseActionCount();
345         final DocumentInfo doc = mModel.getDocument(details.getSelectionKey());
346         if (doc == null) {
347             Log.w(TAG, "Can't view item. No Document available for modeId: "
348                     + details.getSelectionKey());
349             return false;
350         }
351 
352         onDocumentOpened(doc, VIEW_TYPE_PREVIEW, VIEW_TYPE_REGULAR, true);
353         return !doc.isContainer();
354     }
355 
pickDocument(FragmentManager fm, DocumentInfo pickTarget)356     void pickDocument(FragmentManager fm, DocumentInfo pickTarget) {
357         assert(pickTarget != null);
358         mInjector.pickResult.increaseActionCount();
359         Uri result;
360         switch (mState.action) {
361             case ACTION_OPEN_TREE:
362                 mInjector.dialogs.confirmAction(fm, pickTarget, ConfirmFragment.TYPE_OEPN_TREE);
363                 break;
364             case ACTION_PICK_COPY_DESTINATION:
365                 result = pickTarget.derivedUri;
366                 finishPicking(result);
367                 break;
368             default:
369                 // Should not be reached
370                 throw new IllegalStateException("Invalid mState.action");
371         }
372     }
373 
saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener)374     void saveDocument(
375             String mimeType, String displayName, BooleanConsumer inProgressStateListener) {
376         assert(mState.action == ACTION_CREATE);
377         mInjector.pickResult.increaseActionCount();
378         new CreatePickedDocumentTask(
379                 mActivity,
380                 mDocs,
381                 mLastAccessed,
382                 mState.stack,
383                 mimeType,
384                 displayName,
385                 inProgressStateListener,
386                 this::onPickFinished)
387                 .executeOnExecutor(getExecutorForCurrentDirectory());
388     }
389 
390     // User requested to overwrite a target. If confirmed by user #finishPicking() will be
391     // called.
saveDocument(FragmentManager fm, DocumentInfo replaceTarget)392     void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) {
393         assert(mState.action == ACTION_CREATE);
394         mInjector.pickResult.increaseActionCount();
395         assert(replaceTarget != null);
396 
397         // Adding a confirmation dialog breaks an inherited CTS test (testCreateExisting), so we
398         // need to add a feature flag to bypass this feature in ARC++ environment.
399         if (mFeatures.isOverwriteConfirmationEnabled()) {
400             mInjector.dialogs.confirmAction(fm, replaceTarget, ConfirmFragment.TYPE_OVERWRITE);
401         } else {
402             finishPicking(replaceTarget.getDocumentUri());
403         }
404     }
405 
finishPicking(Uri... docs)406     void finishPicking(Uri... docs) {
407         new SetLastAccessedStackTask(
408                 mActivity,
409                 mLastAccessed,
410                 mState.stack,
411                 () -> {
412                     onPickFinished(docs);
413                 }
414         ) .executeOnExecutor(getExecutorForCurrentDirectory());
415     }
416 
onPickFinished(Uri... uris)417     private void onPickFinished(Uri... uris) {
418         if (DEBUG) {
419             Log.d(TAG, "onFinished() " + Arrays.toString(uris));
420         }
421 
422         final Intent intent = new Intent();
423         if (uris.length == 1) {
424             intent.setData(uris[0]);
425         } else if (uris.length > 1) {
426             final ClipData clipData = new ClipData(
427                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
428             for (int i = 1; i < uris.length; i++) {
429                 clipData.addItem(new ClipData.Item(uris[i]));
430             }
431             intent.setClipData(clipData);
432         }
433 
434         updatePickResult(
435             intent, mSearchMgr.isSearching(), Metrics.sanitizeRoot(mState.stack.getRoot()));
436 
437         // TODO: Separate this piece of logic per action.
438         // We don't instantiate different objects for different actions at the first place, so it's
439         // not a easy task to separate this logic cleanly.
440         // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
441         // inheritance structure.
442         if (mState.action == ACTION_GET_CONTENT) {
443             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
444         } else if (mState.action == ACTION_OPEN_TREE) {
445             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
446                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
447                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
448                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
449         } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
450             // Picking a copy destination is only used internally by us, so we
451             // don't need to extend permissions to the caller.
452             intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
453             intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
454         } else {
455             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
456                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
457                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
458         }
459 
460         mActivity.setResult(FragmentActivity.RESULT_OK, intent, 0);
461         mActivity.finish();
462     }
463 
getExecutorForCurrentDirectory()464     private Executor getExecutorForCurrentDirectory() {
465         final DocumentInfo cwd = mState.stack.peek();
466         if (cwd != null && cwd.authority != null) {
467             return mExecutors.lookup(cwd.authority);
468         } else {
469             return AsyncTask.THREAD_POOL_EXECUTOR;
470         }
471     }
472 
473     public interface Addons extends CommonAddons {
474         @Override
onDocumentPicked(DocumentInfo doc)475         void onDocumentPicked(DocumentInfo doc);
476 
477         /**
478          * Overload final method {@link FragmentActivity#setResult(int, Intent)} so that we can
479          * intercept this method call in test environment.
480          */
481         @VisibleForTesting
setResult(int resultCode, Intent result, int notUsed)482         void setResult(int resultCode, Intent result, int notUsed);
483     }
484 }
485