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

Commit 9cee65ca authored by Ikram Gabiyev's avatar Ikram Gabiyev Committed by Android (Google) Code Review
Browse files

Merge "Allow other PiP changes in remote-and-PiP case" into main

parents 32a0da13 d829a8b1
Loading
Loading
Loading
Loading
+51 −27
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.wm.shell.transition;

import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_TO_BACK;

@@ -233,27 +234,11 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
            splitHandler.dismissSplitInBackground(EXIT_REASON_FULLSCREEN_REQUEST);
        }

        TransitionInfo.Change pipChange = null;
        final TransitionInfo pipInfo = subCopy(info, TRANSIT_PIP, false /* withChanges */);
        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
            TransitionInfo.Change change = info.getChanges().get(i);
            if (mPipHandler.isEnteringPip(change, info.getType())) {
                if (pipChange != null) {
                    throw new IllegalStateException("More than 1 pip-entering changes in one"
                            + " transition? " + info);
                }
                pipChange = change;
                info.getChanges().remove(i);
                pipInfo.addChange(pipChange);
            } else if (change.getTaskInfo() == null && change.getParent() != null
                    && pipChange != null && change.getParent().equals(pipChange.getContainer())) {
                // Cache the PiP activity if it's a target and cached pip task change is its parent;
                // note that we are bottom-to-top, so if such activity has a task
                // that is also a target, then it must have been cached already as pipChange.
                TransitionInfo.Change pipActivityChange = info.getChanges().remove(i);
                pipInfo.getChanges().addFirst(pipActivityChange);
            }
        }
        final TransitionInfo pipInfo = removePipChangesFrom(info);
        final boolean hasPipChange = !pipInfo.getChanges().isEmpty();
        final TransitionInfo.Change enterPipChange = pipInfo.getChanges().stream().filter(change ->
                mPipHandler.isEnteringPip(change, info.getType())).findFirst().orElse(null);

        TransitionInfo.Change desktopChange = null;
        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
            TransitionInfo.Change change = info.getChanges().get(i);
@@ -273,8 +258,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
            if (mInFlightSubAnimations > 0) return;
            finishCallback.onTransitionFinished(mFinishWCT);
        };
        if ((pipChange == null && desktopChange == null)
                || (pipChange != null && desktopChange != null)) {
        if ((!hasPipChange && desktopChange == null)
                || (hasPipChange && desktopChange != null)) {
            // Don't split the transition. Let the leftovers handler handle it all.
            // TODO: b/? - split the transition into three pieces when there's both a PIP and a
            //  desktop change are present. For example, during remote intent open over a desktop
@@ -287,7 +272,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
                }
            }
            return false;
        } else if (pipChange != null && desktopChange == null) {
        } else if (hasPipChange && desktopChange == null) {
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
                            + " animation because remote-animation likely doesn't support it #%d",
                    info.getDebugId());
@@ -299,8 +284,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
            if (PipFlags.isPip2ExperimentEnabled()) {
                mPipHandler.startAnimation(mTransition, pipInfo, startTransaction,
                        finishTransaction, finishCB);
            } else {
                mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction,
            } else if (enterPipChange != null) {
                mPipHandler.startEnterAnimation(enterPipChange, otherStartT, finishTransaction,
                        finishCB);
            }

@@ -314,7 +299,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
                    mTransition, info, startTransaction, finishTransaction, finishCB,
                    mMixedHandler);
            return true;
        } else if (pipChange == null && desktopChange != null) {
        } else if (!hasPipChange && desktopChange != null) {
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a"
                            + "separate animation because remote-animation likely doesn't support"
                            + "it #%d", info.getDebugId());
@@ -340,6 +325,45 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
        }
    }

    @NonNull
    private TransitionInfo removePipChangesFrom(@NonNull TransitionInfo outInfo) {
        final TransitionInfo pipInfo = subCopy(outInfo,
                // In PiP2, sub-flight PiP transition doesn't have to be entering PiP.
                PipFlags.isPip2ExperimentEnabled() ? outInfo.getType() : TRANSIT_PIP,
                false /* withChanges */);
        // Cache enter PiP change separately to find config-at-end activity change if present.
        TransitionInfo.Change enterPipChange = null;

        for (int i = outInfo.getChanges().size() - 1; i >= 0; --i) {
            TransitionInfo.Change change = outInfo.getChanges().get(i);

            if (mPipHandler.isEnteringPip(change, outInfo.getType())) {
                if (enterPipChange != null) {
                    throw new IllegalStateException("More than 1 enter-pip changes in one"
                            + " transition? " + outInfo);
                }
                enterPipChange = change;
                outInfo.getChanges().remove(i);
                pipInfo.getChanges().addFirst(enterPipChange);
            } else if (PipFlags.isPip2ExperimentEnabled() && change.getTaskInfo() != null
                    && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
                // Sometimes a PiP change that isn't an entering change could be collected into
                // a different transition.
                outInfo.getChanges().remove(i);
                pipInfo.addChange(change);
            } else if (change.getTaskInfo() == null && enterPipChange != null
                    && change.getParent() != null
                    && change.getParent().equals(enterPipChange.getContainer())) {
                // Cache the PiP activity if it's a target and cached pip task change is its parent;
                // note that we are bottom-to-top, so if such activity has a task
                // that is also a target, then it must have been cached already as pipChange.
                TransitionInfo.Change pipActivityChange = outInfo.getChanges().remove(i);
                pipInfo.getChanges().addFirst(pipActivityChange);
            }
        }
        return pipInfo;
    }

    static boolean animateEnterBubbles(
            @NonNull IBinder transition,
            @NonNull TransitionInfo info,
+183 −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.transition

import android.app.ActivityManager
import android.app.WindowConfiguration
import android.os.IBinder
import android.platform.test.annotations.EnableFlags
import android.view.SurfaceControl
import android.view.WindowManager
import android.window.TransitionInfo
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.wm.shell.Flags
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.activityembedding.ActivityEmbeddingController
import com.android.wm.shell.bubbles.BubbleTransitions
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.keyguard.KeyguardTransitionHandler
import com.android.wm.shell.pip.PipTransitionController
import com.android.wm.shell.splitscreen.StageCoordinator
import com.android.wm.shell.unfold.UnfoldTransitionHandler
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

/**
 * Tests for [DefaultMixedTransition]
 * Build & Run: atest WMShellUnitTests:DefaultMixedTransitionTest
 */
@SmallTest
class DefaultMixedTransitionTest : ShellTestCase() {
    private val mPlayer = mock<Transitions>()
    private val mMixedHandler = mock<MixedTransitionHandler>()
    private val mPipHandler = mock<PipTransitionController>()
    private val mSplitHandler = mock<StageCoordinator>()
    private val mKeyguardHandler = mock<KeyguardTransitionHandler>()
    private val mUnfoldHandler = mock<UnfoldTransitionHandler>()
    private val mActivityEmbeddingController = mock<ActivityEmbeddingController>()
    private val mDesktopTasksController = mock<DesktopTasksController>()
    private val mBubbleTransitions = mock<BubbleTransitions>()
    private val mMockTransition = mock<IBinder>()

    // Mocks for startAnimation arguments, initialized inline
    private val mStartT = mock<SurfaceControl.Transaction>()
    private val mFinishT = mock<SurfaceControl.Transaction>()
    private val mFinishCallback = mock<Transitions.TransitionFinishCallback>()
    private val mockPipToken = mock<WindowContainerToken>()
    private val mockPipLeash = mock<SurfaceControl>()

    // Other mocks needed for the test logic, initialized inline
    private val mRemoteTransitionHandler = mock<RemoteTransitionHandler>()
    private val mRemoteChange = mock<TransitionInfo.Change>()

    @Before
    fun setUp() {
        whenever(mPlayer.remoteTransitionHandler).thenReturn(mRemoteTransitionHandler)
    }

    @EnableFlags(Flags.FLAG_ENABLE_PIP2)
    @Test
    fun remoteAndPipOrDesktop_remoteAndEnterPip_sendPipChangeToPipHandler() {
        // Mock dependencies to prevent side-effects and isolate logic for PiP and remote only.
        whenever(mSplitHandler.transitionImpliesSplitToFullscreen(any())).thenReturn(false)
        whenever(mDesktopTasksController.isDesktopChange(any(), any())).thenReturn(false)
        whenever(mPlayer.dispatchTransition(any(), any(), any(), any(), any(), any()))
            .thenReturn(mRemoteTransitionHandler)

        val defaultMixedTransition = getDefaultMixedTransition(
            DefaultMixedHandler.MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE)

        val info = createRemoteTransitionInfo()
        val pipChange = createPipChange()
        whenever(mPipHandler.isEnteringPip(eq(pipChange), eq(info.type))).thenReturn(true)
        info.addChange(pipChange)

        val handled = defaultMixedTransition.startAnimation(
            mMockTransition, info, mStartT, mFinishT, mFinishCallback)

        // Verify that animation was resolved.
        assertThat(handled).isTrue()

        // Check that PiP handler was called into for the animation with PiP change in its info.
        val infoCaptor = argumentCaptor<TransitionInfo>()
        verify(mPipHandler, times(1)).startAnimation(eq(mMockTransition),
            infoCaptor.capture(), any(), any(), any())

        // Assert that TransitionInfo was separated to contain PiP change only
        // and that transition type is carried over.
        val capturedInfo = infoCaptor.firstValue
        assertThat(capturedInfo.changes).containsExactly(pipChange)
        assertThat(capturedInfo.type).isEqualTo(info.type)
    }

    @EnableFlags(Flags.FLAG_ENABLE_PIP2)
    @Test
    fun remoteAndPipOrDesktop_remoteAndNonEnterPipChange_sendPipChangeToPipHandler() {
        // Mock dependencies to prevent side-effects and isolate logic for PiP and remote only.
        whenever(mSplitHandler.transitionImpliesSplitToFullscreen(any())).thenReturn(false)
        whenever(mDesktopTasksController.isDesktopChange(any(), any())).thenReturn(false)
        whenever(mPlayer.dispatchTransition(any(), any(), any(), any(), any(), any()))
            .thenReturn(mRemoteTransitionHandler)

        val defaultMixedTransition = getDefaultMixedTransition(
            DefaultMixedHandler.MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE)

        val info = createRemoteTransitionInfo()
        // Add a PiP change into the transition that is NOT entering PiP.
        val pipChange = createPipChange()
        whenever(mPipHandler.isEnteringPip(eq(pipChange), eq(info.type))).thenReturn(false)
        info.addChange(pipChange)

        val handled = defaultMixedTransition.startAnimation(
            mMockTransition, info, mStartT, mFinishT, mFinishCallback)

        // Verify that animation was resolved.
        assertThat(handled).isTrue()

        // Check that PiP handler was called into for the animation with PiP change in its info.
        val infoCaptor = argumentCaptor<TransitionInfo>()
        verify(mPipHandler, times(1)).startAnimation(eq(mMockTransition),
            infoCaptor.capture(), any(), any(), any())

        // Assert that TransitionInfo was separated to contain PiP change only
        // and that transition type is carried over; this should be done even with non-enter PiP
        // changes.
        val capturedInfo = infoCaptor.firstValue
        assertThat(capturedInfo.changes).containsExactly(pipChange)
        assertThat(capturedInfo.type).isEqualTo(info.type)
    }

    private fun createRemoteTransitionInfo(): TransitionInfo {
        val info = TransitionInfo(WindowManager.TRANSIT_TO_FRONT, 0 /* flags */)
        info.addChange(mRemoteChange)
        return info
    }

    private fun createPipChange(): TransitionInfo.Change {
        val pipChange = TransitionInfo.Change(mockPipToken, mockPipLeash)
        val pipTaskInfo = ActivityManager.RunningTaskInfo()
        pipTaskInfo.configuration.windowConfiguration.windowingMode =
            WindowConfiguration.WINDOWING_MODE_PINNED
        pipChange.taskInfo = pipTaskInfo
        return pipChange
    }

    private fun getDefaultMixedTransition(type: Int): DefaultMixedTransition {
        return DefaultMixedTransition(
            type,
            mMockTransition,
            mPlayer,
            mMixedHandler,
            mPipHandler,
            mSplitHandler,
            mKeyguardHandler,
            mUnfoldHandler,
            mActivityEmbeddingController,
            mDesktopTasksController,
            mBubbleTransitions
        )
    }
}
 No newline at end of file