blob: 052d80ee0b041a8f2905fb2ff18525aa9abda0f8 [file] [log] [blame]
Garfield Tan7d75f7b2016-09-20 16:33:24 -07001/*
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
Steve McKayb6006b22016-09-29 09:23:45 -070017package com.android.documentsui.files;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070018
Jeff Sharkey2c0b4852019-02-15 15:53:47 -070019import static android.content.ContentResolver.wrap;
20
Felipe Leme9de58072018-01-19 16:40:04 -080021import static com.android.documentsui.base.SharedMinimal.DEBUG;
Steve McKay988d8a32016-09-27 09:41:17 -070022
Tony Huangbb6d8f62019-02-27 14:26:34 +080023import android.app.DownloadManager;
Steve McKayeed2f4e2016-10-03 20:30:52 -070024import android.content.ActivityNotFoundException;
Ben Lin30b0dc12017-03-07 15:37:16 -080025import android.content.ClipData;
Jon Mann30d8c792017-02-21 17:44:49 -080026import android.content.ContentProviderClient;
27import android.content.ContentResolver;
Steve McKay739f94b2016-09-22 14:54:23 -070028import android.content.Intent;
Steve McKay988d8a32016-09-27 09:41:17 -070029import android.net.Uri;
Jeff Sharkeybb68a652019-02-19 11:17:30 -070030import android.os.FileUtils;
Steve McKay739f94b2016-09-22 14:54:23 -070031import android.provider.DocumentsContract;
Steve McKayefd10ac2017-07-25 13:33:57 -070032import android.text.TextUtils;
Steve McKay6d20d192016-09-21 17:57:10 -070033import android.util.Log;
Ben Lin174fc2e2017-03-01 17:53:20 -080034import android.view.DragEvent;
Steve McKay6d20d192016-09-21 17:57:10 -070035
Tony Huang71dac092019-06-14 11:37:32 +080036import androidx.annotation.VisibleForTesting;
Tony Huang8d8d92f2018-09-13 14:41:16 +080037import androidx.fragment.app.FragmentActivity;
Riddle Hsu0c375982018-06-21 22:06:43 +080038import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
39import androidx.recyclerview.selection.MutableSelection;
40import androidx.recyclerview.selection.Selection;
41
Steve McKay739f94b2016-09-22 14:54:23 -070042import com.android.documentsui.AbstractActionHandler;
Steve McKaybd9f05a2016-10-10 10:18:36 -070043import com.android.documentsui.ActionModeAddons;
Steve McKay5b0a2c12016-10-07 11:22:31 -070044import com.android.documentsui.ActivityConfig;
Steve McKaydef48682016-10-03 09:07:38 -070045import com.android.documentsui.DocumentsAccess;
Garfield Tanb285b402016-09-21 17:12:18 -070046import com.android.documentsui.DocumentsApplication;
Garfield Tanda2c0f02017-04-11 13:47:58 -070047import com.android.documentsui.DragAndDropManager;
Jon Mann30d8c792017-02-21 17:44:49 -080048import com.android.documentsui.Injector;
shawnlin9cee68f2019-01-25 11:20:18 +080049import com.android.documentsui.MetricConsts;
Steve McKay739f94b2016-09-22 14:54:23 -070050import com.android.documentsui.Metrics;
Ben Lin30b0dc12017-03-07 15:37:16 -080051import com.android.documentsui.Model;
Steve McKayd0718952016-10-10 13:43:36 -070052import com.android.documentsui.R;
Ben Lin30b0dc12017-03-07 15:37:16 -080053import com.android.documentsui.TimeoutTask;
Steve McKay84440eb2017-06-23 12:35:15 -070054import com.android.documentsui.base.DebugFlags;
Steve McKay98f8c5f2017-03-03 13:52:14 -080055import com.android.documentsui.base.DocumentFilters;
Steve McKay6d20d192016-09-21 17:57:10 -070056import com.android.documentsui.base.DocumentInfo;
Garfield Tanb285b402016-09-21 17:12:18 -070057import com.android.documentsui.base.DocumentStack;
Steve McKay98f8c5f2017-03-03 13:52:14 -080058import com.android.documentsui.base.Features;
Steve McKay988d8a32016-09-27 09:41:17 -070059import com.android.documentsui.base.Lookup;
Steve McKayd0718952016-10-10 13:43:36 -070060import com.android.documentsui.base.MimeTypes;
Tony Huangbb6d8f62019-02-27 14:26:34 +080061import com.android.documentsui.base.Providers;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070062import com.android.documentsui.base.RootInfo;
Ben Lindf0fe892016-10-18 17:36:05 -070063import com.android.documentsui.base.Shared;
Steve McKayc8889af2016-09-23 11:22:41 -070064import com.android.documentsui.base.State;
65import com.android.documentsui.clipping.ClipStore;
Garfield Tanb285b402016-09-21 17:12:18 -070066import com.android.documentsui.clipping.DocumentClipper;
Steve McKayc8889af2016-09-23 11:22:41 -070067import com.android.documentsui.clipping.UrisSupplier;
Steve McKay988d8a32016-09-27 09:41:17 -070068import com.android.documentsui.dirlist.AnimationView;
Steve McKayb6006b22016-09-29 09:23:45 -070069import com.android.documentsui.files.ActionHandler.Addons;
Steve McKayf433d202017-07-12 18:46:09 -070070import com.android.documentsui.inspector.InspectorActivity;
Steve McKay3a268232016-10-19 11:15:47 -070071import com.android.documentsui.queries.SearchViewManager;
Jon Mann9bd40992017-03-24 12:34:34 -070072import com.android.documentsui.roots.ProvidersAccess;
Steve McKayc8889af2016-09-23 11:22:41 -070073import com.android.documentsui.services.FileOperation;
74import com.android.documentsui.services.FileOperationService;
75import com.android.documentsui.services.FileOperations;
76import com.android.documentsui.ui.DialogController;
Tony Huang7e72a2d2018-08-28 17:36:30 +080077
Steve McKayd0718952016-10-10 13:43:36 -070078import java.util.ArrayList;
Steve McKayc8889af2016-09-23 11:22:41 -070079import java.util.List;
Steve McKay988d8a32016-09-27 09:41:17 -070080import java.util.concurrent.Executor;
Steve McKay6d20d192016-09-21 17:57:10 -070081
82import javax.annotation.Nullable;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070083
84/**
Steve McKayb6006b22016-09-29 09:23:45 -070085 * Provides {@link FilesActivity} action specializations to fragments.
Garfield Tan7d75f7b2016-09-20 16:33:24 -070086 */
Tony Huang8d8d92f2018-09-13 14:41:16 +080087public class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionHandler<T> {
Garfield Tan7d75f7b2016-09-20 16:33:24 -070088
Steve McKay6d20d192016-09-21 17:57:10 -070089 private static final String TAG = "ManagerActionHandler";
Tony Huangff4915e2019-10-04 15:07:19 +080090 private static final int SHARE_FILES_COUNT_LIMIT = 100;
Steve McKay6d20d192016-09-21 17:57:10 -070091
Steve McKaybd9f05a2016-10-10 10:18:36 -070092 private final ActionModeAddons mActionModeAddons;
Steve McKay98f8c5f2017-03-03 13:52:14 -080093 private final Features mFeatures;
94 private final ActivityConfig mConfig;
Steve McKayc8889af2016-09-23 11:22:41 -070095 private final DialogController mDialogs;
Steve McKay739f94b2016-09-22 14:54:23 -070096 private final DocumentClipper mClipper;
Steve McKayc8889af2016-09-23 11:22:41 -070097 private final ClipStore mClipStore;
Garfield Tanda2c0f02017-04-11 13:47:58 -070098 private final DragAndDropManager mDragAndDropManager;
Garfield Tane9670332017-03-06 18:33:23 -080099 private final Model mModel;
Steve McKay6d20d192016-09-21 17:57:10 -0700100
Steve McKayc8889af2016-09-23 11:22:41 -0700101 ActionHandler(
102 T activity,
Steve McKayc8889af2016-09-23 11:22:41 -0700103 State state,
Jon Mann9bd40992017-03-24 12:34:34 -0700104 ProvidersAccess providers,
Steve McKaydef48682016-10-03 09:07:38 -0700105 DocumentsAccess docs,
Garfield Tan63bf8132016-10-11 11:00:49 -0700106 SearchViewManager searchMgr,
Steve McKay988d8a32016-09-27 09:41:17 -0700107 Lookup<String, Executor> executors,
Steve McKaybd9f05a2016-10-10 10:18:36 -0700108 ActionModeAddons actionModeAddons,
Steve McKayc8889af2016-09-23 11:22:41 -0700109 DocumentClipper clipper,
Jon Mann30d8c792017-02-21 17:44:49 -0800110 ClipStore clipStore,
Garfield Tanda2c0f02017-04-11 13:47:58 -0700111 DragAndDropManager dragAndDropManager,
Jon Mann30d8c792017-02-21 17:44:49 -0800112 Injector injector) {
Steve McKay988d8a32016-09-27 09:41:17 -0700113
Jon Mann9bd40992017-03-24 12:34:34 -0700114 super(activity, state, providers, docs, searchMgr, executors, injector);
Steve McKayc8889af2016-09-23 11:22:41 -0700115
Steve McKaybd9f05a2016-10-10 10:18:36 -0700116 mActionModeAddons = actionModeAddons;
Steve McKay98f8c5f2017-03-03 13:52:14 -0800117 mFeatures = injector.features;
118 mConfig = injector.config;
Jon Mann30d8c792017-02-21 17:44:49 -0800119 mDialogs = injector.dialogs;
Steve McKay739f94b2016-09-22 14:54:23 -0700120 mClipper = clipper;
Steve McKayc8889af2016-09-23 11:22:41 -0700121 mClipStore = clipStore;
Garfield Tanda2c0f02017-04-11 13:47:58 -0700122 mDragAndDropManager = dragAndDropManager;
Garfield Tane9670332017-03-06 18:33:23 -0800123 mModel = injector.getModel();
Garfield Tan7d75f7b2016-09-20 16:33:24 -0700124 }
125
126 @Override
Ben Lin174fc2e2017-03-01 17:53:20 -0800127 public boolean dropOn(DragEvent event, RootInfo root) {
Ben Lind6a85b92017-03-13 16:26:26 -0700128 if (!root.supportsCreate() || root.isLibrary()) {
129 return false;
130 }
131
Ben Lin30b0dc12017-03-07 15:37:16 -0800132 // DragEvent gets recycled, so it is possible that by the time the callback is called,
133 // event.getLocalState() and event.getClipData() returns null. Thus, we want to save
134 // references to ensure they are non null.
135 final ClipData clipData = event.getClipData();
136 final Object localState = event.getLocalState();
Garfield Tanb285b402016-09-21 17:12:18 -0700137
Garfield Tanda2c0f02017-04-11 13:47:58 -0700138 return mDragAndDropManager.drop(
139 clipData, localState, root, this, mDialogs::showFileOperationStatus);
Ben Lin174fc2e2017-03-01 17:53:20 -0800140 }
141
Steve McKay739f94b2016-09-22 14:54:23 -0700142 @Override
Steve McKay5b0a2c12016-10-07 11:22:31 -0700143 public void openSelectedInNewWindow() {
Riddle Hsu0c375982018-06-21 22:06:43 +0800144 Selection<String> selection = getStableSelection();
Tony Huange2ad38f2019-07-30 14:43:05 +0800145 if (selection.isEmpty()) {
146 return;
147 }
148
Steve McKay5b0a2c12016-10-07 11:22:31 -0700149 assert(selection.size() == 1);
Ben Linf23cbf42016-12-22 15:12:13 -0800150 DocumentInfo doc = mModel.getDocument(selection.iterator().next());
Steve McKay5b0a2c12016-10-07 11:22:31 -0700151 assert(doc != null);
152 openInNewWindow(new DocumentStack(mState.stack, doc));
153 }
154
155 @Override
Steve McKay739f94b2016-09-22 14:54:23 -0700156 public void openSettings(RootInfo root) {
shawnlin9cee68f2019-01-25 11:20:18 +0800157 Metrics.logUserAction(MetricConsts.USER_ACTION_SETTINGS);
Steve McKay739f94b2016-09-22 14:54:23 -0700158 final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
159 intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
160 mActivity.startActivity(intent);
Garfield Tanb285b402016-09-21 17:12:18 -0700161 }
162
163 @Override
164 public void pasteIntoFolder(RootInfo root) {
Ben Lin30b0dc12017-03-07 15:37:16 -0800165 this.getRootDocument(
Garfield Tanb285b402016-09-21 17:12:18 -0700166 root,
Ben Lin30b0dc12017-03-07 15:37:16 -0800167 TimeoutTask.DEFAULT_TIMEOUT,
168 (DocumentInfo doc) -> pasteIntoFolder(root, doc));
Garfield Tanb285b402016-09-21 17:12:18 -0700169 }
170
Garfield Tane9670332017-03-06 18:33:23 -0800171 private void pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc) {
Garfield Tanb285b402016-09-21 17:12:18 -0700172 DocumentStack stack = new DocumentStack(root, doc);
Ben Lin30b0dc12017-03-07 15:37:16 -0800173 mClipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
Garfield Tanb285b402016-09-21 17:12:18 -0700174 }
175
176 @Override
Jon Mann30d8c792017-02-21 17:44:49 -0800177 public @Nullable DocumentInfo renameDocument(String name, DocumentInfo document) {
178 ContentResolver resolver = mActivity.getContentResolver();
179 ContentProviderClient client = null;
180
181 try {
182 client = DocumentsApplication.acquireUnstableProviderOrThrow(
183 resolver, document.derivedUri.getAuthority());
184 Uri newUri = DocumentsContract.renameDocument(
Jeff Sharkey2c0b4852019-02-15 15:53:47 -0700185 wrap(client), document.derivedUri, name);
Jon Mann30d8c792017-02-21 17:44:49 -0800186 return DocumentInfo.fromUri(resolver, newUri);
187 } catch (Exception e) {
188 Log.w(TAG, "Failed to rename file", e);
189 return null;
190 } finally {
Jeff Sharkeybb68a652019-02-19 11:17:30 -0700191 FileUtils.closeQuietly(client);
Jon Mann30d8c792017-02-21 17:44:49 -0800192 }
193 }
194
195 @Override
Steve McKay739f94b2016-09-22 14:54:23 -0700196 public void openRoot(RootInfo root) {
shawnlin9cee68f2019-01-25 11:20:18 +0800197 Metrics.logRootVisited(MetricConsts.FILES_SCOPE, root);
Steve McKay739f94b2016-09-22 14:54:23 -0700198 mActivity.onRootPicked(root);
199 }
200
201 @Override
Riddle Hsu0c375982018-06-21 22:06:43 +0800202 public boolean openItem(ItemDetails<String> details, @ViewType int type,
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900203 @ViewType int fallback) {
Riddle Hsu0c375982018-06-21 22:06:43 +0800204 DocumentInfo doc = mModel.getDocument(details.getSelectionKey());
Steve McKay6d20d192016-09-21 17:57:10 -0700205 if (doc == null) {
Riddle Hsu0c375982018-06-21 22:06:43 +0800206 Log.w(TAG, "Can't view item. No Document available for modeId: "
207 + details.getSelectionKey());
Steve McKay6d20d192016-09-21 17:57:10 -0700208 return false;
209 }
Tony Huang2a022ba2019-01-10 20:02:52 +0800210 mInjector.searchManager.recordHistory();
Steve McKay6d20d192016-09-21 17:57:10 -0700211
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900212 return openDocument(doc, type, fallback);
213 }
214
215 // TODO: Make this private and make tests call openDocument(DocumentDetails, int, int) instead.
216 @VisibleForTesting
217 public boolean openDocument(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
Steve McKay98f8c5f2017-03-03 13:52:14 -0800218 if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900219 onDocumentPicked(doc, type, fallback);
Steve McKay5b0a2c12016-10-07 11:22:31 -0700220 mSelectionMgr.clearSelection();
Tony Huang71dac092019-06-14 11:37:32 +0800221 return !doc.isContainer();
Steve McKay6d20d192016-09-21 17:57:10 -0700222 }
223 return false;
224 }
225
226 @Override
Ben Lind8d0ad22017-01-11 13:30:50 -0800227 public void springOpenDirectory(DocumentInfo doc) {
228 assert(doc.isDirectory());
229 mActionModeAddons.finishActionMode();
230 openContainerDocument(doc);
231 }
232
Riddle Hsu0c375982018-06-21 22:06:43 +0800233 private Selection<String> getSelectedOrFocused() {
234 final MutableSelection<String> selection = this.getStableSelection();
Ben Lind947f012016-10-18 14:32:49 -0700235 if (selection.isEmpty()) {
236 String focusModelId = mFocusHandler.getFocusModelId();
237 if (focusModelId != null) {
238 selection.add(focusModelId);
239 }
240 }
241
242 return selection;
243 }
244
245 @Override
246 public void cutToClipboard() {
shawnlin9cee68f2019-01-25 11:20:18 +0800247 Metrics.logUserAction(MetricConsts.USER_ACTION_CUT_CLIPBOARD);
Riddle Hsu0c375982018-06-21 22:06:43 +0800248 Selection<String> selection = getSelectedOrFocused();
Ben Lind947f012016-10-18 14:32:49 -0700249
250 if (selection.isEmpty()) {
251 return;
252 }
Ben Linc1a32ae2017-04-19 15:19:49 -0700253
254 if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
255 mDialogs.showOperationUnsupported();
256 return;
257 }
258
Ben Lind947f012016-10-18 14:32:49 -0700259 mSelectionMgr.clearSelection();
260
Ben Linf23cbf42016-12-22 15:12:13 -0800261 mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
Ben Lind947f012016-10-18 14:32:49 -0700262
263 mDialogs.showDocumentsClipped(selection.size());
264 }
265
266 @Override
267 public void copyToClipboard() {
shawnlin9cee68f2019-01-25 11:20:18 +0800268 Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_CLIPBOARD);
Riddle Hsu0c375982018-06-21 22:06:43 +0800269 Selection<String> selection = getSelectedOrFocused();
Ben Lind947f012016-10-18 14:32:49 -0700270
271 if (selection.isEmpty()) {
272 return;
273 }
274 mSelectionMgr.clearSelection();
275
Ben Linf23cbf42016-12-22 15:12:13 -0800276 mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
Ben Lind947f012016-10-18 14:32:49 -0700277
278 mDialogs.showDocumentsClipped(selection.size());
279 }
280
Jon Mann253a9922017-03-21 18:53:27 -0700281 @Override
282 public void viewInOwner() {
shawnlin9cee68f2019-01-25 11:20:18 +0800283 Metrics.logUserAction(MetricConsts.USER_ACTION_VIEW_IN_APPLICATION);
Riddle Hsu0c375982018-06-21 22:06:43 +0800284 Selection<String> selection = getSelectedOrFocused();
Jon Mann253a9922017-03-21 18:53:27 -0700285
286 if (selection.isEmpty() || selection.size() > 1) {
287 return;
288 }
289 DocumentInfo doc = mModel.getDocument(selection.iterator().next());
290 Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
Jon Mann9bd40992017-03-24 12:34:34 -0700291 intent.setPackage(mProviders.getPackageName(doc.authority));
Jon Mann253a9922017-03-21 18:53:27 -0700292 intent.addCategory(Intent.CATEGORY_DEFAULT);
293 intent.setData(doc.derivedUri);
294 try {
295 mActivity.startActivity(intent);
296 } catch (ActivityNotFoundException e) {
297 Log.e(TAG, "Failed to view settings in application for " + doc.derivedUri, e);
298 mDialogs.showNoApplicationFound();
299 }
300 }
301
Steve McKayc8889af2016-09-23 11:22:41 -0700302 @Override
shawnlin8dafe612019-08-14 20:10:18 +0800303 public void showDeleteDialog() {
shawnlin48200cd2018-10-16 16:20:42 +0800304 Selection selection = getSelectedOrFocused();
Ben Lind947f012016-10-18 14:32:49 -0700305 if (selection.isEmpty()) {
306 return;
307 }
Steve McKayc8889af2016-09-23 11:22:41 -0700308
shawnlin8dafe612019-08-14 20:10:18 +0800309 DeleteDocumentFragment.show(mActivity.getSupportFragmentManager(),
310 mModel.getDocuments(selection),
311 mState.stack.peek());
312 }
Steve McKayc8889af2016-09-23 11:22:41 -0700313
Steve McKayc8889af2016-09-23 11:22:41 -0700314
shawnlin8dafe612019-08-14 20:10:18 +0800315 @Override
316 public void deleteSelectedDocuments(List<DocumentInfo> docs, DocumentInfo srcParent) {
317 if (docs == null || docs.isEmpty()) {
318 return;
319 }
shawnlin65bbe792018-08-20 16:20:43 +0800320
shawnlin8dafe612019-08-14 20:10:18 +0800321 mActionModeAddons.finishActionMode();
shawnlin48200cd2018-10-16 16:20:42 +0800322
shawnlin8dafe612019-08-14 20:10:18 +0800323 List<Uri> uris = new ArrayList<>(docs.size());
324 for (DocumentInfo doc : docs) {
325 uris.add(doc.derivedUri);
326 }
shawnlin48200cd2018-10-16 16:20:42 +0800327
shawnlin8dafe612019-08-14 20:10:18 +0800328 UrisSupplier srcs;
329 try {
330 srcs = UrisSupplier.create(
331 uris,
332 mClipStore);
333 } catch (Exception e) {
334 Log.e(TAG, "Failed to delete a file because we were unable to get item URIs.", e);
335 mDialogs.showFileOperationStatus(
336 FileOperations.Callback.STATUS_FAILED,
337 FileOperationService.OPERATION_DELETE,
338 uris.size());
339 return;
340 }
shawnlin48200cd2018-10-16 16:20:42 +0800341
shawnlin8dafe612019-08-14 20:10:18 +0800342 FileOperation operation = new FileOperation.Builder()
343 .withOpType(FileOperationService.OPERATION_DELETE)
344 .withDestination(mState.stack)
345 .withSrcs(srcs)
346 .withSrcParent(srcParent == null ? null : srcParent.derivedUri)
347 .build();
shawnlinea3de8b2018-08-03 16:20:55 +0800348
shawnlin8dafe612019-08-14 20:10:18 +0800349 FileOperations.start(mActivity, operation, mDialogs::showFileOperationStatus,
350 FileOperations.createJobId());
Steve McKayc8889af2016-09-23 11:22:41 -0700351 }
352
Steve McKay988d8a32016-09-27 09:41:17 -0700353 @Override
Steve McKayd0718952016-10-10 13:43:36 -0700354 public void shareSelectedDocuments() {
shawnlin9cee68f2019-01-25 11:20:18 +0800355 Metrics.logUserAction(MetricConsts.USER_ACTION_SHARE);
Steve McKayd0718952016-10-10 13:43:36 -0700356
Riddle Hsu0c375982018-06-21 22:06:43 +0800357 Selection<String> selection = getStableSelection();
Tony Huange2ad38f2019-07-30 14:43:05 +0800358 if (selection.isEmpty()) {
359 return;
Tony Huangff4915e2019-10-04 15:07:19 +0800360 } else if (selection.size() > SHARE_FILES_COUNT_LIMIT) {
361 mDialogs.showShareOverLimit(SHARE_FILES_COUNT_LIMIT);
362 return;
Tony Huange2ad38f2019-07-30 14:43:05 +0800363 }
Steve McKayd0718952016-10-10 13:43:36 -0700364
365 // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
Steve McKay98f8c5f2017-03-03 13:52:14 -0800366 List<DocumentInfo> docs = mModel.loadDocuments(
367 selection, DocumentFilters.sharable(mFeatures));
Steve McKayd0718952016-10-10 13:43:36 -0700368
369 Intent intent;
370
371 if (docs.size() == 1) {
Steve McKayd0718952016-10-10 13:43:36 -0700372 intent = new Intent(Intent.ACTION_SEND);
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +0900373 DocumentInfo doc = docs.get(0);
Steve McKayd0718952016-10-10 13:43:36 -0700374 intent.setType(doc.mimeType);
375 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
376
377 } else if (docs.size() > 1) {
378 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
Steve McKayd0718952016-10-10 13:43:36 -0700379
380 final ArrayList<String> mimeTypes = new ArrayList<>();
381 final ArrayList<Uri> uris = new ArrayList<>();
382 for (DocumentInfo doc : docs) {
383 mimeTypes.add(doc.mimeType);
384 uris.add(doc.derivedUri);
385 }
386
387 intent.setType(MimeTypes.findCommonMimeType(mimeTypes));
388 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
389
390 } else {
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +0900391 // Everything filtered out, nothing to share.
Steve McKayd0718952016-10-10 13:43:36 -0700392 return;
393 }
394
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +0900395 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
396 intent.addCategory(Intent.CATEGORY_DEFAULT);
Tomasz Mikolajewski1abcfd12016-12-12 10:08:49 +0900397
Steve McKay98f8c5f2017-03-03 13:52:14 -0800398 if (mFeatures.isVirtualFilesSharingEnabled()
399 && mModel.hasDocuments(selection, DocumentFilters.VIRTUAL)) {
Tomasz Mikolajewski5b127ac2016-10-24 18:24:58 +0900400 intent.addCategory(Intent.CATEGORY_TYPED_OPENABLE);
401 }
402
Steve McKayd0718952016-10-10 13:43:36 -0700403 Intent chooserIntent = Intent.createChooser(
404 intent, mActivity.getResources().getText(R.string.share_via));
405
406 mActivity.startActivity(chooserIntent);
407 }
408
409 @Override
shawnlinea3de8b2018-08-03 16:20:55 +0800410 public void loadDocumentsForCurrentStack() {
shawnlinea3de8b2018-08-03 16:20:55 +0800411 super.loadDocumentsForCurrentStack();
412 }
413
414 @Override
Steve McKay988d8a32016-09-27 09:41:17 -0700415 public void initLocation(Intent intent) {
416 assert(intent != null);
417
Garfield Tan40c85052017-02-02 12:44:35 -0800418 // stack is initialized if it's restored from bundle, which means we're restoring a
419 // previously stored state.
420 if (mState.stack.isInitialized()) {
Jason Chang96f886b2019-03-29 17:59:02 +0800421 if (DEBUG) {
422 Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
423 }
Garfield Tan5f2a9ba2017-05-26 14:35:44 -0700424 restoreRootAndDirectory();
Steve McKay988d8a32016-09-27 09:41:17 -0700425 return;
426 }
427
Garfield Tan40c85052017-02-02 12:44:35 -0800428 if (launchToStackLocation(intent)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800429 if (DEBUG) {
430 Log.d(TAG, "Launched to location from stack.");
431 }
Steve McKay988d8a32016-09-27 09:41:17 -0700432 return;
433 }
434
Steve McKay988d8a32016-09-27 09:41:17 -0700435 if (launchToRoot(intent)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800436 if (DEBUG) {
437 Log.d(TAG, "Launched to root for browsing.");
438 }
Steve McKay988d8a32016-09-27 09:41:17 -0700439 return;
440 }
441
Garfield Tanf8969d62017-02-02 16:55:55 -0800442 if (launchToDocument(intent)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800443 if (DEBUG) {
444 Log.d(TAG, "Launched to a document.");
445 }
Garfield Tanf8969d62017-02-02 16:55:55 -0800446 return;
447 }
448
Tony Huangbb6d8f62019-02-27 14:26:34 +0800449 if (launchToDownloads(intent)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800450 if (DEBUG) {
451 Log.d(TAG, "Launched to a downloads.");
452 }
Tony Huangbb6d8f62019-02-27 14:26:34 +0800453 return;
454 }
455
Jason Chang96f886b2019-03-29 17:59:02 +0800456 if (DEBUG) {
457 Log.d(TAG, "Launching directly into Home directory.");
458 }
Tony Huang4e6242a2019-01-25 11:45:31 +0800459 launchToDefaultLocation();
Steve McKay988d8a32016-09-27 09:41:17 -0700460 }
461
Garfield Tanf8969d62017-02-02 16:55:55 -0800462 @Override
463 protected void launchToDefaultLocation() {
Tony Huang4e6242a2019-01-25 11:45:31 +0800464 if (mFeatures.isDefaultRootInBrowseEnabled()) {
465 loadHomeDir();
466 } else {
467 loadRecent();
468 }
Garfield Tanf8969d62017-02-02 16:55:55 -0800469 }
470
Garfield Tan40c85052017-02-02 12:44:35 -0800471 // If EXTRA_STACK is not null in intent, we'll skip other means of loading
472 // or restoring the stack (like URI).
Steve McKay988d8a32016-09-27 09:41:17 -0700473 //
474 // When restoring from a stack, if a URI is present, it should only ever be:
475 // -- a launch URI: Launch URIs support sensible activity management,
476 // but don't specify a real content target)
477 // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
478 //
479 // Any other URI is *sorta* unexpected...except when browsing an archive
480 // in downloads.
Garfield Tan40c85052017-02-02 12:44:35 -0800481 private boolean launchToStackLocation(Intent intent) {
482 DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
Garfield Tan2a837422016-10-19 11:50:45 -0700483 if (stack == null || stack.getRoot() == null) {
Steve McKay988d8a32016-09-27 09:41:17 -0700484 return false;
485 }
486
Garfield Tan40c85052017-02-02 12:44:35 -0800487 mState.stack.reset(stack);
Steve McKay988d8a32016-09-27 09:41:17 -0700488 if (mState.stack.isEmpty()) {
Garfield Tan2a837422016-10-19 11:50:45 -0700489 mActivity.onRootPicked(mState.stack.getRoot());
Steve McKay988d8a32016-09-27 09:41:17 -0700490 } else {
491 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
492 }
493
494 return true;
495 }
496
Steve McKay988d8a32016-09-27 09:41:17 -0700497 private boolean launchToRoot(Intent intent) {
Garfield Tanc5efea02017-02-22 12:58:29 -0800498 String action = intent.getAction();
Garfield Tan83392022017-05-31 18:05:40 -0700499 if (Intent.ACTION_VIEW.equals(action)) {
Steve McKay988d8a32016-09-27 09:41:17 -0700500 Uri uri = intent.getData();
501 if (DocumentsContract.isRootUri(mActivity, uri)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800502 if (DEBUG) {
503 Log.d(TAG, "Launching with root URI.");
504 }
Steve McKay988d8a32016-09-27 09:41:17 -0700505 // If we've got a specific root to display, restore that root using a dedicated
506 // authority. That way a misbehaving provider won't result in an ANR.
507 loadRoot(uri);
508 return true;
Ivan Chiang9b9a2822018-09-19 17:03:22 +0800509 } else if (DocumentsContract.isRootsUri(mActivity, uri)) {
Jason Chang96f886b2019-03-29 17:59:02 +0800510 if (DEBUG) {
511 Log.d(TAG, "Launching first root with roots URI.");
512 }
Ivan Chiang9b9a2822018-09-19 17:03:22 +0800513 // TODO: b/116760996 Let the user can disambiguate between roots if there are
514 // multiple from DocumentsProvider instead of launching the first root in default
515 loadFirstRoot(uri);
516 return true;
Steve McKay988d8a32016-09-27 09:41:17 -0700517 }
518 }
519 return false;
520 }
521
Garfield Tanf8969d62017-02-02 16:55:55 -0800522 private boolean launchToDocument(Intent intent) {
Garfield Tanc5efea02017-02-22 12:58:29 -0800523 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Garfield Tanf8969d62017-02-02 16:55:55 -0800524 Uri uri = intent.getData();
525 if (DocumentsContract.isDocumentUri(mActivity, uri)) {
526 return launchToDocument(intent.getData());
527 }
528 }
529
530 return false;
531 }
532
Tony Huangbb6d8f62019-02-27 14:26:34 +0800533 private boolean launchToDownloads(Intent intent) {
534 if (DownloadManager.ACTION_VIEW_DOWNLOADS.equals(intent.getAction())) {
535 Uri uri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
536 Providers.ROOT_ID_DOWNLOADS);
537 loadRoot(uri);
538 return true;
539 }
540
541 return false;
542 }
543
Garfield Tan208945c2016-10-04 14:36:38 -0700544 @Override
Steve McKayeed2f4e2016-10-03 20:30:52 -0700545 public void showChooserForDoc(DocumentInfo doc) {
Tomasz Mikolajewskiac3e63e2017-02-13 10:08:58 +0900546 assert(!doc.isDirectory());
Steve McKayeed2f4e2016-10-03 20:30:52 -0700547
548 if (manageDocument(doc)) {
549 Log.w(TAG, "Open with is not yet supported for managed doc.");
550 return;
551 }
552
553 Intent intent = Intent.createChooser(buildViewIntent(doc), null);
Tony Huange6bdbeb2018-11-07 17:56:33 +0800554 intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
Steve McKayeed2f4e2016-10-03 20:30:52 -0700555 try {
556 mActivity.startActivity(intent);
557 } catch (ActivityNotFoundException e) {
558 mDialogs.showNoApplicationFound();
559 }
560 }
561
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900562 private void onDocumentPicked(DocumentInfo doc, @ViewType int type, @ViewType int fallback) {
Steve McKayeed2f4e2016-10-03 20:30:52 -0700563 if (doc.isContainer()) {
Garfield Tan63bf8132016-10-11 11:00:49 -0700564 openContainerDocument(doc);
Steve McKayeed2f4e2016-10-03 20:30:52 -0700565 return;
566 }
567
568 if (manageDocument(doc)) {
569 return;
570 }
571
Geoffrey Pitsch71bc9812018-01-09 15:38:24 -0500572 // For APKs, even if the type is preview, we send an ACTION_VIEW intent to allow
573 // PackageManager to install it. This allows users to install APKs from any root.
574 // The Downloads special case is handled above in #manageDocument.
575 if (MimeTypes.isApkType(doc.mimeType)) {
576 viewDocument(doc);
577 return;
578 }
579
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900580 switch (type) {
581 case VIEW_TYPE_REGULAR:
582 if (viewDocument(doc)) {
583 return;
584 }
585 break;
586
587 case VIEW_TYPE_PREVIEW:
588 if (previewDocument(doc)) {
589 return;
590 }
591 break;
592
593 default:
594 throw new IllegalArgumentException("Illegal view type.");
Steve McKayeed2f4e2016-10-03 20:30:52 -0700595 }
596
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900597 switch (fallback) {
598 case VIEW_TYPE_REGULAR:
599 if (viewDocument(doc)) {
600 return;
601 }
602 break;
603
604 case VIEW_TYPE_PREVIEW:
605 if (previewDocument(doc)) {
606 return;
607 }
608 break;
609
610 case VIEW_TYPE_NONE:
611 break;
612
613 default:
614 throw new IllegalArgumentException("Illegal fallback view type.");
615 }
616
617 // Failed to view including fallback, and it's in an archive.
618 if (type != VIEW_TYPE_NONE && fallback != VIEW_TYPE_NONE && doc.isInArchive()) {
619 mDialogs.showViewInArchivesUnsupported();
620 }
Steve McKayeed2f4e2016-10-03 20:30:52 -0700621 }
622
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900623 private boolean viewDocument(DocumentInfo doc) {
Steve McKayeed2f4e2016-10-03 20:30:52 -0700624 if (doc.isPartial()) {
625 Log.w(TAG, "Can't view partial file.");
626 return false;
627 }
628
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +0900629 if (doc.isInArchive()) {
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900630 Log.w(TAG, "Can't view files in archives.");
Tomasz Mikolajewskie535c572016-11-25 13:58:35 +0900631 return false;
632 }
633
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900634 if (doc.isDirectory()) {
635 Log.w(TAG, "Can't view directories.");
Steve McKayeed2f4e2016-10-03 20:30:52 -0700636 return true;
637 }
638
Steve McKayeed2f4e2016-10-03 20:30:52 -0700639 Intent intent = buildViewIntent(doc);
640 if (DEBUG && intent.getClipData() != null) {
641 Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
642 }
643
644 try {
645 mActivity.startActivity(intent);
646 return true;
647 } catch (ActivityNotFoundException e) {
648 mDialogs.showNoApplicationFound();
649 }
650 return false;
651 }
652
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900653 private boolean previewDocument(DocumentInfo doc) {
Steve McKayeed2f4e2016-10-03 20:30:52 -0700654 if (doc.isPartial()) {
655 Log.w(TAG, "Can't view partial file.");
656 return false;
657 }
658
659 Intent intent = new QuickViewIntentBuilder(
660 mActivity.getPackageManager(),
661 mActivity.getResources(),
662 doc,
Tony Huang7a7e7df2018-11-06 17:16:47 +0800663 mModel,
664 false /* fromPicker */).build();
Steve McKayeed2f4e2016-10-03 20:30:52 -0700665
666 if (intent != null) {
667 // TODO: un-work around issue b/24963914. Should be fixed soon.
668 try {
669 mActivity.startActivity(intent);
670 return true;
671 } catch (SecurityException e) {
672 // Carry on to regular view mode.
673 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
674 }
675 }
676
677 return false;
678 }
679
680 private boolean manageDocument(DocumentInfo doc) {
681 if (isManagedDownload(doc)) {
682 // First try managing the document; we expect manager to filter
683 // based on authority, so we don't grant.
684 Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
685 manage.setData(doc.derivedUri);
686 try {
687 mActivity.startActivity(manage);
688 return true;
689 } catch (ActivityNotFoundException ex) {
690 // Fall back to regular handling.
691 }
692 }
693
694 return false;
695 }
696
697 private boolean isManagedDownload(DocumentInfo doc) {
698 // Anything on downloads goes through the back through downloads manager
699 // (that's the MANAGE_DOCUMENT bit).
700 // This is done for two reasons:
701 // 1) The file in question might be a failed/queued or otherwise have some
702 // specialized download handling.
703 // 2) For APKs, the download manager will add on some important security stuff
704 // like origin URL.
705 // 3) For partial files, the download manager will offer to restart/retry downloads.
706
707 // All other files not on downloads, event APKs, would get no benefit from this
708 // treatment, thusly the "isDownloads" check.
709
710 // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
Tony Huang74d3a2d2019-06-27 14:44:09 +0800711 // files in archives or in child folders. Also, if the activity is already browsing
712 // a ZIP from downloads, then skip MANAGE_DOCUMENTS.
Steve McKayeed2f4e2016-10-03 20:30:52 -0700713 if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
714 && mState.stack.size() > 1) {
715 // viewing the contents of an archive.
716 return false;
717 }
718
Tony Huang74d3a2d2019-06-27 14:44:09 +0800719 // management is only supported in Downloads root or downloaded files show in Recent root.
720 if (Providers.AUTHORITY_DOWNLOADS.equals(doc.authority)) {
721 // only on APKs or partial files.
722 return MimeTypes.isApkType(doc.mimeType) || doc.isPartial();
Steve McKayeed2f4e2016-10-03 20:30:52 -0700723 }
724
725 return false;
726 }
727
728 private Intent buildViewIntent(DocumentInfo doc) {
729 Intent intent = new Intent(Intent.ACTION_VIEW);
730 intent.setDataAndType(doc.derivedUri, doc.mimeType);
731
732 // Downloads has traditionally added the WRITE permission
733 // in the TrampolineActivity. Since this behavior is long
734 // established, we set the same permission for non-managed files
735 // This ensures consistent behavior between the Downloads root
736 // and other roots.
Jason Chang02d46d92019-08-01 14:48:09 +0800737 int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP;
Steve McKayeed2f4e2016-10-03 20:30:52 -0700738 if (doc.isWriteSupported()) {
739 flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
740 }
741 intent.setFlags(flags);
742
743 return intent;
744 }
745
Dooper0930d4c2017-06-02 10:32:00 -0700746 @Override
Austin Kolanderf5042d02017-06-08 09:20:30 -0700747 public void showInspector(DocumentInfo doc) {
shawnlin9cee68f2019-01-25 11:20:18 +0800748 Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR);
Steve McKayf433d202017-07-12 18:46:09 -0700749 Intent intent = new Intent(mActivity, InspectorActivity.class);
Steve McKayefd10ac2017-07-25 13:33:57 -0700750 intent.setData(doc.derivedUri);
751
752 // permit the display of debug info about the file.
Steve McKay84440eb2017-06-23 12:35:15 -0700753 intent.putExtra(
754 Shared.EXTRA_SHOW_DEBUG,
Steve McKay7bd92f72017-07-25 17:21:53 -0700755 mFeatures.isDebugSupportEnabled() &&
Jeff Sharkey94785ef2018-07-09 16:37:41 -0600756 (DEBUG || DebugFlags.getDocumentDetailsEnabled()));
Steve McKayefd10ac2017-07-25 13:33:57 -0700757
758 // The "root document" (top level folder in a root) don't usually have a
759 // human friendly display name. That's because we've never shown the root
760 // folder's name to anyone.
761 // For that reason when the doc being inspected is the root folder,
762 // we override the displayName of the doc w/ the Root's name instead.
763 // The Root's name is shown to the user in the sidebar.
764 if (doc.isDirectory() && mState.stack.size() == 1 && mState.stack.get(0).equals(doc)) {
765 RootInfo root = mActivity.getCurrentRoot();
766 // Recents root title isn't defined, but inspector is disabled for recents root folder.
767 assert !TextUtils.isEmpty(root.title);
768 intent.putExtra(Intent.EXTRA_TITLE, root.title);
769 }
Dooper0930d4c2017-06-02 10:32:00 -0700770 mActivity.startActivity(intent);
771 }
772
Steve McKayc8889af2016-09-23 11:22:41 -0700773 public interface Addons extends CommonAddons {
Steve McKayc8889af2016-09-23 11:22:41 -0700774 }
Garfield Tan7d75f7b2016-09-20 16:33:24 -0700775}