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