1 /* 2 * Copyright (C) 2021 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.common.split; 18 19 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 20 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 21 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; 22 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 23 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 24 25 import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.ValueAnimator; 30 import android.app.ActivityManager; 31 import android.content.Context; 32 import android.content.res.Configuration; 33 import android.graphics.Color; 34 import android.graphics.PixelFormat; 35 import android.graphics.Rect; 36 import android.graphics.drawable.Drawable; 37 import android.os.Binder; 38 import android.view.IWindow; 39 import android.view.LayoutInflater; 40 import android.view.SurfaceControl; 41 import android.view.SurfaceControlViewHost; 42 import android.view.SurfaceSession; 43 import android.view.View; 44 import android.view.WindowManager; 45 import android.view.WindowlessWindowManager; 46 import android.widget.FrameLayout; 47 import android.widget.ImageView; 48 49 import androidx.annotation.NonNull; 50 51 import com.android.launcher3.icons.IconProvider; 52 import com.android.wm.shell.R; 53 import com.android.wm.shell.common.ScreenshotUtils; 54 import com.android.wm.shell.common.SurfaceUtils; 55 56 import java.util.function.Consumer; 57 58 /** 59 * Handles split decor like showing resizing hint for a specific split. 60 */ 61 public class SplitDecorManager extends WindowlessWindowManager { 62 private static final String TAG = SplitDecorManager.class.getSimpleName(); 63 private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground"; 64 private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; 65 66 private final IconProvider mIconProvider; 67 private final SurfaceSession mSurfaceSession; 68 69 private Drawable mIcon; 70 private ImageView mResizingIconView; 71 private SurfaceControlViewHost mViewHost; 72 private SurfaceControl mHostLeash; 73 private SurfaceControl mIconLeash; 74 private SurfaceControl mBackgroundLeash; 75 private SurfaceControl mGapBackgroundLeash; 76 private SurfaceControl mScreenshot; 77 78 private boolean mShown; 79 private boolean mIsResizing; 80 private final Rect mOldBounds = new Rect(); 81 private final Rect mResizingBounds = new Rect(); 82 private final Rect mTempRect = new Rect(); 83 private ValueAnimator mFadeAnimator; 84 private ValueAnimator mScreenshotAnimator; 85 86 private int mIconSize; 87 private int mOffsetX; 88 private int mOffsetY; 89 private int mRunningAnimationCount = 0; 90 SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession)91 public SplitDecorManager(Configuration configuration, IconProvider iconProvider, 92 SurfaceSession surfaceSession) { 93 super(configuration, null /* rootSurface */, null /* hostInputToken */); 94 mIconProvider = iconProvider; 95 mSurfaceSession = surfaceSession; 96 } 97 98 @Override getParentSurface(IWindow window, WindowManager.LayoutParams attrs)99 protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { 100 // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. 101 final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) 102 .setContainerLayer() 103 .setName(TAG) 104 .setHidden(true) 105 .setParent(mHostLeash) 106 .setCallsite("SplitDecorManager#attachToParentSurface"); 107 mIconLeash = builder.build(); 108 return mIconLeash; 109 } 110 111 /** Inflates split decor surface on the root surface. */ inflate(Context context, SurfaceControl rootLeash, Rect rootBounds)112 public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) { 113 if (mIconLeash != null && mViewHost != null) { 114 return; 115 } 116 117 context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, 118 null /* options */); 119 mHostLeash = rootLeash; 120 mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this, 121 "SplitDecorManager"); 122 123 mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); 124 final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) 125 .inflate(R.layout.split_decor, null); 126 mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); 127 128 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 129 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, 130 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); 131 lp.width = rootBounds.width(); 132 lp.height = rootBounds.height(); 133 lp.token = new Binder(); 134 lp.setTitle(TAG); 135 lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; 136 // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports 137 // TRUSTED_OVERLAY for windowless window without input channel. 138 mViewHost.setView(rootLayout, lp); 139 } 140 141 /** Releases the surfaces for split decor. */ release(SurfaceControl.Transaction t)142 public void release(SurfaceControl.Transaction t) { 143 if (mFadeAnimator != null) { 144 if (mFadeAnimator.isRunning()) { 145 mFadeAnimator.cancel(); 146 } 147 mFadeAnimator = null; 148 } 149 if (mScreenshotAnimator != null) { 150 if (mScreenshotAnimator.isRunning()) { 151 mScreenshotAnimator.cancel(); 152 } 153 mScreenshotAnimator = null; 154 } 155 if (mViewHost != null) { 156 mViewHost.release(); 157 mViewHost = null; 158 } 159 if (mIconLeash != null) { 160 t.remove(mIconLeash); 161 mIconLeash = null; 162 } 163 if (mBackgroundLeash != null) { 164 t.remove(mBackgroundLeash); 165 mBackgroundLeash = null; 166 } 167 if (mGapBackgroundLeash != null) { 168 t.remove(mGapBackgroundLeash); 169 mGapBackgroundLeash = null; 170 } 171 if (mScreenshot != null) { 172 t.remove(mScreenshot); 173 mScreenshot = null; 174 } 175 mHostLeash = null; 176 mIcon = null; 177 mResizingIconView = null; 178 mIsResizing = false; 179 mShown = false; 180 mOldBounds.setEmpty(); 181 mResizingBounds.setEmpty(); 182 } 183 184 /** Showing resizing hint. */ onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, boolean immediately)185 public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, 186 Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, 187 boolean immediately) { 188 if (mResizingIconView == null) { 189 return; 190 } 191 192 if (!mIsResizing) { 193 mIsResizing = true; 194 mOldBounds.set(newBounds); 195 } 196 mResizingBounds.set(newBounds); 197 mOffsetX = offsetX; 198 mOffsetY = offsetY; 199 200 final boolean show = 201 newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height(); 202 final boolean update = show != mShown; 203 if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) { 204 // If we need to animate and animator still running, cancel it before we ensure both 205 // background and icon surfaces are non null for next animation. 206 mFadeAnimator.cancel(); 207 } 208 209 if (mBackgroundLeash == null) { 210 mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, 211 RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); 212 t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) 213 .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); 214 } 215 216 if (mGapBackgroundLeash == null && !immediately) { 217 final boolean isLandscape = newBounds.height() == sideBounds.height(); 218 final int left = isLandscape ? mOldBounds.width() : 0; 219 final int top = isLandscape ? 0 : mOldBounds.height(); 220 mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, 221 GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); 222 // Fill up another side bounds area. 223 t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) 224 .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) 225 .setPosition(mGapBackgroundLeash, left, top) 226 .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); 227 } 228 229 if (mIcon == null && resizingTask.topActivityInfo != null) { 230 mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); 231 mResizingIconView.setImageDrawable(mIcon); 232 mResizingIconView.setVisibility(View.VISIBLE); 233 234 WindowManager.LayoutParams lp = 235 (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); 236 lp.width = mIconSize; 237 lp.height = mIconSize; 238 mViewHost.relayout(lp); 239 t.setLayer(mIconLeash, Integer.MAX_VALUE); 240 } 241 t.setPosition(mIconLeash, 242 newBounds.width() / 2 - mIconSize / 2, 243 newBounds.height() / 2 - mIconSize / 2); 244 245 if (update) { 246 if (immediately) { 247 t.setVisibility(mBackgroundLeash, show); 248 t.setVisibility(mIconLeash, show); 249 } else { 250 startFadeAnimation(show, false, null); 251 } 252 mShown = show; 253 } 254 } 255 256 /** Stops showing resizing hint. */ onResized(SurfaceControl.Transaction t, Consumer<Boolean> animFinishedCallback)257 public void onResized(SurfaceControl.Transaction t, Consumer<Boolean> animFinishedCallback) { 258 if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { 259 mScreenshotAnimator.cancel(); 260 } 261 262 if (mScreenshot != null) { 263 t.setPosition(mScreenshot, mOffsetX, mOffsetY); 264 265 final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); 266 mScreenshotAnimator = ValueAnimator.ofFloat(1, 0); 267 mScreenshotAnimator.setDuration(FADE_DURATION); 268 mScreenshotAnimator.addUpdateListener(valueAnimator -> { 269 final float progress = (float) valueAnimator.getAnimatedValue(); 270 animT.setAlpha(mScreenshot, progress); 271 animT.apply(); 272 }); 273 mScreenshotAnimator.addListener(new AnimatorListenerAdapter() { 274 @Override 275 public void onAnimationStart(Animator animation) { 276 mRunningAnimationCount++; 277 } 278 279 @Override 280 public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) { 281 mRunningAnimationCount--; 282 animT.remove(mScreenshot); 283 animT.apply(); 284 animT.close(); 285 mScreenshot = null; 286 287 if (mRunningAnimationCount == 0 && animFinishedCallback != null) { 288 animFinishedCallback.accept(true); 289 } 290 } 291 }); 292 mScreenshotAnimator.start(); 293 } 294 295 if (mResizingIconView == null) { 296 if (mRunningAnimationCount == 0 && animFinishedCallback != null) { 297 animFinishedCallback.accept(false); 298 } 299 return; 300 } 301 302 mIsResizing = false; 303 mOffsetX = 0; 304 mOffsetY = 0; 305 mOldBounds.setEmpty(); 306 mResizingBounds.setEmpty(); 307 if (mFadeAnimator != null && mFadeAnimator.isRunning()) { 308 if (!mShown) { 309 // If fade-out animation is running, just add release callback to it. 310 SurfaceControl.Transaction finishT = new SurfaceControl.Transaction(); 311 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 312 @Override 313 public void onAnimationEnd(Animator animation) { 314 releaseDecor(finishT); 315 finishT.apply(); 316 finishT.close(); 317 if (mRunningAnimationCount == 0 && animFinishedCallback != null) { 318 animFinishedCallback.accept(true); 319 } 320 } 321 }); 322 return; 323 } 324 } 325 if (mShown) { 326 fadeOutDecor(()-> { 327 if (mRunningAnimationCount == 0 && animFinishedCallback != null) { 328 animFinishedCallback.accept(true); 329 } 330 }); 331 } else { 332 // Decor surface is hidden so release it directly. 333 releaseDecor(t); 334 if (mRunningAnimationCount == 0 && animFinishedCallback != null) { 335 animFinishedCallback.accept(false); 336 } 337 } 338 } 339 340 /** Screenshot host leash and attach on it if meet some conditions */ screenshotIfNeeded(SurfaceControl.Transaction t)341 public void screenshotIfNeeded(SurfaceControl.Transaction t) { 342 if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { 343 if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { 344 mScreenshotAnimator.cancel(); 345 } else if (mScreenshot != null) { 346 t.remove(mScreenshot); 347 } 348 349 mTempRect.set(mOldBounds); 350 mTempRect.offsetTo(0, 0); 351 mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, 352 Integer.MAX_VALUE - 1); 353 } 354 } 355 356 /** Set screenshot and attach on host leash it if meet some conditions */ setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t)357 public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { 358 if (screenshot == null || !screenshot.isValid()) return; 359 360 if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { 361 if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { 362 mScreenshotAnimator.cancel(); 363 } else if (mScreenshot != null) { 364 t.remove(mScreenshot); 365 } 366 367 mScreenshot = screenshot; 368 t.reparent(screenshot, mHostLeash); 369 t.setLayer(screenshot, Integer.MAX_VALUE - 1); 370 } 371 } 372 373 /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback 374 * directly. */ fadeOutDecor(Runnable finishedCallback)375 public void fadeOutDecor(Runnable finishedCallback) { 376 if (mShown) { 377 // If previous animation is running, just cancel it. 378 if (mFadeAnimator != null && mFadeAnimator.isRunning()) { 379 mFadeAnimator.cancel(); 380 } 381 382 startFadeAnimation(false /* show */, true, finishedCallback); 383 mShown = false; 384 } else { 385 if (finishedCallback != null) finishedCallback.run(); 386 } 387 } 388 startFadeAnimation(boolean show, boolean releaseSurface, Runnable finishedCallback)389 private void startFadeAnimation(boolean show, boolean releaseSurface, 390 Runnable finishedCallback) { 391 final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); 392 mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); 393 mFadeAnimator.setDuration(FADE_DURATION); 394 mFadeAnimator.addUpdateListener(valueAnimator-> { 395 final float progress = (float) valueAnimator.getAnimatedValue(); 396 if (mBackgroundLeash != null) { 397 animT.setAlpha(mBackgroundLeash, show ? progress : 1 - progress); 398 } 399 if (mIconLeash != null) { 400 animT.setAlpha(mIconLeash, show ? progress : 1 - progress); 401 } 402 animT.apply(); 403 }); 404 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 405 @Override 406 public void onAnimationStart(@NonNull Animator animation) { 407 mRunningAnimationCount++; 408 if (show) { 409 animT.show(mBackgroundLeash).show(mIconLeash); 410 } 411 if (mGapBackgroundLeash != null) { 412 animT.setVisibility(mGapBackgroundLeash, show); 413 } 414 animT.apply(); 415 } 416 417 @Override 418 public void onAnimationEnd(@NonNull Animator animation) { 419 mRunningAnimationCount--; 420 if (!show) { 421 if (mBackgroundLeash != null) { 422 animT.hide(mBackgroundLeash); 423 } 424 if (mIconLeash != null) { 425 animT.hide(mIconLeash); 426 } 427 } 428 if (releaseSurface) { 429 releaseDecor(animT); 430 } 431 animT.apply(); 432 animT.close(); 433 434 if (mRunningAnimationCount == 0 && finishedCallback != null) { 435 finishedCallback.run(); 436 } 437 } 438 }); 439 mFadeAnimator.start(); 440 } 441 442 /** Release or hide decor hint. */ releaseDecor(SurfaceControl.Transaction t)443 private void releaseDecor(SurfaceControl.Transaction t) { 444 if (mBackgroundLeash != null) { 445 t.remove(mBackgroundLeash); 446 mBackgroundLeash = null; 447 } 448 449 if (mGapBackgroundLeash != null) { 450 t.remove(mGapBackgroundLeash); 451 mGapBackgroundLeash = null; 452 } 453 454 if (mIcon != null) { 455 mResizingIconView.setVisibility(View.GONE); 456 mResizingIconView.setImageDrawable(null); 457 t.hide(mIconLeash); 458 mIcon = null; 459 } 460 } 461 getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo)462 private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { 463 final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); 464 return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); 465 } 466 } 467