• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.wm.shell.draganddrop;
18 
19 import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
20 import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
21 import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
22 import static android.view.Display.DEFAULT_DISPLAY;
23 import static android.view.DragEvent.ACTION_DRAG_ENDED;
24 import static android.view.DragEvent.ACTION_DRAG_ENTERED;
25 import static android.view.DragEvent.ACTION_DRAG_EXITED;
26 import static android.view.DragEvent.ACTION_DRAG_LOCATION;
27 import static android.view.DragEvent.ACTION_DRAG_STARTED;
28 import static android.view.DragEvent.ACTION_DROP;
29 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
30 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
31 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
32 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
33 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
34 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
35 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
36 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
37 
38 import android.content.ClipDescription;
39 import android.content.ComponentCallbacks2;
40 import android.content.Context;
41 import android.content.res.Configuration;
42 import android.graphics.PixelFormat;
43 import android.util.Slog;
44 import android.util.SparseArray;
45 import android.view.DragEvent;
46 import android.view.LayoutInflater;
47 import android.view.SurfaceControl;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.WindowManager;
51 import android.widget.FrameLayout;
52 
53 import androidx.annotation.VisibleForTesting;
54 
55 import com.android.internal.logging.InstanceId;
56 import com.android.internal.logging.UiEventLogger;
57 import com.android.internal.protolog.common.ProtoLog;
58 import com.android.launcher3.icons.IconProvider;
59 import com.android.wm.shell.R;
60 import com.android.wm.shell.common.DisplayController;
61 import com.android.wm.shell.common.ShellExecutor;
62 import com.android.wm.shell.common.annotations.ExternalMainThread;
63 import com.android.wm.shell.protolog.ShellProtoLogGroup;
64 import com.android.wm.shell.splitscreen.SplitScreenController;
65 import com.android.wm.shell.sysui.ShellController;
66 import com.android.wm.shell.sysui.ShellInit;
67 
68 import java.util.ArrayList;
69 
70 /**
71  * Handles the global drag and drop handling for the Shell.
72  */
73 public class DragAndDropController implements DisplayController.OnDisplaysChangedListener,
74         View.OnDragListener, ComponentCallbacks2 {
75 
76     private static final String TAG = DragAndDropController.class.getSimpleName();
77 
78     private final Context mContext;
79     private final ShellController mShellController;
80     private final DisplayController mDisplayController;
81     private final DragAndDropEventLogger mLogger;
82     private final IconProvider mIconProvider;
83     private SplitScreenController mSplitScreen;
84     private ShellExecutor mMainExecutor;
85     private ArrayList<DragAndDropListener> mListeners = new ArrayList<>();
86 
87     private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
88 
89     /**
90      * Listener called during drag events, currently just onDragStarted.
91      */
92     public interface DragAndDropListener {
93         /** Called when a drag has started. */
onDragStarted()94         void onDragStarted();
95     }
96 
DragAndDropController(Context context, ShellInit shellInit, ShellController shellController, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor)97     public DragAndDropController(Context context,
98             ShellInit shellInit,
99             ShellController shellController,
100             DisplayController displayController,
101             UiEventLogger uiEventLogger,
102             IconProvider iconProvider,
103             ShellExecutor mainExecutor) {
104         mContext = context;
105         mShellController = shellController;
106         mDisplayController = displayController;
107         mLogger = new DragAndDropEventLogger(uiEventLogger);
108         mIconProvider = iconProvider;
109         mMainExecutor = mainExecutor;
110         shellInit.addInitCallback(this::onInit, this);
111     }
112 
113     /**
114      * Called when the controller is initialized.
115      */
onInit()116     public void onInit() {
117         // TODO(b/238217847): The dependency from SplitscreenController on DragAndDropController is
118         // inverted, which leads to SplitscreenController not setting its instance until after
119         // onDisplayAdded.  We can remove this post once we fix that dependency.
120         mMainExecutor.executeDelayed(() -> {
121             mDisplayController.addDisplayWindowListener(this);
122         }, 0);
123     }
124 
125     /**
126      * Sets the splitscreen controller to use if the feature is available.
127      */
setSplitScreenController(SplitScreenController splitscreen)128     public void setSplitScreenController(SplitScreenController splitscreen) {
129         mSplitScreen = splitscreen;
130     }
131 
132     /** Adds a listener to be notified of drag and drop events. */
addListener(DragAndDropListener listener)133     public void addListener(DragAndDropListener listener) {
134         mListeners.add(listener);
135     }
136 
137     /** Removes a drag and drop listener. */
removeListener(DragAndDropListener listener)138     public void removeListener(DragAndDropListener listener) {
139         mListeners.remove(listener);
140     }
141 
notifyListeners()142     private void notifyListeners() {
143         for (int i = 0; i < mListeners.size(); i++) {
144             mListeners.get(i).onDragStarted();
145         }
146     }
147 
148     @Override
onDisplayAdded(int displayId)149     public void onDisplayAdded(int displayId) {
150         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId);
151         if (displayId != DEFAULT_DISPLAY) {
152             // Ignore non-default displays for now
153             return;
154         }
155 
156         final Context context = mDisplayController.getDisplayContext(displayId)
157                 .createWindowContext(TYPE_APPLICATION_OVERLAY, null);
158         final WindowManager wm = context.getSystemService(WindowManager.class);
159 
160         // TODO(b/169894807): Figure out the right layer for this, needs to be below the task bar
161         final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
162                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
163                 TYPE_APPLICATION_OVERLAY,
164                 FLAG_NOT_FOCUSABLE | FLAG_HARDWARE_ACCELERATED,
165                 PixelFormat.TRANSLUCENT);
166         layoutParams.privateFlags |= SYSTEM_FLAG_SHOW_FOR_ALL_USERS
167                 | PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP
168                 | PRIVATE_FLAG_NO_MOVE_ANIMATION;
169         layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
170         layoutParams.setFitInsetsTypes(0);
171         layoutParams.setTitle("ShellDropTarget");
172 
173         FrameLayout rootView = (FrameLayout) LayoutInflater.from(context).inflate(
174                 R.layout.global_drop_target, null);
175         rootView.setOnDragListener(this);
176         rootView.setVisibility(View.INVISIBLE);
177         DragLayout dragLayout = new DragLayout(context, mSplitScreen, mIconProvider);
178         rootView.addView(dragLayout,
179                 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
180         try {
181             wm.addView(rootView, layoutParams);
182             addDisplayDropTarget(displayId, context, wm, rootView, dragLayout);
183             context.registerComponentCallbacks(this);
184         } catch (WindowManager.InvalidDisplayException e) {
185             Slog.w(TAG, "Unable to add view for display id: " + displayId);
186         }
187     }
188 
189     @VisibleForTesting
addDisplayDropTarget(int displayId, Context context, WindowManager wm, FrameLayout rootView, DragLayout dragLayout)190     void addDisplayDropTarget(int displayId, Context context, WindowManager wm,
191             FrameLayout rootView, DragLayout dragLayout) {
192         mDisplayDropTargets.put(displayId,
193                 new PerDisplay(displayId, context, wm, rootView, dragLayout));
194     }
195 
196     @Override
onDisplayConfigurationChanged(int displayId, Configuration newConfig)197     public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
198         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId);
199         final PerDisplay pd = mDisplayDropTargets.get(displayId);
200         if (pd == null) {
201             return;
202         }
203         pd.rootView.requestApplyInsets();
204     }
205 
206     @Override
onDisplayRemoved(int displayId)207     public void onDisplayRemoved(int displayId) {
208         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display removed: %d", displayId);
209         final PerDisplay pd = mDisplayDropTargets.get(displayId);
210         if (pd == null) {
211             return;
212         }
213         pd.context.unregisterComponentCallbacks(this);
214         pd.wm.removeViewImmediate(pd.rootView);
215         mDisplayDropTargets.remove(displayId);
216     }
217 
218     @Override
onDrag(View target, DragEvent event)219     public boolean onDrag(View target, DragEvent event) {
220         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
221                 "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f",
222                 DragEvent.actionToString(event.getAction()), event.getX(), event.getY(),
223                 event.getOffsetX(), event.getOffsetY());
224         final int displayId = target.getDisplay().getDisplayId();
225         final PerDisplay pd = mDisplayDropTargets.get(displayId);
226         final ClipDescription description = event.getClipDescription();
227 
228         if (pd == null) {
229             return false;
230         }
231 
232         if (event.getAction() == ACTION_DRAG_STARTED) {
233             final boolean hasValidClipData = event.getClipData().getItemCount() > 0
234                     && (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY)
235                             || description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT)
236                             || description.hasMimeType(MIMETYPE_APPLICATION_TASK));
237             pd.isHandlingDrag = hasValidClipData;
238             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
239                     "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s",
240                     pd.isHandlingDrag, event.getClipData().getItemCount(),
241                     getMimeTypes(description));
242         }
243 
244         if (!pd.isHandlingDrag) {
245             return false;
246         }
247 
248         switch (event.getAction()) {
249             case ACTION_DRAG_STARTED:
250                 if (pd.activeDragCount != 0) {
251                     Slog.w(TAG, "Unexpected drag start during an active drag");
252                     return false;
253                 }
254                 InstanceId loggerSessionId = mLogger.logStart(event);
255                 pd.activeDragCount++;
256                 pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId),
257                         event.getClipData(), loggerSessionId);
258                 setDropTargetWindowVisibility(pd, View.VISIBLE);
259                 notifyListeners();
260                 break;
261             case ACTION_DRAG_ENTERED:
262                 pd.dragLayout.show();
263                 break;
264             case ACTION_DRAG_LOCATION:
265                 pd.dragLayout.update(event);
266                 break;
267             case ACTION_DROP: {
268                 pd.dragLayout.update(event);
269                 return handleDrop(event, pd);
270             }
271             case ACTION_DRAG_EXITED: {
272                 // Either one of DROP or EXITED will happen, and when EXITED we won't consume
273                 // the drag surface
274                 pd.dragLayout.hide(event, null);
275                 break;
276             }
277             case ACTION_DRAG_ENDED:
278                 // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP
279                 // or EXITED
280                 if (pd.dragLayout.hasDropped()) {
281                     mLogger.logDrop();
282                 } else {
283                     pd.activeDragCount--;
284                     pd.dragLayout.hide(event, () -> {
285                         if (pd.activeDragCount == 0) {
286                             // Hide the window if another drag hasn't been started while animating
287                             // the drag-end
288                             setDropTargetWindowVisibility(pd, View.INVISIBLE);
289                         }
290                     });
291                 }
292                 mLogger.logEnd();
293                 break;
294         }
295         return true;
296     }
297 
298     /**
299      * Handles dropping on the drop target.
300      */
handleDrop(DragEvent event, PerDisplay pd)301     private boolean handleDrop(DragEvent event, PerDisplay pd) {
302         final SurfaceControl dragSurface = event.getDragSurface();
303         pd.activeDragCount--;
304         return pd.dragLayout.drop(event, dragSurface, () -> {
305             if (pd.activeDragCount == 0) {
306                 // Hide the window if another drag hasn't been started while animating the drop
307                 setDropTargetWindowVisibility(pd, View.INVISIBLE);
308             }
309         });
310     }
311 
312     private void setDropTargetWindowVisibility(PerDisplay pd, int visibility) {
313         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
314                 "Set drop target window visibility: displayId=%d visibility=%d",
315                 pd.displayId, visibility);
316         pd.rootView.setVisibility(visibility);
317         if (visibility == View.VISIBLE) {
318             pd.rootView.requestApplyInsets();
319         }
320     }
321 
322     private String getMimeTypes(ClipDescription description) {
323         String mimeTypes = "";
324         for (int i = 0; i < description.getMimeTypeCount(); i++) {
325             if (i > 0) {
326                 mimeTypes += ", ";
327             }
328             mimeTypes += description.getMimeType(i);
329         }
330         return mimeTypes;
331     }
332 
333     // Note: Component callbacks are always called on the main thread of the process
334     @ExternalMainThread
335     @Override
336     public void onConfigurationChanged(Configuration newConfig) {
337         mMainExecutor.execute(() -> {
338             for (int i = 0; i < mDisplayDropTargets.size(); i++) {
339                 mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
340             }
341         });
342     }
343 
344     // Note: Component callbacks are always called on the main thread of the process
345     @ExternalMainThread
346     @Override
347     public void onTrimMemory(int level) {
348         // Do nothing
349     }
350 
351     // Note: Component callbacks are always called on the main thread of the process
352     @ExternalMainThread
353     @Override
354     public void onLowMemory() {
355         // Do nothing
356     }
357 
358     private static class PerDisplay {
359         final int displayId;
360         final Context context;
361         final WindowManager wm;
362         final FrameLayout rootView;
363         final DragLayout dragLayout;
364 
365         boolean isHandlingDrag;
366         // A count of the number of active drags in progress to ensure that we only hide the window
367         // when all the drag animations have completed
368         int activeDragCount;
369 
370         PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayout dl) {
371             displayId = dispId;
372             context = c;
373             wm = w;
374             rootView = rv;
375             dragLayout = dl;
376         }
377     }
378 }
379