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