• 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.files;
18 
19 import static android.content.ContentResolver.wrap;
20 
21 import static com.android.documentsui.base.SharedMinimal.DEBUG;
22 
23 import android.app.DownloadManager;
24 import android.content.ActivityNotFoundException;
25 import android.content.ClipData;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.os.FileUtils;
31 import android.provider.DocumentsContract;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.DragEvent;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.fragment.app.FragmentActivity;
38 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
39 import androidx.recyclerview.selection.MutableSelection;
40 import androidx.recyclerview.selection.Selection;
41 
42 import com.android.documentsui.AbstractActionHandler;
43 import com.android.documentsui.ActionModeAddons;
44 import com.android.documentsui.ActivityConfig;
45 import com.android.documentsui.DocumentsAccess;
46 import com.android.documentsui.DocumentsApplication;
47 import com.android.documentsui.DragAndDropManager;
48 import com.android.documentsui.Injector;
49 import com.android.documentsui.MetricConsts;
50 import com.android.documentsui.Metrics;
51 import com.android.documentsui.R;
52 import com.android.documentsui.TimeoutTask;
53 import com.android.documentsui.base.DebugFlags;
54 import com.android.documentsui.base.DocumentFilters;
55 import com.android.documentsui.base.DocumentInfo;
56 import com.android.documentsui.base.DocumentStack;
57 import com.android.documentsui.base.Features;
58 import com.android.documentsui.base.Lookup;
59 import com.android.documentsui.base.MimeTypes;
60 import com.android.documentsui.base.Providers;
61 import com.android.documentsui.base.RootInfo;
62 import com.android.documentsui.base.Shared;
63 import com.android.documentsui.base.State;
64 import com.android.documentsui.base.UserId;
65 import com.android.documentsui.clipping.ClipStore;
66 import com.android.documentsui.clipping.DocumentClipper;
67 import com.android.documentsui.clipping.UrisSupplier;
68 import com.android.documentsui.dirlist.AnimationView;
69 import com.android.documentsui.inspector.InspectorActivity;
70 import com.android.documentsui.queries.SearchViewManager;
71 import com.android.documentsui.roots.ProvidersAccess;
72 import com.android.documentsui.services.FileOperation;
73 import com.android.documentsui.services.FileOperationService;
74 import com.android.documentsui.services.FileOperations;
75 
76 import java.util.ArrayList;
77 import java.util.List;
78 import java.util.concurrent.Executor;
79 
80 import javax.annotation.Nullable;
81 
82 /**
83  * Provides {@link FilesActivity} action specializations to fragments.
84  * @param <T> activity which extends {@link FragmentActivity} and implements
85  *              {@link AbstractActionHandler.CommonAddons}.
86  */
87 public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.CommonAddons>
88         extends AbstractActionHandler<T> {
89 
90     private static final String TAG = "ManagerActionHandler";
91     private static final int SHARE_FILES_COUNT_LIMIT = 100;
92 
93     private final ActionModeAddons mActionModeAddons;
94     private final Features mFeatures;
95     private final ActivityConfig mConfig;
96     private final DocumentClipper mClipper;
97     private final ClipStore mClipStore;
98     private final DragAndDropManager mDragAndDropManager;
99 
ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, ActionModeAddons actionModeAddons, DocumentClipper clipper, ClipStore clipStore, DragAndDropManager dragAndDropManager, Injector injector)100     ActionHandler(
101             T activity,
102             State state,
103             ProvidersAccess providers,
104             DocumentsAccess docs,
105             SearchViewManager searchMgr,
106             Lookup<String, Executor> executors,
107             ActionModeAddons actionModeAddons,
108             DocumentClipper clipper,
109             ClipStore clipStore,
110             DragAndDropManager dragAndDropManager,
111             Injector injector) {
112 
113         super(activity, state, providers, docs, searchMgr, executors, injector);
114 
115         mActionModeAddons = actionModeAddons;
116         mFeatures = injector.features;
117         mConfig = injector.config;
118         mClipper = clipper;
119         mClipStore = clipStore;
120         mDragAndDropManager = dragAndDropManager;
121     }
122 
123     @Override
dropOn(DragEvent event, RootInfo root)124     public boolean dropOn(DragEvent event, RootInfo root) {
125         if (!root.supportsCreate() || root.isLibrary()) {
126             return false;
127         }
128 
129         // DragEvent gets recycled, so it is possible that by the time the callback is called,
130         // event.getLocalState() and event.getClipData() returns null. Thus, we want to save
131         // references to ensure they are non null.
132         final ClipData clipData = event.getClipData();
133         final Object localState = event.getLocalState();
134 
135         return mDragAndDropManager.drop(
136                 clipData, localState, root, this, mDialogs::showFileOperationStatus);
137     }
138 
139     @Override
openSelectedInNewWindow()140     public void openSelectedInNewWindow() {
141         Selection<String> selection = getStableSelection();
142         if (selection.isEmpty()) {
143             return;
144         }
145 
146         assert(selection.size() == 1);
147         DocumentInfo doc = mModel.getDocument(selection.iterator().next());
148         assert(doc != null);
149         openInNewWindow(new DocumentStack(mState.stack, doc));
150     }
151 
152     @Override
openSettings(RootInfo root)153     public void openSettings(RootInfo root) {
154         Metrics.logUserAction(MetricConsts.USER_ACTION_SETTINGS);
155         final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
156         intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
157         root.userId.startActivityAsUser(mActivity, intent);
158     }
159 
160     @Override
pasteIntoFolder(RootInfo root)161     public void pasteIntoFolder(RootInfo root) {
162         this.getRootDocument(
163                 root,
164                 TimeoutTask.DEFAULT_TIMEOUT,
165                 (DocumentInfo doc) -> pasteIntoFolder(root, doc));
166     }
167 
pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc)168     private void pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc) {
169         DocumentStack stack = new DocumentStack(root, doc);
170         mClipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
171     }
172 
173     @Override
renameDocument(String name, DocumentInfo document)174     public @Nullable DocumentInfo renameDocument(String name, DocumentInfo document) {
175         ContentResolver resolver = document.userId.getContentResolver(mActivity);
176         ContentProviderClient client = null;
177 
178         try {
179             client = DocumentsApplication.acquireUnstableProviderOrThrow(
180                     resolver, document.derivedUri.getAuthority());
181             Uri newUri = DocumentsContract.renameDocument(
182                     wrap(client), document.derivedUri, name);
183             return DocumentInfo.fromUri(resolver, newUri, document.userId);
184         } catch (Exception e) {
185             Log.w(TAG, "Failed to rename file", e);
186             return null;
187         } finally {
188             FileUtils.closeQuietly(client);
189         }
190     }
191 
192     @Override
openRoot(RootInfo root)193     public void openRoot(RootInfo root) {
194         Metrics.logRootVisited(MetricConsts.FILES_SCOPE, root);
195         mActivity.onRootPicked(root);
196     }
197 
198     @Override
openItem(ItemDetails<String> details, @ViewType int type, @ViewType int fallback)199     public boolean openItem(ItemDetails<String> details, @ViewType int type,
200             @ViewType int fallback) {
201         DocumentInfo doc = mModel.getDocument(details.getSelectionKey());
202         if (doc == null) {
203             Log.w(TAG, "Can't view item. No Document available for modeId: "
204                     + details.getSelectionKey());
205             return false;
206         }
207         mInjector.searchManager.recordHistory();
208 
209         return openDocument(doc, type, fallback);
210     }
211 
212     // TODO: Make this private and make tests call openDocument(DocumentDetails, int, int) instead.
213     @VisibleForTesting
openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback)214     public boolean openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
215         if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
216             onDocumentOpened(doc, type, fallback, false);
217             mSelectionMgr.clearSelection();
218             return !doc.isContainer();
219         }
220         return false;
221     }
222 
223     @Override
springOpenDirectory(DocumentInfo doc)224     public void springOpenDirectory(DocumentInfo doc) {
225         assert(doc.isDirectory());
226         mActionModeAddons.finishActionMode();
227         openContainerDocument(doc);
228     }
229 
getSelectedOrFocused()230     private Selection<String> getSelectedOrFocused() {
231         final MutableSelection<String> selection = this.getStableSelection();
232         if (selection.isEmpty()) {
233             String focusModelId = mFocusHandler.getFocusModelId();
234             if (focusModelId != null) {
235                 selection.add(focusModelId);
236             }
237         }
238 
239         return selection;
240     }
241 
242     @Override
cutToClipboard()243     public void cutToClipboard() {
244         Metrics.logUserAction(MetricConsts.USER_ACTION_CUT_CLIPBOARD);
245         Selection<String> selection = getSelectedOrFocused();
246 
247         if (selection.isEmpty()) {
248             return;
249         }
250 
251         if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
252             mDialogs.showOperationUnsupported();
253             return;
254         }
255 
256         mSelectionMgr.clearSelection();
257 
258         mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
259 
260         mDialogs.showDocumentsClipped(selection.size());
261     }
262 
263     @Override
copyToClipboard()264     public void copyToClipboard() {
265         Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_CLIPBOARD);
266         Selection<String> selection = getSelectedOrFocused();
267 
268         if (selection.isEmpty()) {
269             return;
270         }
271         mSelectionMgr.clearSelection();
272 
273         mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
274 
275         mDialogs.showDocumentsClipped(selection.size());
276     }
277 
278     @Override
viewInOwner()279     public void viewInOwner() {
280         Metrics.logUserAction(MetricConsts.USER_ACTION_VIEW_IN_APPLICATION);
281         Selection<String> selection = getSelectedOrFocused();
282 
283         if (selection.isEmpty() || selection.size() > 1) {
284             return;
285         }
286         DocumentInfo doc = mModel.getDocument(selection.iterator().next());
287         Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
288         intent.setPackage(mProviders.getPackageName(UserId.DEFAULT_USER, doc.authority));
289         intent.addCategory(Intent.CATEGORY_DEFAULT);
290         intent.setData(doc.derivedUri);
291         try {
292             doc.userId.startActivityAsUser(mActivity, intent);
293         } catch (ActivityNotFoundException e) {
294             Log.e(TAG, "Failed to view settings in application for " + doc.derivedUri, e);
295             mDialogs.showNoApplicationFound();
296         }
297     }
298 
299     @Override
showDeleteDialog()300     public void showDeleteDialog() {
301         Selection selection = getSelectedOrFocused();
302         if (selection.isEmpty()) {
303             return;
304         }
305 
306         DeleteDocumentFragment.show(mActivity.getSupportFragmentManager(),
307                 mModel.getDocuments(selection),
308                 mState.stack.peek());
309     }
310 
311 
312     @Override
deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent)313     public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) {
314         if (docs == null || docs.isEmpty()) {
315             return;
316         }
317 
318         mActionModeAddons.finishActionMode();
319 
320         List<Uri> uris = new ArrayList<>(docs.size());
321         for (DocumentInfo doc : docs) {
322             uris.add(doc.derivedUri);
323         }
324 
325         UrisSupplier srcs;
326         try {
327             srcs = UrisSupplier.create(
328                     uris,
329                     mClipStore);
330         } catch (Exception e) {
331             Log.e(TAG, "Failed to delete a file because we were unable to get item URIs.", e);
332             mDialogs.showFileOperationStatus(
333                     FileOperations.Callback.STATUS_FAILED,
334                     FileOperationService.OPERATION_DELETE,
335                     uris.size());
336             return;
337         }
338 
339         FileOperation operation = new FileOperation.Builder()
340                 .withOpType(FileOperationService.OPERATION_DELETE)
341                 .withDestination(mState.stack)
342                 .withSrcs(srcs)
343                 .withSrcParent(srcParent == null ? null : srcParent.derivedUri)
344                 .build();
345 
346         FileOperations.start(mActivity, operation, mDialogs::showFileOperationStatus,
347                 FileOperations.createJobId());
348     }
349 
350     @Override
shareSelectedDocuments()351     public void shareSelectedDocuments() {
352         Metrics.logUserAction(MetricConsts.USER_ACTION_SHARE);
353 
354         Selection<String> selection = getStableSelection();
355         if (selection.isEmpty()) {
356             return;
357         } else if (selection.size() > SHARE_FILES_COUNT_LIMIT) {
358             mDialogs.showShareOverLimit(SHARE_FILES_COUNT_LIMIT);
359             return;
360         }
361 
362         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
363         List<DocumentInfo> docs = mModel.loadDocuments(
364                 selection, DocumentFilters.sharable(mFeatures));
365 
366         Intent intent;
367 
368         if (docs.size() == 1) {
369             intent = new Intent(Intent.ACTION_SEND);
370             DocumentInfo doc = docs.get(0);
371             intent.setType(doc.mimeType);
372             intent.putExtra(Intent.EXTRA_STREAM, doc.getDocumentUri());
373 
374         } else if (docs.size() > 1) {
375             intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
376 
377             final ArrayList<String> mimeTypes = new ArrayList<>();
378             final ArrayList<Uri> uris = new ArrayList<>();
379             for (DocumentInfo doc : docs) {
380                 mimeTypes.add(doc.mimeType);
381                 uris.add(doc.getDocumentUri());
382             }
383 
384             intent.setType(MimeTypes.findCommonMimeType(mimeTypes));
385             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
386 
387         } else {
388             // Everything filtered out, nothing to share.
389             return;
390         }
391 
392         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
393         intent.addCategory(Intent.CATEGORY_DEFAULT);
394 
395         if (mFeatures.isVirtualFilesSharingEnabled()
396                 && mModel.hasDocuments(selection, DocumentFilters.VIRTUAL)) {
397             intent.addCategory(Intent.CATEGORY_TYPED_OPENABLE);
398         }
399 
400         Intent chooserIntent = Intent.createChooser(
401                 intent, mActivity.getResources().getText(R.string.share_via));
402 
403         mActivity.startActivity(chooserIntent);
404     }
405 
406     @Override
loadDocumentsForCurrentStack()407     public void loadDocumentsForCurrentStack() {
408         super.loadDocumentsForCurrentStack();
409     }
410 
411     @Override
initLocation(Intent intent)412     public void initLocation(Intent intent) {
413         assert(intent != null);
414 
415         // stack is initialized if it's restored from bundle, which means we're restoring a
416         // previously stored state.
417         if (mState.stack.isInitialized()) {
418             if (DEBUG) {
419                 Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
420             }
421             restoreRootAndDirectory();
422             return;
423         }
424 
425         if (launchToStackLocation(intent)) {
426             if (DEBUG) {
427                 Log.d(TAG, "Launched to location from stack.");
428             }
429             return;
430         }
431 
432         if (launchToRoot(intent)) {
433             if (DEBUG) {
434                 Log.d(TAG, "Launched to root for browsing.");
435             }
436             return;
437         }
438 
439         if (launchToDocument(intent)) {
440             if (DEBUG) {
441                 Log.d(TAG, "Launched to a document.");
442             }
443             return;
444         }
445 
446         if (launchToDownloads(intent)) {
447             if (DEBUG) {
448                 Log.d(TAG, "Launched to a downloads.");
449             }
450             return;
451         }
452 
453         if (DEBUG) {
454             Log.d(TAG, "Launching directly into Home directory.");
455         }
456         launchToDefaultLocation();
457     }
458 
459     @Override
launchToDefaultLocation()460     protected void launchToDefaultLocation() {
461         loadHomeDir();
462     }
463 
464     // If EXTRA_STACK is not null in intent, we'll skip other means of loading
465     // or restoring the stack (like URI).
466     //
467     // When restoring from a stack, if a URI is present, it should only ever be:
468     // -- a launch URI: Launch URIs support sensible activity management,
469     //    but don't specify a real content target)
470     // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
471     //
472     // Any other URI is *sorta* unexpected...except when browsing an archive
473     // in downloads.
launchToStackLocation(Intent intent)474     private boolean launchToStackLocation(Intent intent) {
475         DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
476         if (stack == null || stack.getRoot() == null) {
477             return false;
478         }
479 
480         mState.stack.reset(stack);
481         if (mState.stack.isEmpty()) {
482             mActivity.onRootPicked(mState.stack.getRoot());
483         } else {
484             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
485         }
486 
487         return true;
488     }
489 
launchToRoot(Intent intent)490     private boolean launchToRoot(Intent intent) {
491         String action = intent.getAction();
492         if (Intent.ACTION_VIEW.equals(action)) {
493             Uri uri = intent.getData();
494             if (DocumentsContract.isRootUri(mActivity, uri)) {
495                 if (DEBUG) {
496                     Log.d(TAG, "Launching with root URI.");
497                 }
498                 // If we've got a specific root to display, restore that root using a dedicated
499                 // authority. That way a misbehaving provider won't result in an ANR.
500                 loadRoot(uri, UserId.DEFAULT_USER);
501                 return true;
502             } else if (DocumentsContract.isRootsUri(mActivity, uri)) {
503                 if (DEBUG) {
504                     Log.d(TAG, "Launching first root with roots URI.");
505                 }
506                 // TODO: b/116760996 Let the user can disambiguate between roots if there are
507                 // multiple from DocumentsProvider instead of launching the first root in default
508                 loadFirstRoot(uri);
509                 return true;
510             }
511         }
512         return false;
513     }
514 
launchToDocument(Intent intent)515     private boolean launchToDocument(Intent intent) {
516         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
517             Uri uri = intent.getData();
518             if (DocumentsContract.isDocumentUri(mActivity, uri)) {
519                 return launchToDocument(intent.getData());
520             }
521         }
522 
523         return false;
524     }
525 
launchToDownloads(Intent intent)526     private boolean launchToDownloads(Intent intent) {
527         if (DownloadManager.ACTION_VIEW_DOWNLOADS.equals(intent.getAction())) {
528             Uri uri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
529                     Providers.ROOT_ID_DOWNLOADS);
530             loadRoot(uri, UserId.DEFAULT_USER);
531             return true;
532         }
533 
534         return false;
535     }
536 
537     @Override
showChooserForDoc(DocumentInfo doc)538     public void showChooserForDoc(DocumentInfo doc) {
539         assert(!doc.isDirectory());
540 
541         if (manageDocument(doc)) {
542             Log.w(TAG, "Open with is not yet supported for managed doc.");
543             return;
544         }
545 
546         Intent intent = Intent.createChooser(buildViewIntent(doc), null);
547         intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
548         try {
549             doc.userId.startActivityAsUser(mActivity, intent);
550         } catch (ActivityNotFoundException e) {
551             mDialogs.showNoApplicationFound();
552         }
553     }
554 
555     @Override
showInspector(DocumentInfo doc)556     public void showInspector(DocumentInfo doc) {
557         Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR);
558         Intent intent = InspectorActivity.createIntent(mActivity, doc.derivedUri, doc.userId);
559 
560         // permit the display of debug info about the file.
561         intent.putExtra(
562                 Shared.EXTRA_SHOW_DEBUG,
563                 mFeatures.isDebugSupportEnabled() &&
564                         (DEBUG || DebugFlags.getDocumentDetailsEnabled()));
565 
566         // The "root document" (top level folder in a root) don't usually have a
567         // human friendly display name. That's because we've never shown the root
568         // folder's name to anyone.
569         // For that reason when the doc being inspected is the root folder,
570         // we override the displayName of the doc w/ the Root's name instead.
571         // The Root's name is shown to the user in the sidebar.
572         if (doc.isDirectory() && mState.stack.size() == 1 && mState.stack.get(0).equals(doc)) {
573             RootInfo root = mActivity.getCurrentRoot();
574             // Recents root title isn't defined, but inspector is disabled for recents root folder.
575             assert !TextUtils.isEmpty(root.title);
576             intent.putExtra(Intent.EXTRA_TITLE, root.title);
577         }
578         mActivity.startActivity(intent);
579     }
580 }
581