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