1 /* 2 * Copyright (C) 2024 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.pip2.phone; 18 19 import android.annotation.IntDef; 20 import android.app.TaskInfo; 21 import android.graphics.Rect; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.view.SurfaceControl; 25 import android.window.WindowContainerToken; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.VisibleForTesting; 30 31 import com.android.internal.protolog.ProtoLog; 32 import com.android.internal.util.Preconditions; 33 import com.android.wm.shell.common.pip.PipDesktopState; 34 import com.android.wm.shell.protolog.ShellProtoLogGroup; 35 import com.android.wm.shell.shared.annotations.ShellMainThread; 36 37 import java.io.PrintWriter; 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Contains the state relevant to carry out or probe the status of PiP transitions. 45 * 46 * <p>Existing and new PiP components can subscribe to PiP transition related state changes 47 * via <code>PipTransitionStateChangedListener</code>.</p> 48 * 49 * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering. 50 * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs 51 * to some other class <code>Bar</code>, a special care must be given when manipulating state A in 52 * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of 53 * the class <code>Bar</code>.</p> 54 * 55 * <p>Hence, the recommended usage for classes who want to subscribe to 56 * <code>PipTransitionState</code> changes is to manipulate only their own internal state or 57 * <code>PipTransitionState</code> state.</p> 58 * 59 * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should 60 * just be moved to <code>PipTransitionState</code> and become a shared state 61 * between Foo and Bar.</p> 62 * 63 * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code> 64 * receives a <code>Bundle</code> extra object that can be optionally set via 65 * <code>setState(state, extra)</code>. This can be used to resolve extra information to update 66 * relevant internal or <code>PipTransitionState</code> state. However, each listener 67 * needs to check for whether the extra passed is correct for a particular state, 68 * and throw an <code>IllegalStateException</code> otherwise.</p> 69 */ 70 public class PipTransitionState { 71 private static final String TAG = PipTransitionState.class.getSimpleName(); 72 73 public static final int UNDEFINED = 0; 74 75 // State for Launcher animating the swipe PiP to home animation. 76 public static final int SWIPING_TO_PIP = 1; 77 78 // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation. 79 public static final int ENTERING_PIP = 2; 80 81 // State for app finishing drawing in PiP mode as a final step in enter PiP flow. 82 public static final int ENTERED_PIP = 3; 83 84 // State to indicate we have scheduled a PiP bounds change transition. 85 public static final int SCHEDULED_BOUNDS_CHANGE = 4; 86 87 // State for the start of playing a transition to change PiP bounds. At this point, WM Core 88 // is aware of the new PiP bounds, but Shell might still be continuing animating. 89 public static final int CHANGING_PIP_BOUNDS = 5; 90 91 // State for finishing animating into new PiP bounds after resize is complete. 92 public static final int CHANGED_PIP_BOUNDS = 6; 93 94 // State for starting exiting PiP. 95 public static final int EXITING_PIP = 7; 96 97 // State for finishing exit PiP flow. 98 public static final int EXITED_PIP = 8; 99 100 private static final int FIRST_CUSTOM_STATE = 1000; 101 102 private int mPrevCustomState = FIRST_CUSTOM_STATE; 103 104 @IntDef(prefix = { "TRANSITION_STATE_" }, value = { 105 UNDEFINED, 106 SWIPING_TO_PIP, 107 ENTERING_PIP, 108 ENTERED_PIP, 109 SCHEDULED_BOUNDS_CHANGE, 110 CHANGING_PIP_BOUNDS, 111 CHANGED_PIP_BOUNDS, 112 EXITING_PIP, 113 EXITED_PIP, 114 }) 115 @Retention(RetentionPolicy.SOURCE) 116 public @interface TransitionState {} 117 118 @TransitionState 119 private int mState; 120 121 // 122 // Dependencies 123 // 124 125 @ShellMainThread 126 private final Handler mMainHandler; 127 128 private final PipDesktopState mPipDesktopState; 129 130 // 131 // Swipe up to enter PiP related state 132 // 133 134 // true if Launcher has started swipe PiP to home animation 135 private boolean mInSwipePipToHomeTransition; 136 137 // App bounds used when as a starting point to swipe PiP to home animation in Launcher; 138 // these are also used to calculate the app icon overlay buffer size. 139 @NonNull 140 private final Rect mSwipePipToHomeAppBounds = new Rect(); 141 142 // 143 // Task related caches 144 // 145 146 // pinned PiP task's leash 147 @Nullable 148 private SurfaceControl mPinnedTaskLeash; 149 150 // pinned PiP task info 151 @Nullable 152 private TaskInfo mPipTaskInfo; 153 154 // Overlay leash potentially used during swipe PiP to home transition; 155 // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. 156 @Nullable 157 private SurfaceControl mSwipePipToHomeOverlay; 158 159 // 160 // Scheduling-related state 161 // 162 @Nullable 163 private Runnable mOnIdlePipTransitionStateRunnable; 164 165 private boolean mInFixedRotation = false; 166 167 /** 168 * An interface to track state updates as we progress through PiP transitions. 169 */ 170 public interface PipTransitionStateChangedListener { 171 172 /** Reports changes in PiP transition state. */ onPipTransitionStateChanged(@ransitionState int oldState, @TransitionState int newState, @Nullable Bundle extra)173 void onPipTransitionStateChanged(@TransitionState int oldState, 174 @TransitionState int newState, @Nullable Bundle extra); 175 } 176 177 private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>(); 178 PipTransitionState(@hellMainThread Handler handler, PipDesktopState pipDesktopState)179 public PipTransitionState(@ShellMainThread Handler handler, PipDesktopState pipDesktopState) { 180 mMainHandler = handler; 181 mPipDesktopState = pipDesktopState; 182 } 183 184 /** 185 * @return the state of PiP in the context of transitions. 186 */ 187 @TransitionState getState()188 public int getState() { 189 return mState; 190 } 191 192 /** 193 * Sets the state of PiP in the context of transitions. 194 */ setState(@ransitionState int state)195 public void setState(@TransitionState int state) { 196 setState(state, null /* extra */); 197 } 198 199 /** 200 * Sets the state of PiP in the context of transitions 201 * 202 * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. 203 */ setState(@ransitionState int state, @Nullable Bundle extra)204 public void setState(@TransitionState int state, @Nullable Bundle extra) { 205 if (state == ENTERING_PIP || state == SWIPING_TO_PIP 206 || state == SCHEDULED_BOUNDS_CHANGE || state == CHANGING_PIP_BOUNDS) { 207 // States listed above require extra bundles to be provided. 208 Preconditions.checkArgument(extra != null && !extra.isEmpty(), 209 "No extra bundle for " + stateToString(state) + " state."); 210 } 211 if (!shouldTransitionToState(state)) { 212 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 213 "%s: Attempted to transition to an invalid state=%s, while in %s", 214 TAG, stateToString(state), this); 215 return; 216 } 217 218 if (mState != state) { 219 final int prevState = mState; 220 mState = state; 221 dispatchPipTransitionStateChanged(prevState, mState, extra); 222 } 223 224 maybeRunOnIdlePipTransitionStateCallback(); 225 } 226 227 /** 228 * Posts the state update for PiP in the context of transitions onto the main handler. 229 * 230 * <p>This is done to guarantee that any callback dispatches for the present state are 231 * complete. This is relevant for states that have multiple listeners, such as 232 * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with 233 * the actual transition scheduling.</p> 234 */ postState(@ransitionState int state)235 public void postState(@TransitionState int state) { 236 postState(state, null /* extra */); 237 } 238 239 /** 240 * Posts the state update for PiP in the context of transitions onto the main handler. 241 * 242 * <p>This is done to guarantee that any callback dispatches for the present state are 243 * complete. This is relevant for states that have multiple listeners, such as 244 * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with 245 * the actual transition scheduling.</p> 246 * 247 * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. 248 */ postState(@ransitionState int state, @Nullable Bundle extra)249 public void postState(@TransitionState int state, @Nullable Bundle extra) { 250 mMainHandler.post(() -> setState(state, extra)); 251 } 252 dispatchPipTransitionStateChanged(@ransitionState int oldState, @TransitionState int newState, @Nullable Bundle extra)253 private void dispatchPipTransitionStateChanged(@TransitionState int oldState, 254 @TransitionState int newState, @Nullable Bundle extra) { 255 mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra)); 256 } 257 258 /** 259 * Schedule a callback to run when in a valid idle PiP state. 260 * 261 * <p>We only allow for one callback to be scheduled to avoid cases with multiple transitions 262 * being scheduled. For instance, if user double taps and IME shows, this would 263 * schedule a bounds change transition for IME appearing. But if some other transition would 264 * want to animate PiP before the scheduled callback executes, we would rather want to replace 265 * the existing callback with a new one, to avoid multiple animations 266 * as soon as we are idle.</p> 267 */ setOnIdlePipTransitionStateRunnable( @ullable Runnable onIdlePipTransitionStateRunnable)268 public void setOnIdlePipTransitionStateRunnable( 269 @Nullable Runnable onIdlePipTransitionStateRunnable) { 270 mOnIdlePipTransitionStateRunnable = onIdlePipTransitionStateRunnable; 271 maybeRunOnIdlePipTransitionStateCallback(); 272 } 273 maybeRunOnIdlePipTransitionStateCallback()274 private void maybeRunOnIdlePipTransitionStateCallback() { 275 if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) { 276 mMainHandler.post(mOnIdlePipTransitionStateRunnable); 277 mOnIdlePipTransitionStateRunnable = null; 278 } 279 } 280 281 /** 282 * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. 283 */ addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener)284 public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { 285 if (mCallbacks.contains(listener)) { 286 return; 287 } 288 mCallbacks.add(listener); 289 } 290 291 /** 292 * @return true if provided {@link PipTransitionStateChangedListener} 293 * is registered before removing it. 294 */ removePipTransitionStateChangedListener( PipTransitionStateChangedListener listener)295 public boolean removePipTransitionStateChangedListener( 296 PipTransitionStateChangedListener listener) { 297 return mCallbacks.remove(listener); 298 } 299 300 /** 301 * @return true if we have fully entered PiP. 302 */ isInPip()303 public boolean isInPip() { 304 return mState > ENTERING_PIP && mState < EXITING_PIP; 305 } 306 setSwipePipToHomeState(@ullable SurfaceControl overlayLeash, @NonNull Rect appBounds)307 void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash, 308 @NonNull Rect appBounds) { 309 mInSwipePipToHomeTransition = true; 310 if (overlayLeash != null && !appBounds.isEmpty()) { 311 mSwipePipToHomeOverlay = overlayLeash; 312 mSwipePipToHomeAppBounds.set(appBounds); 313 } 314 } 315 resetSwipePipToHomeState()316 void resetSwipePipToHomeState() { 317 mInSwipePipToHomeTransition = false; 318 mSwipePipToHomeOverlay = null; 319 mSwipePipToHomeAppBounds.setEmpty(); 320 } 321 322 @Nullable getPipTaskToken()323 public WindowContainerToken getPipTaskToken() { 324 return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null; 325 } 326 getPinnedTaskLeash()327 @Nullable SurfaceControl getPinnedTaskLeash() { 328 return mPinnedTaskLeash; 329 } 330 setPinnedTaskLeash(@ullable SurfaceControl leash)331 void setPinnedTaskLeash(@Nullable SurfaceControl leash) { 332 mPinnedTaskLeash = leash; 333 } 334 getPipTaskInfo()335 @Nullable TaskInfo getPipTaskInfo() { 336 return mPipTaskInfo; 337 } 338 setPipTaskInfo(@ullable TaskInfo pipTaskInfo)339 void setPipTaskInfo(@Nullable TaskInfo pipTaskInfo) { 340 mPipTaskInfo = pipTaskInfo; 341 } 342 343 /** 344 * @return true if either in swipe or button-nav fixed rotation. 345 */ isInFixedRotation()346 public boolean isInFixedRotation() { 347 return mInFixedRotation; 348 } 349 350 /** 351 * Sets the fixed rotation flag. 352 */ setInFixedRotation(boolean inFixedRotation)353 public void setInFixedRotation(boolean inFixedRotation) { 354 mInFixedRotation = inFixedRotation; 355 if (!inFixedRotation) { 356 maybeRunOnIdlePipTransitionStateCallback(); 357 } 358 } 359 360 /** 361 * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. 362 */ isInSwipePipToHomeTransition()363 public boolean isInSwipePipToHomeTransition() { 364 return mInSwipePipToHomeTransition; 365 } 366 367 /** 368 * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP; 369 * null if srcRectHint provided is valid. 370 */ 371 @Nullable getSwipePipToHomeOverlay()372 public SurfaceControl getSwipePipToHomeOverlay() { 373 return mSwipePipToHomeOverlay; 374 } 375 376 /** 377 * @return app bounds used to calculate 378 */ 379 @NonNull getSwipePipToHomeAppBounds()380 public Rect getSwipePipToHomeAppBounds() { 381 return mSwipePipToHomeAppBounds; 382 } 383 384 /** 385 * @return a custom state solely for internal use by the caller. 386 */ 387 @TransitionState getCustomState()388 public int getCustomState() { 389 return ++mPrevCustomState; 390 } 391 392 @VisibleForTesting shouldTransitionToState(@ransitionState int newState)393 boolean shouldTransitionToState(@TransitionState int newState) { 394 switch (newState) { 395 case SCHEDULED_BOUNDS_CHANGE: 396 // Allow scheduling bounds change only when both of these are true: 397 // - while in PiP, except for if another bounds change was scheduled but hasn't 398 // started playing yet 399 // - there is no drag-to-desktop gesture in progress; otherwise the PiP resize 400 // transition will block the drag-to-desktop transitions from finishing 401 return isInPip() && !mPipDesktopState.isDragToDesktopInProgress(); 402 default: 403 return true; 404 } 405 } 406 stateToString(int state)407 private static String stateToString(int state) { 408 switch (state) { 409 case UNDEFINED: return "undefined"; 410 case SWIPING_TO_PIP: return "swiping_to_pip"; 411 case ENTERING_PIP: return "entering-pip"; 412 case ENTERED_PIP: return "entered-pip"; 413 case SCHEDULED_BOUNDS_CHANGE: return "scheduled_bounds_change"; 414 case CHANGING_PIP_BOUNDS: return "changing-bounds"; 415 case CHANGED_PIP_BOUNDS: return "changed-bounds"; 416 case EXITING_PIP: return "exiting-pip"; 417 case EXITED_PIP: return "exited-pip"; 418 } 419 throw new IllegalStateException("Unknown state: " + state); 420 } 421 isPipStateIdle()422 public boolean isPipStateIdle() { 423 // This needs to be a valid in-PiP state that isn't a transient state. 424 return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation(); 425 } 426 427 @Override toString()428 public String toString() { 429 return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", 430 stateToString(mState), mInSwipePipToHomeTransition); 431 } 432 433 /** Dumps internal state. */ dump(PrintWriter pw, String prefix)434 public void dump(PrintWriter pw, String prefix) { 435 final String innerPrefix = prefix + " "; 436 pw.println(prefix + TAG); 437 pw.println(innerPrefix + "mState=" + stateToString(mState)); 438 } 439 } 440