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