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.view.Display.DEFAULT_DISPLAY;
20 import static android.view.DragEvent.ACTION_DRAG_ENDED;
21 import static android.view.DragEvent.ACTION_DRAG_ENTERED;
22 import static android.view.DragEvent.ACTION_DRAG_EXITED;
23 import static android.view.DragEvent.ACTION_DRAG_LOCATION;
24 import static android.view.DragEvent.ACTION_DRAG_STARTED;
25 import static android.view.DragEvent.ACTION_DROP;
26 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
27 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
28 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
29 import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
30 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
31 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
32 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
33 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
34 
35 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
36 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
37 
38 import android.app.ActivityTaskManager;
39 import android.content.ClipDescription;
40 import android.content.ComponentCallbacks2;
41 import android.content.Context;
42 import android.content.res.Configuration;
43 import android.graphics.HardwareRenderer;
44 import android.graphics.PixelFormat;
45 import android.util.Slog;
46 import android.util.SparseArray;
47 import android.view.DragEvent;
48 import android.view.LayoutInflater;
49 import android.view.SurfaceControl;
50 import android.view.View;
51 import android.view.ViewGroup;
52 import android.view.WindowManager;
53 import android.widget.FrameLayout;
54 
55 import androidx.annotation.BinderThread;
56 import androidx.annotation.NonNull;
57 import androidx.annotation.VisibleForTesting;
58 
59 import com.android.internal.logging.InstanceId;
60 import com.android.internal.logging.UiEventLogger;
61 import com.android.internal.protolog.common.ProtoLog;
62 import com.android.launcher3.icons.IconProvider;
63 import com.android.wm.shell.R;
64 import com.android.wm.shell.common.DisplayController;
65 import com.android.wm.shell.common.ExternalInterfaceBinder;
66 import com.android.wm.shell.common.RemoteCallable;
67 import com.android.wm.shell.common.ShellExecutor;
68 import com.android.wm.shell.common.annotations.ExternalMainThread;
69 import com.android.wm.shell.protolog.ShellProtoLogGroup;
70 import com.android.wm.shell.splitscreen.SplitScreenController;
71 import com.android.wm.shell.sysui.ShellCommandHandler;
72 import com.android.wm.shell.sysui.ShellController;
73 import com.android.wm.shell.sysui.ShellInit;
74 
75 import java.io.PrintWriter;
76 import java.util.ArrayList;
77 
78 /**
79  * Handles the global drag and drop handling for the Shell.
80  */
81 public class DragAndDropController implements RemoteCallable<DragAndDropController>,
82         DisplayController.OnDisplaysChangedListener,
83         View.OnDragListener, ComponentCallbacks2 {
84 
85     private static final String TAG = DragAndDropController.class.getSimpleName();
86 
87     private final Context mContext;
88     private final ShellController mShellController;
89     private final ShellCommandHandler mShellCommandHandler;
90     private final DisplayController mDisplayController;
91     private final DragAndDropEventLogger mLogger;
92     private final IconProvider mIconProvider;
93     private SplitScreenController mSplitScreen;
94     private ShellExecutor mMainExecutor;
95     private ArrayList<DragAndDropListener> mListeners = new ArrayList<>();
96 
97     private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>();
98 
99     /**
100      * Listener called during drag events, currently just onDragStarted.
101      */
102     public interface DragAndDropListener {
103         /** Called when a drag has started. */
onDragStarted()104         void onDragStarted();
105     }
106 
107     /**
108      * Creates {@link DragAndDropController}. Returns {@code null} if the feature is disabled.
109      */
create(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor)110     public static DragAndDropController create(Context context,
111             ShellInit shellInit,
112             ShellController shellController,
113             ShellCommandHandler shellCommandHandler,
114             DisplayController displayController,
115             UiEventLogger uiEventLogger,
116             IconProvider iconProvider,
117             ShellExecutor mainExecutor) {
118         if (!context.getResources().getBoolean(R.bool.config_enableShellDragDrop)) {
119             return null;
120         }
121         return new DragAndDropController(context, shellInit, shellController, shellCommandHandler,
122                 displayController, uiEventLogger, iconProvider, mainExecutor);
123     }
124 
DragAndDropController(Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor)125     DragAndDropController(Context context,
126             ShellInit shellInit,
127             ShellController shellController,
128             ShellCommandHandler shellCommandHandler,
129             DisplayController displayController,
130             UiEventLogger uiEventLogger,
131             IconProvider iconProvider,
132             ShellExecutor mainExecutor) {
133         mContext = context;
134         mShellController = shellController;
135         mShellCommandHandler = shellCommandHandler;
136         mDisplayController = displayController;
137         mLogger = new DragAndDropEventLogger(uiEventLogger);
138         mIconProvider = iconProvider;
139         mMainExecutor = mainExecutor;
140         shellInit.addInitCallback(this::onInit, this);
141     }
142 
143     /**
144      * Called when the controller is initialized.
145      */
onInit()146     public void onInit() {
147         // TODO(b/238217847): The dependency from SplitscreenController on DragAndDropController is
148         // inverted, which leads to SplitscreenController not setting its instance until after
149         // onDisplayAdded.  We can remove this post once we fix that dependency.
150         mMainExecutor.executeDelayed(() -> {
151             mDisplayController.addDisplayWindowListener(this);
152         }, 0);
153         mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP,
154                 this::createExternalInterface, this);
155         mShellCommandHandler.addDumpCallback(this::dump, this);
156     }
157 
createExternalInterface()158     private ExternalInterfaceBinder createExternalInterface() {
159         return new IDragAndDropImpl(this);
160     }
161 
162     @Override
getContext()163     public Context getContext() {
164         return mContext;
165     }
166 
167     @Override
getRemoteCallExecutor()168     public ShellExecutor getRemoteCallExecutor() {
169         return mMainExecutor;
170     }
171 
172     /**
173      * Sets the splitscreen controller to use if the feature is available.
174      */
setSplitScreenController(SplitScreenController splitscreen)175     public void setSplitScreenController(SplitScreenController splitscreen) {
176         mSplitScreen = splitscreen;
177     }
178 
179     /** Adds a listener to be notified of drag and drop events. */
addListener(DragAndDropListener listener)180     public void addListener(DragAndDropListener listener) {
181         mListeners.add(listener);
182     }
183 
184     /** Removes a drag and drop listener. */
removeListener(DragAndDropListener listener)185     public void removeListener(DragAndDropListener listener) {
186         mListeners.remove(listener);
187     }
188 
notifyDragStarted()189     private void notifyDragStarted() {
190         for (int i = 0; i < mListeners.size(); i++) {
191             mListeners.get(i).onDragStarted();
192         }
193     }
194 
195     @Override
onDisplayAdded(int displayId)196     public void onDisplayAdded(int displayId) {
197         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId);
198         if (displayId != DEFAULT_DISPLAY) {
199             // Ignore non-default displays for now
200             return;
201         }
202 
203         final Context context = mDisplayController.getDisplayContext(displayId)
204                 .createWindowContext(TYPE_APPLICATION_OVERLAY, null);
205         final WindowManager wm = context.getSystemService(WindowManager.class);
206         final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
207                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
208                 TYPE_APPLICATION_OVERLAY,
209                 FLAG_NOT_FOCUSABLE | FLAG_HARDWARE_ACCELERATED,
210                 PixelFormat.TRANSLUCENT);
211         layoutParams.privateFlags |= SYSTEM_FLAG_SHOW_FOR_ALL_USERS
212                 | PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP
213                 | PRIVATE_FLAG_NO_MOVE_ANIMATION;
214         layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
215         layoutParams.setFitInsetsTypes(0);
216         layoutParams.setTitle("ShellDropTarget");
217 
218         FrameLayout rootView = (FrameLayout) LayoutInflater.from(context).inflate(
219                 R.layout.global_drop_target, null);
220         rootView.setOnDragListener(this);
221         rootView.setVisibility(View.INVISIBLE);
222         DragLayout dragLayout = new DragLayout(context, mSplitScreen, mIconProvider);
223         rootView.addView(dragLayout,
224                 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
225         try {
226             wm.addView(rootView, layoutParams);
227             addDisplayDropTarget(displayId, context, wm, rootView, dragLayout);
228             context.registerComponentCallbacks(this);
229         } catch (WindowManager.InvalidDisplayException e) {
230             Slog.w(TAG, "Unable to add view for display id: " + displayId);
231         }
232     }
233 
234     @VisibleForTesting
addDisplayDropTarget(int displayId, Context context, WindowManager wm, FrameLayout rootView, DragLayout dragLayout)235     void addDisplayDropTarget(int displayId, Context context, WindowManager wm,
236             FrameLayout rootView, DragLayout dragLayout) {
237         mDisplayDropTargets.put(displayId,
238                 new PerDisplay(displayId, context, wm, rootView, dragLayout));
239     }
240 
241     @Override
onDisplayConfigurationChanged(int displayId, Configuration newConfig)242     public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
243         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display changed: %d", displayId);
244         final PerDisplay pd = mDisplayDropTargets.get(displayId);
245         if (pd == null) {
246             return;
247         }
248         pd.rootView.requestApplyInsets();
249     }
250 
251     @Override
onDisplayRemoved(int displayId)252     public void onDisplayRemoved(int displayId) {
253         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display removed: %d", displayId);
254         final PerDisplay pd = mDisplayDropTargets.get(displayId);
255         if (pd == null) {
256             return;
257         }
258         pd.context.unregisterComponentCallbacks(this);
259         pd.wm.removeViewImmediate(pd.rootView);
260         mDisplayDropTargets.remove(displayId);
261     }
262 
263     @Override
onDrag(View target, DragEvent event)264     public boolean onDrag(View target, DragEvent event) {
265         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
266                 "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f",
267                 DragEvent.actionToString(event.getAction()), event.getX(), event.getY(),
268                 event.getOffsetX(), event.getOffsetY());
269         final int displayId = target.getDisplay().getDisplayId();
270         final PerDisplay pd = mDisplayDropTargets.get(displayId);
271         final ClipDescription description = event.getClipDescription();
272 
273         if (pd == null) {
274             return false;
275         }
276 
277         if (event.getAction() == ACTION_DRAG_STARTED) {
278             pd.isHandlingDrag = DragUtils.canHandleDrag(event);
279             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
280                     "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s",
281                     pd.isHandlingDrag, event.getClipData().getItemCount(),
282                     DragUtils.getMimeTypesConcatenated(description));
283         }
284 
285         if (!pd.isHandlingDrag) {
286             return false;
287         }
288 
289         switch (event.getAction()) {
290             case ACTION_DRAG_STARTED:
291                 if (pd.activeDragCount != 0) {
292                     Slog.w(TAG, "Unexpected drag start during an active drag");
293                     return false;
294                 }
295                 // TODO(b/290391688): Also update the session data with task stack changes
296                 InstanceId loggerSessionId = mLogger.logStart(event);
297                 pd.activeDragCount++;
298                 pd.dragSession = new DragSession(mContext, ActivityTaskManager.getInstance(),
299                         mDisplayController.getDisplayLayout(displayId), event.getClipData());
300                 pd.dragSession.update();
301                 pd.dragLayout.prepare(pd.dragSession, loggerSessionId);
302                 setDropTargetWindowVisibility(pd, View.VISIBLE);
303                 notifyDragStarted();
304                 break;
305             case ACTION_DRAG_ENTERED:
306                 pd.dragLayout.show();
307                 break;
308             case ACTION_DRAG_LOCATION:
309                 pd.dragLayout.update(event);
310                 break;
311             case ACTION_DROP: {
312                 pd.dragLayout.update(event);
313                 return handleDrop(event, pd);
314             }
315             case ACTION_DRAG_EXITED: {
316                 // Either one of DROP or EXITED will happen, and when EXITED we won't consume
317                 // the drag surface
318                 pd.dragLayout.hide(event, null);
319                 break;
320             }
321             case ACTION_DRAG_ENDED:
322                 // TODO(b/290391688): Ensure sure it's not possible to get ENDED without DROP
323                 // or EXITED
324                 if (pd.dragLayout.hasDropped()) {
325                     mLogger.logDrop();
326                 } else {
327                     pd.activeDragCount--;
328                     pd.dragLayout.hide(event, () -> {
329                         if (pd.activeDragCount == 0) {
330                             // Hide the window if another drag hasn't been started while animating
331                             // the drag-end
332                             setDropTargetWindowVisibility(pd, View.INVISIBLE);
333                         }
334                     });
335                 }
336                 mLogger.logEnd();
337                 break;
338         }
339         return true;
340     }
341 
342     /**
343      * Handles dropping on the drop target.
344      */
handleDrop(DragEvent event, PerDisplay pd)345     private boolean handleDrop(DragEvent event, PerDisplay pd) {
346         final SurfaceControl dragSurface = event.getDragSurface();
347         pd.activeDragCount--;
348         return pd.dragLayout.drop(event, dragSurface, () -> {
349             if (pd.activeDragCount == 0) {
350                 // Hide the window if another drag hasn't been started while animating the drop
351                 setDropTargetWindowVisibility(pd, View.INVISIBLE);
352             }
353         });
354     }
355 
356     private void setDropTargetWindowVisibility(PerDisplay pd, int visibility) {
357         pd.setWindowVisibility(visibility);
358     }
359 
360     /**
361      * Returns if any displays are currently ready to handle a drag/drop.
362      */
363     private boolean isReadyToHandleDrag() {
364         for (int i = 0; i < mDisplayDropTargets.size(); i++) {
365             if (mDisplayDropTargets.valueAt(i).mHasDrawn) {
366                 return true;
367             }
368         }
369         return false;
370     }
371 
372     // Note: Component callbacks are always called on the main thread of the process
373     @ExternalMainThread
374     @Override
375     public void onConfigurationChanged(Configuration newConfig) {
376         mMainExecutor.execute(() -> {
377             for (int i = 0; i < mDisplayDropTargets.size(); i++) {
378                 mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig);
379             }
380         });
381     }
382 
383     // Note: Component callbacks are always called on the main thread of the process
384     @ExternalMainThread
385     @Override
386     public void onTrimMemory(int level) {
387         // Do nothing
388     }
389 
390     // Note: Component callbacks are always called on the main thread of the process
391     @ExternalMainThread
392     @Override
393     public void onLowMemory() {
394         // Do nothing
395     }
396 
397     /**
398      * Dumps information about this controller.
399      */
400     public void dump(@NonNull PrintWriter pw, String prefix) {
401         pw.println(prefix + TAG);
402         pw.println(prefix + " listeners=" + mListeners.size());
403     }
404 
405     /**
406      * The interface for calls from outside the host process.
407      */
408     @BinderThread
409     private static class IDragAndDropImpl extends IDragAndDrop.Stub
410             implements ExternalInterfaceBinder {
411         private DragAndDropController mController;
412 
413         public IDragAndDropImpl(DragAndDropController controller) {
414             mController = controller;
415         }
416 
417         /**
418          * Invalidates this instance, preventing future calls from updating the controller.
419          */
420         @Override
421         public void invalidate() {
422             mController = null;
423         }
424 
425         @Override
426         public boolean isReadyToHandleDrag() {
427             boolean[] result = new boolean[1];
428             executeRemoteCallWithTaskPermission(mController, "isReadyToHandleDrag",
429                     controller -> result[0] = controller.isReadyToHandleDrag(),
430                     true /* blocking */
431             );
432             return result[0];
433         }
434     }
435 
436     private static class PerDisplay implements HardwareRenderer.FrameDrawingCallback {
437         final int displayId;
438         final Context context;
439         final WindowManager wm;
440         final FrameLayout rootView;
441         final DragLayout dragLayout;
442         // Tracks whether the window has fully drawn since it was last made visible
443         boolean mHasDrawn;
444 
445         boolean isHandlingDrag;
446         // A count of the number of active drags in progress to ensure that we only hide the window
447         // when all the drag animations have completed
448         int activeDragCount;
449         // The active drag session
450         DragSession dragSession;
451 
452         PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayout dl) {
453             displayId = dispId;
454             context = c;
455             wm = w;
456             rootView = rv;
457             dragLayout = dl;
458         }
459 
460         private void setWindowVisibility(int visibility) {
461             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
462                     "Set drop target window visibility: displayId=%d visibility=%d",
463                     displayId, visibility);
464             rootView.setVisibility(visibility);
465             if (visibility == View.VISIBLE) {
466                 rootView.requestApplyInsets();
467                 if (!mHasDrawn && rootView.getViewRootImpl() != null) {
468                     rootView.getViewRootImpl().registerRtFrameCallback(this);
469                 }
470             } else {
471                 mHasDrawn = false;
472             }
473         }
474 
475         @Override
476         public void onFrameDraw(long frame) {
477             mHasDrawn = true;
478         }
479     }
480 }
481