blob: d49b6894f80f1fe2dce0e0833eef1780a3bf199b [file] [log] [blame]
Garfield Tanda2c0f02017-04-11 13:47:58 -07001/*
2 * Copyright (C) 2017 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;
18
Jeff Sharkeya4ff00f2018-07-09 14:57:51 -060019import androidx.annotation.IntDef;
20import androidx.annotation.Nullable;
Garfield Tanda2c0f02017-04-11 13:47:58 -070021import android.content.ClipData;
22import android.content.Context;
23import android.graphics.drawable.Drawable;
24import android.net.Uri;
25import android.provider.DocumentsContract;
KOUSHIK PANUGANTI6ca7acc2018-04-17 16:00:10 -070026import androidx.annotation.VisibleForTesting;
Garfield Tanda2c0f02017-04-11 13:47:58 -070027import android.view.DragEvent;
28import android.view.KeyEvent;
29import android.view.View;
30
Ben Lin7f7ee102017-05-26 14:22:58 -070031import com.android.documentsui.MenuManager.SelectionDetails;
Garfield Tanda2c0f02017-04-11 13:47:58 -070032import com.android.documentsui.base.DocumentInfo;
33import com.android.documentsui.base.DocumentStack;
Bill Lin49bef292018-11-30 21:51:47 +080034import com.android.documentsui.base.MimeTypes;
Garfield Tanda2c0f02017-04-11 13:47:58 -070035import com.android.documentsui.base.RootInfo;
36import com.android.documentsui.clipping.DocumentClipper;
37import com.android.documentsui.dirlist.IconHelper;
38import com.android.documentsui.services.FileOperationService;
39import com.android.documentsui.services.FileOperationService.OpType;
40import com.android.documentsui.services.FileOperations;
41
42import java.lang.annotation.Retention;
43import java.lang.annotation.RetentionPolicy;
44import java.util.ArrayList;
45import java.util.List;
46
47/**
48 * Manager that tracks control key state, calculates the default file operation (move or copy)
49 * when user drops, and updates drag shadow state.
50 */
51public interface DragAndDropManager {
52
53 @IntDef({ STATE_NOT_ALLOWED, STATE_UNKNOWN, STATE_MOVE, STATE_COPY })
54 @Retention(RetentionPolicy.SOURCE)
55 @interface State {}
56 int STATE_UNKNOWN = 0;
57 int STATE_NOT_ALLOWED = 1;
58 int STATE_MOVE = 2;
59 int STATE_COPY = 3;
60
61 /**
62 * Intercepts and handles a {@link KeyEvent}. Used to track the state of Ctrl key state.
63 */
64 void onKeyEvent(KeyEvent event);
65
66 /**
67 * Starts a drag and drop.
68 *
69 * @param v the view which
70 * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)} will be
71 * called.
Garfield Tanda2c0f02017-04-11 13:47:58 -070072 * @param srcs documents that are dragged
73 * @param root the root in which documents being dragged are
74 * @param invalidDest destinations that don't accept this drag and drop
75 * @param iconHelper used to load document icons
Garfield Tan2e81db62017-04-27 15:06:49 -070076 * @param parent {@link DocumentInfo} of the container of srcs
Garfield Tanda2c0f02017-04-11 13:47:58 -070077 */
78 void startDrag(
79 View v,
Garfield Tanda2c0f02017-04-11 13:47:58 -070080 List<DocumentInfo> srcs,
81 RootInfo root,
82 List<Uri> invalidDest,
Ben Lin7f7ee102017-05-26 14:22:58 -070083 SelectionDetails selectionDetails,
Garfield Tan2e81db62017-04-27 15:06:49 -070084 IconHelper iconHelper,
85 @Nullable DocumentInfo parent);
Garfield Tanda2c0f02017-04-11 13:47:58 -070086
87 /**
88 * Checks whether the document can be spring opened.
89 * @param root the root in which the document is
90 * @param doc the document to check
91 * @return true if policy allows spring opening it; false otherwise
92 */
93 boolean canSpringOpen(RootInfo root, DocumentInfo doc);
94
95 /**
96 * Updates the state to {@link #STATE_NOT_ALLOWED} without any further checks. This is used when
97 * the UI component that handles the drag event already has enough information to disallow
98 * dropping by itself.
99 *
100 * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
101 */
102 void updateStateToNotAllowed(View v);
103
104 /**
105 * Updates the state according to the destination passed.
106 * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
107 * @param destRoot the root of the destination document.
108 * @param destDoc the destination document. Can be null if this is TBD. Must be a folder.
109 * @return the new state. Can be any state in {@link State}.
110 */
111 @State int updateState(
112 View v, RootInfo destRoot, @Nullable DocumentInfo destDoc);
113
114 /**
115 * Resets state back to {@link #STATE_UNKNOWN}. This is used when user drags items leaving a UI
116 * component.
117 * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
118 */
119 void resetState(View v);
120
121 /**
122 * Drops items onto the a root.
123 *
124 * @param clipData the clip data that contains sources information.
125 * @param localState used to determine if this is a multi-window drag and drop.
126 * @param destRoot the target root
127 * @param actions {@link ActionHandler} used to load root document.
128 * @param callback callback called when file operation is rejected or scheduled.
129 * @return true if target accepts this drop; false otherwise
130 */
131 boolean drop(ClipData clipData, Object localState, RootInfo destRoot, ActionHandler actions,
132 FileOperations.Callback callback);
133
134 /**
135 * Drops items onto the target.
136 *
137 * @param clipData the clip data that contains sources information.
138 * @param localState used to determine if this is a multi-window drag and drop.
139 * @param dstStack the document stack pointing to the destination folder.
140 * @param callback callback called when file operation is rejected or scheduled.
141 * @return true if target accepts this drop; false otherwise
142 */
143 boolean drop(ClipData clipData, Object localState, DocumentStack dstStack,
144 FileOperations.Callback callback);
145
146 /**
147 * Called when drag and drop ended.
148 *
149 * This can be called multiple times as multiple {@link View.OnDragListener} might delegate
150 * {@link DragEvent#ACTION_DRAG_ENDED} events to this class so any work inside needs to be
151 * idempotent.
152 */
153 void dragEnded();
154
155 static DragAndDropManager create(Context context, DocumentClipper clipper) {
156 return new RuntimeDragAndDropManager(context, clipper);
157 }
158
159 class RuntimeDragAndDropManager implements DragAndDropManager {
160 private static final String SRC_ROOT_KEY = "dragAndDropMgr:srcRoot";
161
162 private final Context mContext;
163 private final DocumentClipper mClipper;
164 private final DragShadowBuilder mShadowBuilder;
165 private final Drawable mDefaultShadowIcon;
166
167 private @State int mState = STATE_UNKNOWN;
168
169 // Key events info. This is used to derive state when user drags items into a view to derive
170 // type of file operations.
171 private boolean mIsCtrlPressed;
172
173 // Drag events info. These are used to derive state and update drag shadow when user changes
174 // Ctrl key state.
175 private View mView;
176 private List<Uri> mInvalidDest;
177 private ClipData mClipData;
178 private RootInfo mDestRoot;
179 private DocumentInfo mDestDoc;
180
Garfield Tan2e81db62017-04-27 15:06:49 -0700181 // Boolean flag for current drag and drop operation. Returns true if the files can only
182 // be copied (ie. files that don't support delete or remove).
183 private boolean mMustBeCopied;
184
Garfield Tanda2c0f02017-04-11 13:47:58 -0700185 private RuntimeDragAndDropManager(Context context, DocumentClipper clipper) {
186 this(
187 context.getApplicationContext(),
188 clipper,
189 new DragShadowBuilder(context),
Bill Lin49bef292018-11-30 21:51:47 +0800190 IconUtils.loadMimeIcon(context, MimeTypes.GENERIC_TYPE));
Garfield Tanda2c0f02017-04-11 13:47:58 -0700191 }
192
193 @VisibleForTesting
194 RuntimeDragAndDropManager(Context context, DocumentClipper clipper,
195 DragShadowBuilder builder, Drawable defaultShadowIcon) {
196 mContext = context;
197 mClipper = clipper;
198 mShadowBuilder = builder;
199 mDefaultShadowIcon = defaultShadowIcon;
200 }
201
202 @Override
203 public void onKeyEvent(KeyEvent event) {
204 switch (event.getKeyCode()) {
205 case KeyEvent.KEYCODE_CTRL_LEFT:
206 case KeyEvent.KEYCODE_CTRL_RIGHT:
207 adjustCtrlKeyCount(event);
208 }
209 }
210
211 private void adjustCtrlKeyCount(KeyEvent event) {
212 assert(event.getKeyCode() == KeyEvent.KEYCODE_CTRL_LEFT
213 || event.getKeyCode() == KeyEvent.KEYCODE_CTRL_RIGHT);
214
215 mIsCtrlPressed = event.isCtrlPressed();
216
217 // There is an ongoing drag and drop if mView is not null.
218 if (mView != null) {
219 // There is no need to update the state if current state is unknown or not allowed.
220 if (mState == STATE_COPY || mState == STATE_MOVE) {
221 updateState(mView, mDestRoot, mDestDoc);
222 }
223 }
224 }
225
226 @Override
227 public void startDrag(
228 View v,
Garfield Tanda2c0f02017-04-11 13:47:58 -0700229 List<DocumentInfo> srcs,
230 RootInfo root,
231 List<Uri> invalidDest,
Ben Lin7f7ee102017-05-26 14:22:58 -0700232 SelectionDetails selectionDetails,
Garfield Tan2e81db62017-04-27 15:06:49 -0700233 IconHelper iconHelper,
234 @Nullable DocumentInfo parent) {
Garfield Tanda2c0f02017-04-11 13:47:58 -0700235
236 mView = v;
237 mInvalidDest = invalidDest;
Ben Lin7f7ee102017-05-26 14:22:58 -0700238 mMustBeCopied = !selectionDetails.canDelete();
Garfield Tanda2c0f02017-04-11 13:47:58 -0700239
240 List<Uri> uris = new ArrayList<>(srcs.size());
241 for (DocumentInfo doc : srcs) {
242 uris.add(doc.derivedUri);
243 }
Garfield Tan2e81db62017-04-27 15:06:49 -0700244 mClipData = (parent == null)
245 ? mClipper.getClipDataForDocuments(uris, FileOperationService.OPERATION_UNKNOWN)
246 : mClipper.getClipDataForDocuments(
Garfield Tanda2c0f02017-04-11 13:47:58 -0700247 uris, FileOperationService.OPERATION_UNKNOWN, parent);
248 mClipData.getDescription().getExtras()
249 .putString(SRC_ROOT_KEY, root.getUri().toString());
250
251 updateShadow(srcs, iconHelper);
252
Ben Linc0b6b3f2017-05-19 15:11:28 -0700253 int flag = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE;
Ben Lin7f7ee102017-05-26 14:22:58 -0700254 if (!selectionDetails.containsFilesInArchive()) {
Ben Linc0b6b3f2017-05-19 15:11:28 -0700255 flag |= View.DRAG_FLAG_GLOBAL_URI_READ
256 | View.DRAG_FLAG_GLOBAL_URI_WRITE;
257 }
Garfield Tanda2c0f02017-04-11 13:47:58 -0700258 startDragAndDrop(
259 v,
260 mClipData,
261 mShadowBuilder,
262 this, // Used to detect multi-window drag and drop
Ben Linc0b6b3f2017-05-19 15:11:28 -0700263 flag);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700264 }
265
266 private void updateShadow(List<DocumentInfo> srcs, IconHelper iconHelper) {
267 final String title;
268 final Drawable icon;
269
270 final int size = srcs.size();
271 if (size == 1) {
272 DocumentInfo doc = srcs.get(0);
273 title = doc.displayName;
274 icon = iconHelper.getDocumentIcon(mContext, doc);
275 } else {
276 title = mContext.getResources()
277 .getQuantityString(R.plurals.elements_dragged, size, size);
278 icon = mDefaultShadowIcon;
279 }
280
281 mShadowBuilder.updateTitle(title);
282 mShadowBuilder.updateIcon(icon);
283
284 mShadowBuilder.onStateUpdated(STATE_UNKNOWN);
285 }
286
287 /**
288 * A workaround of that
289 * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)} is final.
290 */
291 @VisibleForTesting
292 void startDragAndDrop(View v, ClipData clipData, DragShadowBuilder builder,
293 Object localState, int flags) {
294 v.startDragAndDrop(clipData, builder, localState, flags);
295 }
296
297 @Override
298 public boolean canSpringOpen(RootInfo root, DocumentInfo doc) {
299 return isValidDestination(root, doc.derivedUri);
300 }
301
302 @Override
303 public void updateStateToNotAllowed(View v) {
304 mView = v;
305 updateState(STATE_NOT_ALLOWED);
306 }
307
308 @Override
309 public @State int updateState(
310 View v, RootInfo destRoot, @Nullable DocumentInfo destDoc) {
311
312 mView = v;
313 mDestRoot = destRoot;
314 mDestDoc = destDoc;
315
316 if (!destRoot.supportsCreate()) {
317 updateState(STATE_NOT_ALLOWED);
318 return STATE_NOT_ALLOWED;
319 }
320
321 if (destDoc == null) {
322 updateState(STATE_UNKNOWN);
323 return STATE_UNKNOWN;
324 }
325
326 assert(destDoc.isDirectory());
327
328 if (!destDoc.isCreateSupported() || mInvalidDest.contains(destDoc.derivedUri)) {
329 updateState(STATE_NOT_ALLOWED);
330 return STATE_NOT_ALLOWED;
331 }
332
333 @State int state;
334 final @OpType int opType = calculateOpType(mClipData, destRoot);
335 switch (opType) {
336 case FileOperationService.OPERATION_COPY:
337 state = STATE_COPY;
338 break;
339 case FileOperationService.OPERATION_MOVE:
340 state = STATE_MOVE;
341 break;
342 default:
343 // Should never happen
344 throw new IllegalStateException("Unknown opType: " + opType);
345 }
346
347 updateState(state);
348 return state;
349 }
350
351 @Override
352 public void resetState(View v) {
353 mView = v;
354
355 updateState(STATE_UNKNOWN);
356 }
357
358 private void updateState(@State int state) {
359 mState = state;
360
361 mShadowBuilder.onStateUpdated(state);
362 updateDragShadow(mView);
363 }
364
365 /**
366 * A workaround of that {@link View#updateDragShadow(View.DragShadowBuilder)} is final.
367 */
368 @VisibleForTesting
369 void updateDragShadow(View v) {
370 v.updateDragShadow(mShadowBuilder);
371 }
372
373 @Override
374 public boolean drop(ClipData clipData, Object localState, RootInfo destRoot,
375 ActionHandler action, FileOperations.Callback callback) {
376
377 final Uri rootDocUri =
378 DocumentsContract.buildDocumentUri(destRoot.authority, destRoot.documentId);
379 if (!isValidDestination(destRoot, rootDocUri)) {
380 return false;
381 }
382
Garfield Tan3b987cc2017-04-17 10:13:43 -0700383 // Calculate the op type now just in case user releases Ctrl key while we're obtaining
384 // root document in the background.
385 final @OpType int opType = calculateOpType(clipData, destRoot);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700386 action.getRootDocument(
387 destRoot,
388 TimeoutTask.DEFAULT_TIMEOUT,
389 (DocumentInfo doc) -> {
Garfield Tan3b987cc2017-04-17 10:13:43 -0700390 dropOnRootDocument(clipData, localState, destRoot, doc, opType, callback);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700391 });
392
393 return true;
394 }
395
Garfield Tan3b987cc2017-04-17 10:13:43 -0700396 private void dropOnRootDocument(
397 ClipData clipData,
398 Object localState,
399 RootInfo destRoot,
400 @Nullable DocumentInfo destRootDoc,
401 @OpType int opType,
402 FileOperations.Callback callback) {
Garfield Tanda2c0f02017-04-11 13:47:58 -0700403 if (destRootDoc == null) {
404 callback.onOperationResult(
405 FileOperations.Callback.STATUS_FAILED,
Garfield Tan3b987cc2017-04-17 10:13:43 -0700406 opType,
Garfield Tanda2c0f02017-04-11 13:47:58 -0700407 0);
408 } else {
409 dropChecked(
Garfield Tan3b987cc2017-04-17 10:13:43 -0700410 clipData,
411 localState,
412 new DocumentStack(destRoot, destRootDoc),
413 opType,
414 callback);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700415 }
416 }
417
418 @Override
419 public boolean drop(ClipData clipData, Object localState, DocumentStack dstStack,
420 FileOperations.Callback callback) {
421
422 if (!canCopyTo(dstStack)) {
423 return false;
424 }
425
Garfield Tan3b987cc2017-04-17 10:13:43 -0700426 dropChecked(
427 clipData,
428 localState,
429 dstStack,
430 calculateOpType(clipData, dstStack.getRoot()),
431 callback);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700432 return true;
433 }
434
435 private void dropChecked(ClipData clipData, Object localState, DocumentStack dstStack,
Garfield Tan3b987cc2017-04-17 10:13:43 -0700436 @OpType int opType, FileOperations.Callback callback) {
Garfield Tanda2c0f02017-04-11 13:47:58 -0700437
438 // Recognize multi-window drag and drop based on the fact that localState is not
439 // carried between processes. It will stop working when the localsState behavior
440 // is changed. The info about window should be passed in the localState then.
441 // The localState could also be null for copying from Recents in single window
442 // mode, but Recents doesn't offer this functionality (no directories).
shawnlin9cee68f2019-01-25 11:20:18 +0800443 Metrics.logUserAction(
444 localState == null ? MetricConsts.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
445 : MetricConsts.USER_ACTION_DRAG_N_DROP);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700446
Garfield Tan3b987cc2017-04-17 10:13:43 -0700447 mClipper.copyFromClipData(dstStack, clipData, opType, callback);
Garfield Tanda2c0f02017-04-11 13:47:58 -0700448 }
449
450 @Override
451 public void dragEnded() {
452 // Multiple drag listeners might delegate drag ended event to this method, so anything
453 // in this method needs to be idempotent. Otherwise we need to designate one listener
454 // that always exists and only let it notify us when drag ended, which will further
455 // complicate code and introduce one more coupling. This is a Android framework
456 // limitation.
457
458 mView = null;
459 mInvalidDest = null;
460 mClipData = null;
461 mDestDoc = null;
462 mDestRoot = null;
Ben Linc1a32ae2017-04-19 15:19:49 -0700463 mMustBeCopied = false;
Garfield Tanda2c0f02017-04-11 13:47:58 -0700464 }
465
466 private @OpType int calculateOpType(ClipData clipData, RootInfo destRoot) {
Ben Linc1a32ae2017-04-19 15:19:49 -0700467 if (mMustBeCopied) {
468 return FileOperationService.OPERATION_COPY;
469 }
470
Garfield Tanda2c0f02017-04-11 13:47:58 -0700471 final String srcRootUri = clipData.getDescription().getExtras().getString(SRC_ROOT_KEY);
472 final String destRootUri = destRoot.getUri().toString();
473
474 assert(srcRootUri != null);
475 assert(destRootUri != null);
476
477 if (srcRootUri.equals(destRootUri)) {
478 return mIsCtrlPressed
479 ? FileOperationService.OPERATION_COPY
480 : FileOperationService.OPERATION_MOVE;
481 } else {
482 return mIsCtrlPressed
483 ? FileOperationService.OPERATION_MOVE
484 : FileOperationService.OPERATION_COPY;
485 }
486 }
487
488 private boolean canCopyTo(DocumentStack dstStack) {
489 final RootInfo root = dstStack.getRoot();
490 final DocumentInfo dst = dstStack.peek();
491 return isValidDestination(root, dst.derivedUri);
492 }
493
494 private boolean isValidDestination(RootInfo root, Uri dstUri) {
495 return root.supportsCreate() && !mInvalidDest.contains(dstUri);
496 }
497 }
498}