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.app.StatusBarManager.DISABLE_NONE; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 21 import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; 22 import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; 23 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 24 25 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 26 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 27 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; 28 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; 29 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; 30 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; 31 32 import android.animation.Animator; 33 import android.animation.AnimatorListenerAdapter; 34 import android.animation.ValueAnimator; 35 import android.annotation.SuppressLint; 36 import android.app.ActivityManager; 37 import android.app.StatusBarManager; 38 import android.content.Context; 39 import android.content.res.Configuration; 40 import android.graphics.Color; 41 import android.graphics.Insets; 42 import android.graphics.Rect; 43 import android.graphics.drawable.Drawable; 44 import android.view.DragEvent; 45 import android.view.SurfaceControl; 46 import android.view.WindowInsets; 47 import android.view.WindowInsets.Type; 48 import android.widget.LinearLayout; 49 50 import com.android.internal.logging.InstanceId; 51 import com.android.internal.protolog.common.ProtoLog; 52 import com.android.launcher3.icons.IconProvider; 53 import com.android.wm.shell.R; 54 import com.android.wm.shell.animation.Interpolators; 55 import com.android.wm.shell.protolog.ShellProtoLogGroup; 56 import com.android.wm.shell.splitscreen.SplitScreenController; 57 58 import java.util.ArrayList; 59 60 /** 61 * Coordinates the visible drop targets for the current drag within a single display. 62 */ 63 public class DragLayout extends LinearLayout { 64 65 // While dragging the status bar is hidden. 66 private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS 67 | StatusBarManager.DISABLE_NOTIFICATION_ALERTS 68 | StatusBarManager.DISABLE_CLOCK 69 | StatusBarManager.DISABLE_SYSTEM_INFO; 70 71 private final DragAndDropPolicy mPolicy; 72 private final SplitScreenController mSplitScreenController; 73 private final IconProvider mIconProvider; 74 private final StatusBarManager mStatusBarManager; 75 private final Configuration mLastConfiguration = new Configuration(); 76 77 private DragAndDropPolicy.Target mCurrentTarget = null; 78 private DropZoneView mDropZoneView1; 79 private DropZoneView mDropZoneView2; 80 81 private int mDisplayMargin; 82 private int mDividerSize; 83 private Insets mInsets = Insets.NONE; 84 85 private boolean mIsShowing; 86 private boolean mHasDropped; 87 private DragSession mSession; 88 89 @SuppressLint("WrongConstant") DragLayout(Context context, SplitScreenController splitScreenController, IconProvider iconProvider)90 public DragLayout(Context context, SplitScreenController splitScreenController, 91 IconProvider iconProvider) { 92 super(context); 93 mSplitScreenController = splitScreenController; 94 mIconProvider = iconProvider; 95 mPolicy = new DragAndDropPolicy(context, splitScreenController); 96 mStatusBarManager = context.getSystemService(StatusBarManager.class); 97 mLastConfiguration.setTo(context.getResources().getConfiguration()); 98 99 mDisplayMargin = context.getResources().getDimensionPixelSize( 100 R.dimen.drop_layout_display_margin); 101 mDividerSize = context.getResources().getDimensionPixelSize( 102 R.dimen.split_divider_bar_width); 103 104 // Always use LTR because we assume dropZoneView1 is on the left and 2 is on the right when 105 // showing the highlight. 106 setLayoutDirection(LAYOUT_DIRECTION_LTR); 107 mDropZoneView1 = new DropZoneView(context); 108 mDropZoneView2 = new DropZoneView(context); 109 addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT, 110 MATCH_PARENT)); 111 addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT, 112 MATCH_PARENT)); 113 ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1; 114 ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1; 115 int orientation = getResources().getConfiguration().orientation; 116 setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE 117 ? LinearLayout.HORIZONTAL 118 : LinearLayout.VERTICAL); 119 updateContainerMargins(getResources().getConfiguration().orientation); 120 } 121 122 @Override onApplyWindowInsets(WindowInsets insets)123 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 124 mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout()); 125 recomputeDropTargets(); 126 127 final int orientation = getResources().getConfiguration().orientation; 128 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 129 mDropZoneView1.setBottomInset(mInsets.bottom); 130 mDropZoneView2.setBottomInset(mInsets.bottom); 131 } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 132 mDropZoneView1.setBottomInset(0); 133 mDropZoneView2.setBottomInset(mInsets.bottom); 134 } 135 return super.onApplyWindowInsets(insets); 136 } 137 onConfigChanged(Configuration newConfig)138 public void onConfigChanged(Configuration newConfig) { 139 if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE 140 && getOrientation() != HORIZONTAL) { 141 setOrientation(LinearLayout.HORIZONTAL); 142 updateContainerMargins(newConfig.orientation); 143 } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT 144 && getOrientation() != VERTICAL) { 145 setOrientation(LinearLayout.VERTICAL); 146 updateContainerMargins(newConfig.orientation); 147 } 148 149 final int diff = newConfig.diff(mLastConfiguration); 150 final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0 151 || (diff & CONFIG_UI_MODE) != 0; 152 if (themeChanged) { 153 mDropZoneView1.onThemeChange(); 154 mDropZoneView2.onThemeChange(); 155 } 156 mLastConfiguration.setTo(newConfig); 157 } 158 updateContainerMarginsForSingleTask()159 private void updateContainerMarginsForSingleTask() { 160 mDropZoneView1.setContainerMargin( 161 mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); 162 mDropZoneView2.setContainerMargin(0, 0, 0, 0); 163 } 164 updateContainerMargins(int orientation)165 private void updateContainerMargins(int orientation) { 166 final float halfMargin = mDisplayMargin / 2f; 167 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 168 mDropZoneView1.setContainerMargin( 169 mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin); 170 mDropZoneView2.setContainerMargin( 171 halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); 172 } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { 173 mDropZoneView1.setContainerMargin( 174 mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin); 175 mDropZoneView2.setContainerMargin( 176 mDisplayMargin, halfMargin, mDisplayMargin, mDisplayMargin); 177 } 178 } 179 hasDropped()180 public boolean hasDropped() { 181 return mHasDropped; 182 } 183 184 /** 185 * Called when a new drag is started. 186 */ prepare(DragSession session, InstanceId loggerSessionId)187 public void prepare(DragSession session, InstanceId loggerSessionId) { 188 mPolicy.start(session, loggerSessionId); 189 mSession = session; 190 mHasDropped = false; 191 mCurrentTarget = null; 192 193 boolean alreadyInSplit = mSplitScreenController != null 194 && mSplitScreenController.isSplitScreenVisible(); 195 if (!alreadyInSplit) { 196 ActivityManager.RunningTaskInfo taskInfo1 = mSession.runningTaskInfo; 197 if (taskInfo1 != null) { 198 final int activityType = taskInfo1.getActivityType(); 199 if (activityType == ACTIVITY_TYPE_STANDARD) { 200 Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); 201 int bgColor1 = getResizingBackgroundColor(taskInfo1); 202 mDropZoneView1.setAppInfo(bgColor1, icon1); 203 mDropZoneView2.setAppInfo(bgColor1, icon1); 204 updateDropZoneSizes(null, null); // passing null splits the views evenly 205 } else { 206 // We use the first drop zone to show the fullscreen highlight, and don't need 207 // to set additional info 208 mDropZoneView1.setForceIgnoreBottomMargin(true); 209 updateDropZoneSizesForSingleTask(); 210 updateContainerMarginsForSingleTask(); 211 } 212 } 213 } else { 214 // We're already in split so get taskInfo from the controller to populate icon / color. 215 ActivityManager.RunningTaskInfo topOrLeftTask = 216 mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); 217 ActivityManager.RunningTaskInfo bottomOrRightTask = 218 mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); 219 if (topOrLeftTask != null && bottomOrRightTask != null) { 220 Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo); 221 int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask); 222 Drawable bottomOrRightIcon = mIconProvider.getIcon( 223 bottomOrRightTask.topActivityInfo); 224 int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask); 225 mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon); 226 mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon); 227 } 228 229 // Update the dropzones to match existing split sizes 230 Rect topOrLeftBounds = new Rect(); 231 Rect bottomOrRightBounds = new Rect(); 232 mSplitScreenController.getStageBounds(topOrLeftBounds, bottomOrRightBounds); 233 updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds); 234 } 235 } 236 updateDropZoneSizesForSingleTask()237 private void updateDropZoneSizesForSingleTask() { 238 final LinearLayout.LayoutParams dropZoneView1 = 239 (LayoutParams) mDropZoneView1.getLayoutParams(); 240 final LinearLayout.LayoutParams dropZoneView2 = 241 (LayoutParams) mDropZoneView2.getLayoutParams(); 242 dropZoneView1.width = MATCH_PARENT; 243 dropZoneView1.height = MATCH_PARENT; 244 dropZoneView2.width = 0; 245 dropZoneView2.height = 0; 246 dropZoneView1.weight = 1; 247 dropZoneView2.weight = 0; 248 mDropZoneView1.setLayoutParams(dropZoneView1); 249 mDropZoneView2.setLayoutParams(dropZoneView2); 250 } 251 252 /** 253 * Sets the size of the two drop zones based on the provided bounds. The divider sits between 254 * the views and its size is included in the calculations. 255 * 256 * @param bounds1 bounds to apply to the first dropzone view, null if split in half. 257 * @param bounds2 bounds to apply to the second dropzone view, null if split in half. 258 */ updateDropZoneSizes(Rect bounds1, Rect bounds2)259 private void updateDropZoneSizes(Rect bounds1, Rect bounds2) { 260 final int orientation = getResources().getConfiguration().orientation; 261 final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT; 262 final int halfDivider = mDividerSize / 2; 263 final LinearLayout.LayoutParams dropZoneView1 = 264 (LayoutParams) mDropZoneView1.getLayoutParams(); 265 final LinearLayout.LayoutParams dropZoneView2 = 266 (LayoutParams) mDropZoneView2.getLayoutParams(); 267 if (isPortrait) { 268 dropZoneView1.width = MATCH_PARENT; 269 dropZoneView2.width = MATCH_PARENT; 270 dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT; 271 dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT; 272 } else { 273 dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT; 274 dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT; 275 dropZoneView1.height = MATCH_PARENT; 276 dropZoneView2.height = MATCH_PARENT; 277 } 278 dropZoneView1.weight = bounds1 != null ? 0 : 1; 279 dropZoneView2.weight = bounds2 != null ? 0 : 1; 280 mDropZoneView1.setLayoutParams(dropZoneView1); 281 mDropZoneView2.setLayoutParams(dropZoneView2); 282 } 283 show()284 public void show() { 285 mIsShowing = true; 286 recomputeDropTargets(); 287 } 288 289 /** 290 * Recalculates the drop targets based on the current policy. 291 */ recomputeDropTargets()292 private void recomputeDropTargets() { 293 if (!mIsShowing) { 294 return; 295 } 296 final ArrayList<DragAndDropPolicy.Target> targets = mPolicy.getTargets(mInsets); 297 for (int i = 0; i < targets.size(); i++) { 298 final DragAndDropPolicy.Target target = targets.get(i); 299 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", target); 300 // Inset the draw region by a little bit 301 target.drawRegion.inset(mDisplayMargin, mDisplayMargin); 302 } 303 } 304 305 /** 306 * Updates the visible drop target as the user drags. 307 */ update(DragEvent event)308 public void update(DragEvent event) { 309 if (mHasDropped) { 310 return; 311 } 312 // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the 313 // visibility of the current region 314 DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( 315 (int) event.getX(), (int) event.getY()); 316 if (mCurrentTarget != target) { 317 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); 318 if (target == null) { 319 // Animating to no target 320 animateSplitContainers(false, null /* animCompleteCallback */); 321 } else if (mCurrentTarget == null) { 322 if (mPolicy.getNumTargets() == 1) { 323 animateFullscreenContainer(true); 324 } else { 325 animateSplitContainers(true, null /* animCompleteCallback */); 326 animateHighlight(target); 327 } 328 } else if (mCurrentTarget.type != target.type) { 329 // Switching between targets 330 mDropZoneView1.animateSwitch(); 331 mDropZoneView2.animateSwitch(); 332 // Announce for accessibility. 333 switch (target.type) { 334 case TYPE_SPLIT_LEFT: 335 mDropZoneView1.announceForAccessibility( 336 mContext.getString(R.string.accessibility_split_left)); 337 break; 338 case TYPE_SPLIT_RIGHT: 339 mDropZoneView2.announceForAccessibility( 340 mContext.getString(R.string.accessibility_split_right)); 341 break; 342 case TYPE_SPLIT_TOP: 343 mDropZoneView1.announceForAccessibility( 344 mContext.getString(R.string.accessibility_split_top)); 345 break; 346 case TYPE_SPLIT_BOTTOM: 347 mDropZoneView2.announceForAccessibility( 348 mContext.getString(R.string.accessibility_split_bottom)); 349 break; 350 } 351 } 352 mCurrentTarget = target; 353 } 354 } 355 356 /** 357 * Hides the drag layout and animates out the visible drop targets. 358 */ hide(DragEvent event, Runnable hideCompleteCallback)359 public void hide(DragEvent event, Runnable hideCompleteCallback) { 360 mIsShowing = false; 361 animateSplitContainers(false, () -> { 362 if (hideCompleteCallback != null) { 363 hideCompleteCallback.run(); 364 } 365 switch (event.getAction()) { 366 case DragEvent.ACTION_DROP: 367 case DragEvent.ACTION_DRAG_ENDED: 368 mSession = null; 369 } 370 }); 371 // Reset the state if we previously force-ignore the bottom margin 372 mDropZoneView1.setForceIgnoreBottomMargin(false); 373 mDropZoneView2.setForceIgnoreBottomMargin(false); 374 updateContainerMargins(getResources().getConfiguration().orientation); 375 mCurrentTarget = null; 376 } 377 378 /** 379 * Handles the drop onto a target and animates out the visible drop targets. 380 */ drop(DragEvent event, SurfaceControl dragSurface, Runnable dropCompleteCallback)381 public boolean drop(DragEvent event, SurfaceControl dragSurface, 382 Runnable dropCompleteCallback) { 383 final boolean handledDrop = mCurrentTarget != null; 384 mHasDropped = true; 385 386 // Process the drop 387 mPolicy.handleDrop(mCurrentTarget, event.getClipData()); 388 389 // Start animating the drop UI out with the drag surface 390 hide(event, dropCompleteCallback); 391 if (handledDrop) { 392 hideDragSurface(dragSurface); 393 } 394 return handledDrop; 395 } 396 hideDragSurface(SurfaceControl dragSurface)397 private void hideDragSurface(SurfaceControl dragSurface) { 398 final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); 399 final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f); 400 // Currently the splash icon animation runs with the default ValueAnimator duration of 401 // 300ms 402 dragSurfaceAnimator.setDuration(300); 403 dragSurfaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 404 dragSurfaceAnimator.addUpdateListener(animation -> { 405 float t = animation.getAnimatedFraction(); 406 float alpha = 1f - t; 407 // TODO: Scale the drag surface as well once we make all the source surfaces 408 // consistent 409 tx.setAlpha(dragSurface, alpha); 410 tx.apply(); 411 }); 412 dragSurfaceAnimator.addListener(new AnimatorListenerAdapter() { 413 private boolean mCanceled = false; 414 415 @Override 416 public void onAnimationCancel(Animator animation) { 417 cleanUpSurface(); 418 mCanceled = true; 419 } 420 421 @Override 422 public void onAnimationEnd(Animator animation) { 423 if (mCanceled) { 424 // Already handled above 425 return; 426 } 427 cleanUpSurface(); 428 } 429 430 private void cleanUpSurface() { 431 // Clean up the drag surface 432 tx.remove(dragSurface); 433 tx.apply(); 434 } 435 }); 436 dragSurfaceAnimator.start(); 437 } 438 animateFullscreenContainer(boolean visible)439 private void animateFullscreenContainer(boolean visible) { 440 mStatusBarManager.disable(visible 441 ? HIDE_STATUS_BAR_FLAGS 442 : DISABLE_NONE); 443 // We're only using the first drop zone if there is one fullscreen target 444 mDropZoneView1.setShowingMargin(visible); 445 mDropZoneView1.setShowingHighlight(visible); 446 } 447 animateSplitContainers(boolean visible, Runnable animCompleteCallback)448 private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) { 449 mStatusBarManager.disable(visible 450 ? HIDE_STATUS_BAR_FLAGS 451 : DISABLE_NONE); 452 mDropZoneView1.setShowingMargin(visible); 453 mDropZoneView2.setShowingMargin(visible); 454 Animator animator = mDropZoneView1.getAnimator(); 455 if (animCompleteCallback != null) { 456 if (animator != null) { 457 animator.addListener(new AnimatorListenerAdapter() { 458 @Override 459 public void onAnimationEnd(Animator animation) { 460 animCompleteCallback.run(); 461 } 462 }); 463 } else { 464 // If there's no animator the animation is done so run immediately 465 animCompleteCallback.run(); 466 } 467 } 468 } 469 animateHighlight(DragAndDropPolicy.Target target)470 private void animateHighlight(DragAndDropPolicy.Target target) { 471 if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) { 472 mDropZoneView1.setShowingHighlight(true); 473 mDropZoneView2.setShowingHighlight(false); 474 } else if (target.type == TYPE_SPLIT_RIGHT || target.type == TYPE_SPLIT_BOTTOM) { 475 mDropZoneView1.setShowingHighlight(false); 476 mDropZoneView2.setShowingHighlight(true); 477 } 478 } 479 getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo)480 private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { 481 final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); 482 return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); 483 } 484 } 485