Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +1 −0 Original line number Diff line number Diff line Loading @@ -479,6 +479,7 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); }); } else { mPipTransitionState.setIsPipBoundsChangingWithDisplay(true); t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds()); } // Update the size spec in PipBoundsState afterwards. Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +5 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; import com.android.wm.shell.pip2.phone.transition.PipDisplayChangeObserver; import com.android.wm.shell.pip2.phone.transition.PipExpandHandler; import com.android.wm.shell.pip2.phone.transition.PipTransitionUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; Loading Loading @@ -143,6 +144,7 @@ public class PipTransition extends PipTransitionController implements // Internal state and relevant cached info // private final PipExpandHandler mExpandHandler; private final PipDisplayChangeObserver mPipDisplayChangeObserver; private Transitions.TransitionFinishCallback mFinishCallback; Loading Loading @@ -189,12 +191,15 @@ public class PipTransition extends PipTransitionController implements pipBoundsState, pipBoundsAlgorithm, pipTransitionState, pipDisplayLayoutState, pipDesktopState, pipInteractionHandler, splitScreenControllerOptional); mPipDisplayChangeObserver = new PipDisplayChangeObserver(pipTransitionState, pipBoundsState); } @Override protected void onInit() { if (PipFlags.isPip2ExperimentEnabled()) { mTransitions.addHandler(this); mTransitions.registerObserver(mPipDisplayChangeObserver); } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +30 −3 Original line number Diff line number Diff line Loading @@ -169,6 +169,8 @@ public class PipTransitionState { private boolean mInFixedRotation = false; private boolean mIsPipBoundsChangingWithDisplay = false; /** * An interface to track state updates as we progress through PiP transitions. */ Loading Loading @@ -373,6 +375,25 @@ public class PipTransitionState { } } /** * @return true if a display change is ungoing with a PiP bounds change. */ public boolean isPipBoundsChangingWithDisplay() { return mIsPipBoundsChangingWithDisplay; } /** * Sets the PiP bounds change with display change flag. */ public void setIsPipBoundsChangingWithDisplay(boolean isPipBoundsChangingWithDisplay) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Set mIsPipBoundsChangingWithDisplay=%b", TAG, isPipBoundsChangingWithDisplay); mIsPipBoundsChangingWithDisplay = isPipBoundsChangingWithDisplay; if (!isPipBoundsChangingWithDisplay) { maybeRunOnIdlePipTransitionStateCallback(); } } /** * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. */ Loading Loading @@ -438,13 +459,16 @@ public class PipTransitionState { public boolean isPipStateIdle() { // This needs to be a valid in-PiP state that isn't a transient state. return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation(); return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation() && !isPipBoundsChangingWithDisplay(); } @Override public String toString() { return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", stateToString(mState), mInSwipePipToHomeTransition); return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b, " + "mIsPipBoundsChangingWithDisplay=%b, mInFixedRotation=%b", stateToString(mState), mInSwipePipToHomeTransition, mIsPipBoundsChangingWithDisplay, mInFixedRotation); } /** Dumps internal state. */ Loading @@ -452,5 +476,8 @@ public class PipTransitionState { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); pw.println(innerPrefix + "mState=" + stateToString(mState)); pw.println(innerPrefix + "mInFixedRotation=" + mInFixedRotation); pw.println(innerPrefix + "mIsPipBoundsChangingWithDisplay=" + mIsPipBoundsChangingWithDisplay); } } libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipDisplayChangeObserver.java 0 → 100644 +93 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.pip2.phone.transition; import android.graphics.Rect; import android.os.IBinder; import android.util.Pair; import android.view.SurfaceControl; import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.transition.Transitions; /** * An implementation of {@link Transitions.TransitionObserver} to track of external transitions * that might affect a PiP task as well. */ public class PipDisplayChangeObserver implements Transitions.TransitionObserver { private final PipTransitionState mPipTransitionState; private final PipBoundsState mPipBoundsState; @Nullable private Pair<IBinder, TransitionInfo> mDisplayChangeTransition; public PipDisplayChangeObserver(PipTransitionState pipTransitionState, PipBoundsState pipBoundsState) { mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; } @Override public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { if (TransitionUtil.hasDisplayChange(info)) { mDisplayChangeTransition = new Pair<>(transition, info); } } @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { if (mDisplayChangeTransition != null && mDisplayChangeTransition.first == merged) { maybeUpdatePipStateOnDisplayChange(mDisplayChangeTransition.second /* info */); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); mDisplayChangeTransition = null; } } @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { if (mDisplayChangeTransition != null && mDisplayChangeTransition.first == transition) { maybeUpdatePipStateOnDisplayChange(mDisplayChangeTransition.second /* info */); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); mDisplayChangeTransition = null; } } @VisibleForTesting @Nullable Pair<IBinder, TransitionInfo> getDisplayChangeTransition() { return mDisplayChangeTransition; } private void maybeUpdatePipStateOnDisplayChange(@NonNull TransitionInfo info) { final TransitionInfo.Change pipChange = PipTransitionUtils.getPipChange(info); if (pipChange == null) return; final Rect endBounds = pipChange.getEndAbsBounds(); mPipBoundsState.setBounds(endBounds); } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java +21 −0 Original line number Diff line number Diff line Loading @@ -267,6 +267,27 @@ public class PipTransitionStateTest extends ShellTestCase { mPipTransitionState.getOnIdlePipTransitionStateRunnable()); } @Test public void testSetIsPipBoundsChangingWithDisplay_toFalse_thenIdle() { when(mMainHandler.obtainMessage(anyInt())).thenAnswer(invocation -> new Message().setWhat(invocation.getArgument(0))); // Pick an initially idle ENTERED_PIP state mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); // Enter an non-idle state as PiP bounds change with the display mPipTransitionState.setIsPipBoundsChangingWithDisplay(true); final Runnable onIdleRunnable = mock(Runnable.class); mPipTransitionState.setOnIdlePipTransitionStateRunnable(onIdleRunnable); // We are supposed to be in a non-idle state, so the runnable should not be posted yet. verify(mMainHandler, never()).sendMessage(any()); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); verify(mMainHandler, times(1)) .sendMessage(argThat(msg -> msg.getCallback() == onIdleRunnable)); } @Test public void testPostState_noImmediateStateChange_postedOnHandler() { mPipTransitionState.setState(PipTransitionState.UNDEFINED); Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +1 −0 Original line number Diff line number Diff line Loading @@ -479,6 +479,7 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); }); } else { mPipTransitionState.setIsPipBoundsChangingWithDisplay(true); t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds()); } // Update the size spec in PipBoundsState afterwards. Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +5 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; import com.android.wm.shell.pip2.phone.transition.PipDisplayChangeObserver; import com.android.wm.shell.pip2.phone.transition.PipExpandHandler; import com.android.wm.shell.pip2.phone.transition.PipTransitionUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; Loading Loading @@ -143,6 +144,7 @@ public class PipTransition extends PipTransitionController implements // Internal state and relevant cached info // private final PipExpandHandler mExpandHandler; private final PipDisplayChangeObserver mPipDisplayChangeObserver; private Transitions.TransitionFinishCallback mFinishCallback; Loading Loading @@ -189,12 +191,15 @@ public class PipTransition extends PipTransitionController implements pipBoundsState, pipBoundsAlgorithm, pipTransitionState, pipDisplayLayoutState, pipDesktopState, pipInteractionHandler, splitScreenControllerOptional); mPipDisplayChangeObserver = new PipDisplayChangeObserver(pipTransitionState, pipBoundsState); } @Override protected void onInit() { if (PipFlags.isPip2ExperimentEnabled()) { mTransitions.addHandler(this); mTransitions.registerObserver(mPipDisplayChangeObserver); } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +30 −3 Original line number Diff line number Diff line Loading @@ -169,6 +169,8 @@ public class PipTransitionState { private boolean mInFixedRotation = false; private boolean mIsPipBoundsChangingWithDisplay = false; /** * An interface to track state updates as we progress through PiP transitions. */ Loading Loading @@ -373,6 +375,25 @@ public class PipTransitionState { } } /** * @return true if a display change is ungoing with a PiP bounds change. */ public boolean isPipBoundsChangingWithDisplay() { return mIsPipBoundsChangingWithDisplay; } /** * Sets the PiP bounds change with display change flag. */ public void setIsPipBoundsChangingWithDisplay(boolean isPipBoundsChangingWithDisplay) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Set mIsPipBoundsChangingWithDisplay=%b", TAG, isPipBoundsChangingWithDisplay); mIsPipBoundsChangingWithDisplay = isPipBoundsChangingWithDisplay; if (!isPipBoundsChangingWithDisplay) { maybeRunOnIdlePipTransitionStateCallback(); } } /** * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. */ Loading Loading @@ -438,13 +459,16 @@ public class PipTransitionState { public boolean isPipStateIdle() { // This needs to be a valid in-PiP state that isn't a transient state. return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation(); return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation() && !isPipBoundsChangingWithDisplay(); } @Override public String toString() { return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", stateToString(mState), mInSwipePipToHomeTransition); return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b, " + "mIsPipBoundsChangingWithDisplay=%b, mInFixedRotation=%b", stateToString(mState), mInSwipePipToHomeTransition, mIsPipBoundsChangingWithDisplay, mInFixedRotation); } /** Dumps internal state. */ Loading @@ -452,5 +476,8 @@ public class PipTransitionState { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); pw.println(innerPrefix + "mState=" + stateToString(mState)); pw.println(innerPrefix + "mInFixedRotation=" + mInFixedRotation); pw.println(innerPrefix + "mIsPipBoundsChangingWithDisplay=" + mIsPipBoundsChangingWithDisplay); } }
libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipDisplayChangeObserver.java 0 → 100644 +93 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.pip2.phone.transition; import android.graphics.Rect; import android.os.IBinder; import android.util.Pair; import android.view.SurfaceControl; import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.transition.Transitions; /** * An implementation of {@link Transitions.TransitionObserver} to track of external transitions * that might affect a PiP task as well. */ public class PipDisplayChangeObserver implements Transitions.TransitionObserver { private final PipTransitionState mPipTransitionState; private final PipBoundsState mPipBoundsState; @Nullable private Pair<IBinder, TransitionInfo> mDisplayChangeTransition; public PipDisplayChangeObserver(PipTransitionState pipTransitionState, PipBoundsState pipBoundsState) { mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; } @Override public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { if (TransitionUtil.hasDisplayChange(info)) { mDisplayChangeTransition = new Pair<>(transition, info); } } @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { if (mDisplayChangeTransition != null && mDisplayChangeTransition.first == merged) { maybeUpdatePipStateOnDisplayChange(mDisplayChangeTransition.second /* info */); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); mDisplayChangeTransition = null; } } @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { if (mDisplayChangeTransition != null && mDisplayChangeTransition.first == transition) { maybeUpdatePipStateOnDisplayChange(mDisplayChangeTransition.second /* info */); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); mDisplayChangeTransition = null; } } @VisibleForTesting @Nullable Pair<IBinder, TransitionInfo> getDisplayChangeTransition() { return mDisplayChangeTransition; } private void maybeUpdatePipStateOnDisplayChange(@NonNull TransitionInfo info) { final TransitionInfo.Change pipChange = PipTransitionUtils.getPipChange(info); if (pipChange == null) return; final Rect endBounds = pipChange.getEndAbsBounds(); mPipBoundsState.setBounds(endBounds); } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java +21 −0 Original line number Diff line number Diff line Loading @@ -267,6 +267,27 @@ public class PipTransitionStateTest extends ShellTestCase { mPipTransitionState.getOnIdlePipTransitionStateRunnable()); } @Test public void testSetIsPipBoundsChangingWithDisplay_toFalse_thenIdle() { when(mMainHandler.obtainMessage(anyInt())).thenAnswer(invocation -> new Message().setWhat(invocation.getArgument(0))); // Pick an initially idle ENTERED_PIP state mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); // Enter an non-idle state as PiP bounds change with the display mPipTransitionState.setIsPipBoundsChangingWithDisplay(true); final Runnable onIdleRunnable = mock(Runnable.class); mPipTransitionState.setOnIdlePipTransitionStateRunnable(onIdleRunnable); // We are supposed to be in a non-idle state, so the runnable should not be posted yet. verify(mMainHandler, never()).sendMessage(any()); mPipTransitionState.setIsPipBoundsChangingWithDisplay(false); verify(mMainHandler, times(1)) .sendMessage(argThat(msg -> msg.getCallback() == onIdleRunnable)); } @Test public void testPostState_noImmediateStateChange_postedOnHandler() { mPipTransitionState.setState(PipTransitionState.UNDEFINED); Loading