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