Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d937c27 authored by Ikram Gabiyev's avatar Ikram Gabiyev
Browse files

[PiP2] Track display change transitions in PiP

Use a transition observer to track display changing transitions
and update the PiP state if such transition is also changing PiP.

Also make sure PipTransitionState::isPipStateIdle() -> false
while the display change transition involving a PiP change is ongoing.
This should delay any KCA updates or other PiP bounds change transitions
from being scheduled until display change transition is finished and
until the PipBoundsState has been properly updated.

Bug: 382151065
Flag: com.android.wm.shell.enable_pip2
Test: atest PipDisplayChangeObserverTest
Test: atest PipTransitionStateTest
Test: repeatedly fold/unfold while in PiP
Change-Id: Idaf15bd79f18f30e5152bcd3416101dd5522d99c
parent 855e2029
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -464,6 +464,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.
+5 −0
Original line number Diff line number Diff line
@@ -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;
@@ -142,6 +143,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;

@@ -188,12 +190,15 @@ public class PipTransition extends PipTransitionController implements
                pipBoundsState, pipBoundsAlgorithm,
                pipTransitionState, pipDisplayLayoutState, pipDesktopState, pipInteractionHandler,
                splitScreenControllerOptional);
        mPipDisplayChangeObserver = new PipDisplayChangeObserver(pipTransitionState,
                pipBoundsState);
    }

    @Override
    protected void onInit() {
        if (PipUtils.isPip2ExperimentEnabled()) {
            mTransitions.addHandler(this);
            mTransitions.registerObserver(mPipDisplayChangeObserver);
        }
    }

+30 −3
Original line number Diff line number Diff line
@@ -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.
     */
@@ -374,6 +376,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.
     */
@@ -439,13 +460,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. */
@@ -453,5 +477,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);
    }
}
+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);
    }
}
+21 −0
Original line number Diff line number Diff line
@@ -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