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