Merge "New BroadcastReceiverFlow" into main
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 9d71801..838cb3b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -39,13 +39,13 @@
 
 @VisibleForTesting
 class SceneGestureHandler(
-    private val layoutImpl: SceneTransitionLayoutImpl,
+    internal val layoutImpl: SceneTransitionLayoutImpl,
     internal val orientation: Orientation,
     private val coroutineScope: CoroutineScope,
 ) {
     val draggable: DraggableHandler = SceneDraggableHandler(this)
 
-    private var transitionState
+    internal var transitionState
         get() = layoutImpl.state.transitionState
         set(value) {
             layoutImpl.state.transitionState = value
@@ -58,7 +58,7 @@
      * Note: the initialScene here does not matter, it's only used for initializing the transition
      * and will be replaced when a drag event starts.
      */
-    private val swipeTransition = SwipeTransition(initialScene = currentScene)
+    internal val swipeTransition = SwipeTransition(initialScene = currentScene)
 
     internal val currentScene: Scene
         get() = layoutImpl.scene(transitionState.currentScene)
@@ -415,7 +415,7 @@
         }
     }
 
-    private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+    internal class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
         var _currentScene by mutableStateOf(initialScene)
         override val currentScene: SceneKey
             get() = _currentScene.key
@@ -598,9 +598,29 @@
         return PriorityNestedScrollConnection(
             canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
                 canChangeScene = offsetBeforeStart == Offset.Zero
-                gestureHandler.isDrivingTransition &&
+
+                val canInterceptSwipeTransition =
                     canChangeScene &&
-                    offsetAvailable.toAmount() != 0f
+                        gestureHandler.isDrivingTransition &&
+                        offsetAvailable.toAmount() != 0f
+                if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
+
+                val progress = gestureHandler.swipeTransition.progress
+                val threshold = gestureHandler.layoutImpl.transitionInterceptionThreshold
+                fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
+
+                // The transition is always between 0 and 1. If it is close to either of these
+                // intervals, we want to go directly to the TransitionState.Idle.
+                // The progress value can go beyond this range in the case of overscroll.
+                val shouldSnapToIdle = isProgressCloseTo(0f) || isProgressCloseTo(1f)
+                if (shouldSnapToIdle) {
+                    gestureHandler.swipeTransition.stopOffsetAnimation()
+                    gestureHandler.transitionState =
+                        TransitionState.Idle(gestureHandler.swipeTransition.currentScene)
+                }
+
+                // Start only if we cannot consume this event
+                !shouldSnapToIdle
             },
             canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
                 val amount = offsetAvailable.toAmount()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index efdfe7a..9c31445 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import androidx.annotation.FloatRange
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
@@ -41,6 +42,8 @@
  * @param transitions the definition of the transitions used to animate a change of scene.
  * @param state the observable state of this layout.
  * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
+ * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
+ *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
  * @param scenes the configuration of the different scenes of this layout.
  */
 @Composable
@@ -51,6 +54,7 @@
     modifier: Modifier = Modifier,
     state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
     edgeDetector: EdgeDetector = DefaultEdgeDetector,
+    @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
     val density = LocalDensity.current
@@ -63,6 +67,7 @@
             state = state,
             density = density,
             edgeDetector = edgeDetector,
+            transitionInterceptionThreshold = transitionInterceptionThreshold,
             coroutineScope = coroutineScope,
         )
     }
@@ -71,6 +76,7 @@
     layoutImpl.transitions = transitions
     layoutImpl.density = density
     layoutImpl.edgeDetector = edgeDetector
+    layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
 
     layoutImpl.setScenes(scenes)
     layoutImpl.setCurrentScene(currentScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 0b06953..94f2737 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -50,6 +50,7 @@
     internal val state: SceneTransitionLayoutState,
     density: Density,
     edgeDetector: EdgeDetector,
+    transitionInterceptionThreshold: Float,
     coroutineScope: CoroutineScope,
 ) {
     internal val scenes = SnapshotStateMap<SceneKey, Scene>()
@@ -62,6 +63,7 @@
     internal var transitions by mutableStateOf(transitions)
     internal var density: Density by mutableStateOf(density)
     internal var edgeDetector by mutableStateOf(edgeDetector)
+    internal var transitionInterceptionThreshold by mutableStateOf(transitionInterceptionThreshold)
 
     private val horizontalGestureHandler: SceneGestureHandler
     private val verticalGestureHandler: SceneGestureHandler
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 7ab2096..49ef31b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -69,6 +69,8 @@
             scene(SceneC) { Text("SceneC") }
         }
 
+        val transitionInterceptionThreshold = 0.05f
+
         val sceneGestureHandler =
             SceneGestureHandler(
                 layoutImpl =
@@ -79,6 +81,7 @@
                             state = layoutState,
                             density = Density(1f),
                             edgeDetector = DefaultEdgeDetector,
+                            transitionInterceptionThreshold = transitionInterceptionThreshold,
                             coroutineScope = coroutineScope,
                         )
                         .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
@@ -107,6 +110,9 @@
         val transitionState: TransitionState
             get() = layoutState.transitionState
 
+        val progress: Float
+            get() = (transitionState as Transition).progress
+
         fun advanceUntilIdle() {
             coroutineScope.testScheduler.advanceUntilIdle()
         }
@@ -145,13 +151,12 @@
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
         draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
-        val transition = transitionState as Transition
 
         draggable.onDelta(pixels = deltaInPixels10)
-        assertThat(transition.progress).isEqualTo(0.1f)
+        assertThat(progress).isEqualTo(0.1f)
 
         draggable.onDelta(pixels = deltaInPixels10)
-        assertThat(transition.progress).isEqualTo(0.2f)
+        assertThat(progress).isEqualTo(0.2f)
     }
 
     @Test
@@ -257,8 +262,7 @@
             )
 
         assertScene(currentScene = SceneA, isIdle = false)
-        val transition = transitionState as Transition
-        assertThat(transition.progress).isEqualTo(0.1f)
+        assertThat(progress).isEqualTo(0.1f)
         assertThat(consumed).isEqualTo(offsetY10)
     }
 
@@ -282,13 +286,12 @@
         nestedScroll.scroll(available = offsetY10)
         assertScene(currentScene = SceneA, isIdle = false)
 
-        val transition = transitionState as Transition
-        assertThat(transition.progress).isEqualTo(0.1f)
+        assertThat(progress).isEqualTo(0.1f)
 
         // start intercept preScroll
         val consumed =
             nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
-        assertThat(transition.progress).isEqualTo(0.2f)
+        assertThat(progress).isEqualTo(0.2f)
 
         // do nothing on postScroll
         nestedScroll.onPostScroll(
@@ -296,13 +299,71 @@
             available = Offset.Zero,
             source = NestedScrollSource.Drag
         )
-        assertThat(transition.progress).isEqualTo(0.2f)
+        assertThat(progress).isEqualTo(0.2f)
 
         nestedScroll.scroll(available = offsetY10)
-        assertThat(transition.progress).isEqualTo(0.3f)
+        assertThat(progress).isEqualTo(0.3f)
         assertScene(currentScene = SceneA, isIdle = false)
     }
 
+    private suspend fun TestGestureScope.preScrollAfterSceneTransition(
+        firstScroll: Float,
+        secondScroll: Float
+    ) {
+        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
+        // start scene transition
+        nestedScroll.scroll(available = Offset(0f, SCREEN_SIZE * firstScroll))
+
+        // stop scene transition (start the "stop animation")
+        nestedScroll.onPreFling(available = Velocity.Zero)
+
+        // a pre scroll event, that could be intercepted by SceneGestureHandler
+        nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
+    }
+
+    // Float tolerance for comparisons
+    private val tolerance = 0.00001f
+
+    @Test
+    fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
+        val first = transitionInterceptionThreshold - tolerance
+        val second = 0.01f
+
+        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+        assertScene(SceneA, isIdle = true)
+    }
+
+    @Test
+    fun scrollAndFling_scrollMinInterceptable_interceptPreScrollEvents() = runGestureTest {
+        val first = transitionInterceptionThreshold + tolerance
+        val second = 0.01f
+
+        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+        assertThat(progress).isWithin(tolerance).of(first + second)
+    }
+
+    @Test
+    fun scrollAndFling_scrollMaxInterceptable_interceptPreScrollEvents() = runGestureTest {
+        val first = 1f - transitionInterceptionThreshold - tolerance
+        val second = 0.01f
+
+        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+        assertThat(progress).isWithin(tolerance).of(first + second)
+    }
+
+    @Test
+    fun scrollAndFling_scrollMoreThanInterceptable_goToIdleOnNextScene() = runGestureTest {
+        val first = 1f - transitionInterceptionThreshold + tolerance
+        val second = 0.01f
+
+        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+
+        assertScene(SceneC, isIdle = true)
+    }
+
     @Test
     fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
@@ -444,24 +505,23 @@
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
         draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
-        val transition = transitionState as Transition
 
         draggable.onDelta(deltaInPixels10)
-        assertThat(transition.progress).isEqualTo(0.1f)
+        assertThat(progress).isEqualTo(0.1f)
 
         // now we can intercept the scroll events
         nestedScroll.scroll(available = offsetY10)
-        assertThat(transition.progress).isEqualTo(0.2f)
+        assertThat(progress).isEqualTo(0.2f)
 
         // this should be ignored, we are scrolling now!
         draggable.onDragStopped(velocityThreshold)
         assertScene(currentScene = SceneA, isIdle = false)
 
         nestedScroll.scroll(available = offsetY10)
-        assertThat(transition.progress).isEqualTo(0.3f)
+        assertThat(progress).isEqualTo(0.3f)
 
         nestedScroll.scroll(available = offsetY10)
-        assertThat(transition.progress).isEqualTo(0.4f)
+        assertThat(progress).isEqualTo(0.4f)
 
         nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
         assertScene(currentScene = SceneC, isIdle = false)
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index d3eedd7..1687157 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -191,9 +191,16 @@
         launchDeviceDiscovery();
         startQueuedActions();
         if (!mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) {
-            mService.sendCecCommand(
-                    HdmiCecMessageBuilder.buildRequestActiveSource(
-                            getDeviceInfo().getLogicalAddress()));
+            addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
+                @Override
+                public void onComplete(int result) {
+                    if (result != HdmiControlManager.RESULT_SUCCESS) {
+                        mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(
+                                getDeviceInfo().getLogicalAddress(),
+                                getDeviceInfo().getPhysicalAddress()));
+                    }
+                }
+            }));
         }
     }
 
@@ -1325,6 +1332,8 @@
         removeAction(TimerRecordingAction.class);
         removeAction(NewDeviceAction.class);
         removeAction(AbsoluteVolumeAudioStatusAction.class);
+        // Remove pending actions.
+        removeAction(RequestActiveSourceAction.class);
 
         // Keep SAM enabled if eARC is enabled, unless we're going to Standby.
         if (initiatedByCec || !mService.isEarcEnabled()){
diff --git a/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
new file mode 100644
index 0000000..017c86d
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.server.hdmi;
+
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
+import android.util.Slog;
+
+/**
+ * Feature action that sends <Request Active Source> message and waits for <Active Source>.
+ */
+public class RequestActiveSourceAction extends HdmiCecFeatureAction {
+    private static final String TAG = "RequestActiveSourceAction";
+
+    // State to wait for the <Active Source> message.
+    private static final int STATE_WAIT_FOR_ACTIVE_SOURCE = 1;
+
+    RequestActiveSourceAction(HdmiCecLocalDevice source, IHdmiControlCallback callback) {
+        super(source, callback);
+    }
+
+    @Override
+    boolean start() {
+        Slog.v(TAG, "RequestActiveSourceAction started.");
+
+        sendCommand(HdmiCecMessageBuilder.buildRequestActiveSource(getSourceAddress()));
+
+        mState = STATE_WAIT_FOR_ACTIVE_SOURCE;
+        addTimer(mState, HdmiConfig.TIMEOUT_MS);
+        return true;
+    }
+
+    @Override
+    boolean processCommand(HdmiCecMessage cmd) {
+        // The action finishes successfully if the <Active Source> message is received.
+        // {@link HdmiCecLocalDevice#onMessage} handles this message, so false is returned.
+        if (cmd.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
+            finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
+        }
+        return false;
+    }
+
+    @Override
+    void handleTimerEvent(int state) {
+        if (mState != state) {
+            return;
+        }
+        if (mState == STATE_WAIT_FOR_ACTIVE_SOURCE) {
+            finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index c632727f..9e5bea7 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -1680,4 +1680,47 @@
         assertThat(mHdmiControlService.isSystemAudioActivated()).isTrue();
 
     }
+
+    @Test
+    public void onAddressAllocated_startRequestActiveSourceAction_playbackActiveSource() {
+        HdmiCecMessage requestActiveSource =
+                HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV);
+        HdmiCecMessage activeSourceFromPlayback =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_PLAYBACK_1, 0x1000);
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+        mHdmiControlService.getHdmiCecNetwork().clearLocalDevices();
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
+        mNativeWrapper.clearResultMessages();
+        mNativeWrapper.onCecMessage(activeSourceFromPlayback);
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv);
+    }
+
+    @Test
+    public void onAddressAllocated_startRequestActiveSourceAction_noActiveSource() {
+        HdmiCecMessage requestActiveSource =
+                HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV);
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+        mHdmiControlService.getHdmiCecNetwork().clearLocalDevices();
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
index 4a33594..6655932 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -31,7 +31,9 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -143,6 +145,15 @@
         secureWindow.mAttrs.flags |= FLAG_SECURE;
         assertEquals(SNAPSHOT_MODE_APP_THEME,
                 mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
+
+        // Verifies that if the snapshot can be cached, then getSnapshotMode should be respected.
+        // Otherwise a real snapshot can be taken even if the activity disables recents screenshot.
+        spyOn(mWm.mTaskSnapshotController);
+        final int disabledInRecentsTaskId = disabledWindow.getTask().mTaskId;
+        mAtm.takeTaskSnapshot(disabledInRecentsTaskId, true /* updateCache */);
+        verify(mWm.mTaskSnapshotController, never()).prepareTaskSnapshot(any(), any());
+        mAtm.takeTaskSnapshot(disabledInRecentsTaskId, false /* updateCache */);
+        verify(mWm.mTaskSnapshotController).prepareTaskSnapshot(any(), any());
     }
 
     @Test