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