blob: e1b529e4deb8921b0603b512892c59c99fffb5ac [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
17package com.android.documentsui.picker;
18
Steve McKay988d8a32016-09-27 09:41:17 -070019import static com.android.documentsui.base.Shared.DEBUG;
Garfield Tan23ac60c2017-03-13 17:40:43 -070020import static com.android.documentsui.base.State.ACTION_CREATE;
21import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
Garfield Tan894d4872017-03-17 12:23:22 -070022import static com.android.documentsui.base.State.ACTION_OPEN;
Garfield Tan23ac60c2017-03-13 17:40:43 -070023import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
24import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
Steve McKay988d8a32016-09-27 09:41:17 -070025
Steve McKayc8889af2016-09-23 11:22:41 -070026import android.app.Activity;
Garfield Tan23ac60c2017-03-13 17:40:43 -070027import android.app.FragmentManager;
28import android.content.ClipData;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070029import android.content.Intent;
30import android.content.pm.ResolveInfo;
31import android.net.Uri;
Garfield Tan23ac60c2017-03-13 17:40:43 -070032import android.os.AsyncTask;
33import android.os.Parcelable;
Garfield Tanfc726172016-11-04 17:49:29 -070034import android.provider.DocumentsContract;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070035import android.provider.Settings;
Steve McKay6d20d192016-09-21 17:57:10 -070036import android.util.Log;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070037
Steve McKay739f94b2016-09-22 14:54:23 -070038import com.android.documentsui.AbstractActionHandler;
Steve McKay4f78ba62016-10-04 16:48:49 -070039import com.android.documentsui.ActivityConfig;
Steve McKaydef48682016-10-03 09:07:38 -070040import com.android.documentsui.DocumentsAccess;
Jon Mann30d8c792017-02-21 17:44:49 -080041import com.android.documentsui.Injector;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070042import com.android.documentsui.Metrics;
Garfield Tan23ac60c2017-03-13 17:40:43 -070043import com.android.documentsui.base.BooleanConsumer;
Steve McKay6d20d192016-09-21 17:57:10 -070044import com.android.documentsui.base.DocumentInfo;
Steve McKay739f94b2016-09-22 14:54:23 -070045import com.android.documentsui.base.DocumentStack;
Steve McKay98f8c5f2017-03-03 13:52:14 -080046import com.android.documentsui.base.Features;
Steve McKay988d8a32016-09-27 09:41:17 -070047import com.android.documentsui.base.Lookup;
Steve McKay739f94b2016-09-22 14:54:23 -070048import com.android.documentsui.base.RootInfo;
Steve McKay0f5ec832016-10-04 14:21:56 -070049import com.android.documentsui.base.Shared;
Steve McKay988d8a32016-09-27 09:41:17 -070050import com.android.documentsui.base.State;
Garfield Tanbab25162017-03-08 18:46:25 -080051import com.android.documentsui.dirlist.AnimationView;
Steve McKay6d20d192016-09-21 17:57:10 -070052import com.android.documentsui.dirlist.DocumentDetails;
Garfield Tane9670332017-03-06 18:33:23 -080053import com.android.documentsui.Model;
Steve McKayc8889af2016-09-23 11:22:41 -070054import com.android.documentsui.picker.ActionHandler.Addons;
Steve McKay3a268232016-10-19 11:15:47 -070055import com.android.documentsui.queries.SearchViewManager;
Steve McKay988d8a32016-09-27 09:41:17 -070056import com.android.documentsui.roots.RootsAccess;
Garfield Tan23ac60c2017-03-13 17:40:43 -070057import com.android.documentsui.services.FileOperationService;
Garfield Tanbab25162017-03-08 18:46:25 -080058import com.android.internal.annotations.VisibleForTesting;
Steve McKay988d8a32016-09-27 09:41:17 -070059
Garfield Tan23ac60c2017-03-13 17:40:43 -070060import java.util.Arrays;
Steve McKay988d8a32016-09-27 09:41:17 -070061import java.util.concurrent.Executor;
Steve McKay6d20d192016-09-21 17:57:10 -070062
63import javax.annotation.Nullable;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070064
65/**
66 * Provides {@link PickActivity} action specializations to fragments.
67 */
Steve McKayc8889af2016-09-23 11:22:41 -070068class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T> {
Garfield Tan7d75f7b2016-09-20 16:33:24 -070069
Steve McKay6d20d192016-09-21 17:57:10 -070070 private static final String TAG = "PickerActionHandler";
71
Steve McKay98f8c5f2017-03-03 13:52:14 -080072 private final Features mFeatures;
Steve McKay5b0a2c12016-10-07 11:22:31 -070073 private final ActivityConfig mConfig;
Garfield Tan23ac60c2017-03-13 17:40:43 -070074 private final Model mModel;
75 private final LastAccessedStorage mLastAccessed;
Steve McKay6d20d192016-09-21 17:57:10 -070076
Steve McKay988d8a32016-09-27 09:41:17 -070077 ActionHandler(
78 T activity,
79 State state,
80 RootsAccess roots,
Steve McKaydef48682016-10-03 09:07:38 -070081 DocumentsAccess docs,
Garfield Tan63bf8132016-10-11 11:00:49 -070082 SearchViewManager searchMgr,
Steve McKay988d8a32016-09-27 09:41:17 -070083 Lookup<String, Executor> executors,
Garfield Tan23ac60c2017-03-13 17:40:43 -070084 Injector injector,
85 LastAccessedStorage lastAccessed) {
Steve McKay988d8a32016-09-27 09:41:17 -070086
Jon Mann30d8c792017-02-21 17:44:49 -080087 super(activity, state, roots, docs, searchMgr, executors, injector);
Steve McKay988d8a32016-09-27 09:41:17 -070088
Jon Mann30d8c792017-02-21 17:44:49 -080089 mConfig = injector.config;
Steve McKay98f8c5f2017-03-03 13:52:14 -080090 mFeatures = injector.features;
Garfield Tane1ddda52017-03-08 17:30:35 -080091 mModel = injector.getModel();
Garfield Tan23ac60c2017-03-13 17:40:43 -070092 mLastAccessed = lastAccessed;
Garfield Tan7d75f7b2016-09-20 16:33:24 -070093 }
94
95 @Override
Steve McKay988d8a32016-09-27 09:41:17 -070096 public void initLocation(Intent intent) {
Garfield Tan2a837422016-10-19 11:50:45 -070097 assert(intent != null);
Steve McKay988d8a32016-09-27 09:41:17 -070098
Garfield Tan40c85052017-02-02 12:44:35 -080099 // stack is initialized if it's restored from bundle, which means we're restoring a
100 // previously stored state.
101 if (mState.stack.isInitialized()) {
Garfield Tan2a837422016-10-19 11:50:45 -0700102 if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
103 return;
Steve McKay988d8a32016-09-27 09:41:17 -0700104 }
Garfield Tan2a837422016-10-19 11:50:45 -0700105
106 // We set the activity title in AsyncTask.onPostExecute().
107 // To prevent talkback from reading aloud the default title, we clear it here.
108 mActivity.setTitle("");
109
110 if (launchHomeForCopyDestination(intent)) {
111 if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination.");
112 return;
113 }
114
Steve McKay98f8c5f2017-03-03 13:52:14 -0800115 if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) {
Garfield Tan2a837422016-10-19 11:50:45 -0700116 if (DEBUG) Log.d(TAG, "Launched to a document.");
117 return;
118 }
119
120 if (DEBUG) Log.d(TAG, "Load last accessed stack.");
121 loadLastAccessedStack();
Steve McKay988d8a32016-09-27 09:41:17 -0700122 }
123
Garfield Tanf8969d62017-02-02 16:55:55 -0800124 @Override
125 protected void launchToDefaultLocation() {
126 loadLastAccessedStack();
127 }
128
Garfield Tan2a837422016-10-19 11:50:45 -0700129 private boolean launchHomeForCopyDestination(Intent intent) {
130 // As a matter of policy we don't load the last used stack for the copy
131 // destination picker (user is already in Files app).
132 // Consensus was that the experice was too confusing.
133 // In all other cases, where the user is visiting us from another app
134 // we restore the stack as last used from that app.
135 if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
136 loadHomeDir();
137 return true;
138 }
139
140 return false;
141 }
142
143 private boolean launchToDocument(Intent intent) {
Garfield Tanf8969d62017-02-02 16:55:55 -0800144 Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
Garfield Tan2a837422016-10-19 11:50:45 -0700145 if (uri != null) {
Garfield Tanf8969d62017-02-02 16:55:55 -0800146 return launchToDocument(uri);
Garfield Tan2a837422016-10-19 11:50:45 -0700147 }
148
149 return false;
150 }
151
Garfield Tan16868832016-09-26 10:01:45 -0700152 private void loadLastAccessedStack() {
153 if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
Garfield Tan23ac60c2017-03-13 17:40:43 -0700154 new LoadLastAccessedStackTask<>(
155 mActivity, mLastAccessed, mState, mRoots, this::onLastAccessedStackLoaded)
Garfield Tanbab25162017-03-08 18:46:25 -0800156 .execute();
157 }
158
Garfield Tan894d4872017-03-17 12:23:22 -0700159 private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) {
Garfield Tanbab25162017-03-08 18:46:25 -0800160 if (stack == null) {
Garfield Tanae937582017-03-09 13:57:07 -0800161 loadDefaultLocation();
Garfield Tanbab25162017-03-08 18:46:25 -0800162 } else {
163 mState.stack.reset(stack);
Garfield Tanae937582017-03-09 13:57:07 -0800164 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
Garfield Tanbab25162017-03-08 18:46:25 -0800165 }
Garfield Tanae937582017-03-09 13:57:07 -0800166 }
Garfield Tanbab25162017-03-08 18:46:25 -0800167
Garfield Tanae937582017-03-09 13:57:07 -0800168 private void loadDefaultLocation() {
169 switch (mState.action) {
Garfield Tan894d4872017-03-17 12:23:22 -0700170 case ACTION_CREATE:
Garfield Tanae937582017-03-09 13:57:07 -0800171 loadHomeDir();
172 break;
Garfield Tan23ac60c2017-03-13 17:40:43 -0700173 case ACTION_GET_CONTENT:
Garfield Tan894d4872017-03-17 12:23:22 -0700174 case ACTION_OPEN:
Garfield Tan23ac60c2017-03-13 17:40:43 -0700175 case ACTION_OPEN_TREE:
Garfield Tanae937582017-03-09 13:57:07 -0800176 mState.stack.changeRoot(mRoots.getRecentsRoot());
177 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
178 break;
179 default:
180 throw new UnsupportedOperationException("Unexpected action type: " + mState.action);
181 }
Garfield Tan16868832016-09-26 10:01:45 -0700182 }
183
Steve McKay988d8a32016-09-27 09:41:17 -0700184 @Override
Garfield Tan7d75f7b2016-09-20 16:33:24 -0700185 public void showAppDetails(ResolveInfo info) {
186 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
187 intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null));
188 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
189 mActivity.startActivity(intent);
190 }
191
192 @Override
Steve McKay739f94b2016-09-22 14:54:23 -0700193 public void openInNewWindow(DocumentStack path) {
194 // Open new window support only depends on vanilla Activity, so it is
195 // implemented in our parent class. But we don't support that in
196 // picking. So as a matter of defensiveness, we override that here.
197 throw new UnsupportedOperationException("Can't open in new window");
198 }
199
200 @Override
201 public void openRoot(RootInfo root) {
Garfield Tan2a8719c2017-01-19 16:17:04 -0800202 Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root);
Steve McKay739f94b2016-09-22 14:54:23 -0700203 mActivity.onRootPicked(root);
204 }
205
206 @Override
Steve McKay6d20d192016-09-21 17:57:10 -0700207 public void openRoot(ResolveInfo info) {
Garfield Tan7d75f7b2016-09-20 16:33:24 -0700208 Metrics.logAppVisited(mActivity, info);
209 mActivity.onAppPicked(info);
210 }
Steve McKay6d20d192016-09-21 17:57:10 -0700211
Steve McKay5f296942016-09-23 08:40:07 -0700212 @Override
Ben Lind8d0ad22017-01-11 13:30:50 -0800213 public void springOpenDirectory(DocumentInfo doc) {
214 }
215
216 @Override
Tomasz Mikolajewskid22cc182017-03-15 16:13:46 +0900217 public boolean openDocument(DocumentDetails details, @ViewType int type,
218 @ViewType int fallback) {
Ben Linf23cbf42016-12-22 15:12:13 -0800219 DocumentInfo doc = mModel.getDocument(details.getModelId());
Steve McKay6d20d192016-09-21 17:57:10 -0700220 if (doc == null) {
221 Log.w(TAG,
222 "Can't view item. No Document available for modeId: " + details.getModelId());
223 return false;
224 }
225
Steve McKay5b0a2c12016-10-07 11:22:31 -0700226 if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
Steve McKayeed2f4e2016-10-03 20:30:52 -0700227 mActivity.onDocumentPicked(doc);
Steve McKay5b0a2c12016-10-07 11:22:31 -0700228 mSelectionMgr.clearSelection();
Steve McKay6d20d192016-09-21 17:57:10 -0700229 return true;
230 }
231 return false;
232 }
233
Garfield Tan23ac60c2017-03-13 17:40:43 -0700234 void pickDocument(DocumentInfo pickTarget) {
235 assert(pickTarget != null);
236 Uri result;
237 switch (mState.action) {
238 case ACTION_OPEN_TREE:
239 result = DocumentsContract.buildTreeDocumentUri(
240 pickTarget.authority, pickTarget.documentId);
241 break;
242 case ACTION_PICK_COPY_DESTINATION:
243 result = pickTarget.derivedUri;
244 break;
245 default:
246 // Should not be reached
247 throw new IllegalStateException("Invalid mState.action");
248 }
249 finishPicking(result);
250 }
251
252 void saveDocument(
253 String mimeType, String displayName, BooleanConsumer inProgressStateListener) {
254 assert(mState.action == ACTION_CREATE);
255 new CreatePickedDocumentTask(
256 mActivity,
Garfield Tan894d4872017-03-17 12:23:22 -0700257 mDocs,
Garfield Tan23ac60c2017-03-13 17:40:43 -0700258 mLastAccessed,
259 mState.stack,
260 mimeType,
261 displayName,
262 inProgressStateListener,
263 this::onPickFinished)
264 .executeOnExecutor(getExecutorForCurrentDirectory());
265 }
266
267 // User requested to overwrite a target. If confirmed by user #finishPicking() will be
268 // called.
269 void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) {
270 assert(mState.action == ACTION_CREATE);
271 assert(replaceTarget != null);
272
273 mInjector.dialogs.confirmOverwrite(fm, replaceTarget);
274 }
275
276 void finishPicking(Uri... docs) {
277 new SetLastAccessedStackTask(
278 mActivity,
279 mLastAccessed,
280 mState.stack,
281 () -> {
282 onPickFinished(docs);
283 }
284 ) .executeOnExecutor(getExecutorForCurrentDirectory());
285 }
286
287 private void onPickFinished(Uri... uris) {
288 if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
289
290 final Intent intent = new Intent();
291 if (uris.length == 1) {
292 intent.setData(uris[0]);
293 } else if (uris.length > 1) {
294 final ClipData clipData = new ClipData(
295 null, mState.acceptMimes, new ClipData.Item(uris[0]));
296 for (int i = 1; i < uris.length; i++) {
297 clipData.addItem(new ClipData.Item(uris[i]));
298 }
299 intent.setClipData(clipData);
300 }
301
302 // TODO: Separate this piece of logic per action.
303 // We don't instantiate different objects for different actions at the first place, so it's
304 // not a easy task to separate this logic cleanly.
305 // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
306 // inheritance structure.
307 if (mState.action == ACTION_GET_CONTENT) {
308 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
309 } else if (mState.action == ACTION_OPEN_TREE) {
310 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
311 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
312 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
313 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
314 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
315 // Picking a copy destination is only used internally by us, so we
316 // don't need to extend permissions to the caller.
317 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
318 intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
319 } else {
320 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
321 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
322 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
323 }
324
Garfield Tan894d4872017-03-17 12:23:22 -0700325 mActivity.setResult(Activity.RESULT_OK, intent, 0);
Garfield Tan23ac60c2017-03-13 17:40:43 -0700326 mActivity.finish();
327 }
328
329 private Executor getExecutorForCurrentDirectory() {
Garfield Tan894d4872017-03-17 12:23:22 -0700330 final DocumentInfo cwd = mState.stack.peek();
Garfield Tan23ac60c2017-03-13 17:40:43 -0700331 if (cwd != null && cwd.authority != null) {
332 return mExecutors.lookup(cwd.authority);
333 } else {
334 return AsyncTask.THREAD_POOL_EXECUTOR;
335 }
336 }
337
Steve McKayc8889af2016-09-23 11:22:41 -0700338 public interface Addons extends CommonAddons {
339 void onAppPicked(ResolveInfo info);
Steve McKayeed2f4e2016-10-03 20:30:52 -0700340 void onDocumentPicked(DocumentInfo doc);
Garfield Tan894d4872017-03-17 12:23:22 -0700341
342 /**
343 * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept
344 * this method call in test environment.
345 */
346 @VisibleForTesting
347 void setResult(int resultCode, Intent result, int notUsed);
Steve McKayc8889af2016-09-23 11:22:41 -0700348 }
Garfield Tan7d75f7b2016-09-20 16:33:24 -0700349}