Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +16 −4 Original line number Diff line number Diff line Loading @@ -25,9 +25,10 @@ import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.posturing.domain.interactor.advanceTimeBySlidingWindowAndRun import com.android.systemui.dock.DockManager import com.android.systemui.dock.fakeDockManager import com.android.systemui.kosmos.Kosmos Loading Loading @@ -127,7 +128,12 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.setPosturedState(PosturedState.NotPostured) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.NotPostured(confidence = 1f), ) ) assertThat(shouldAutoOpen).isFalse() assertThat(suppressionReason) Loading @@ -135,7 +141,13 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) ) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(shouldAutoOpen).isTrue() assertThat(suppressionReason).isNull() } Loading @@ -153,7 +165,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState(PositionState()) fakeDockManager.setIsDocked(true) assertThat(shouldAutoOpen).isFalse() Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +216 −2 Original line number Diff line number Diff line Loading @@ -16,22 +16,39 @@ package com.android.systemui.communal.posturing.domain.interactor import android.hardware.Sensor import android.hardware.TriggerEventListener import android.platform.test.annotations.EnableFlags import android.service.dreams.Flags.FLAG_ALLOW_DREAM_WHEN_POSTURED import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.advanceTimeBy import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos import com.android.systemui.util.sensors.asyncSensorManager import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.stub @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED) class PosturingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() Loading @@ -44,8 +61,166 @@ class PosturingInteractorTest : SysuiTestCase() { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() } @Test fun testLowConfidenceOrientation() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isFalse() } @Test fun testLowConfidenceStationary() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isFalse() } @Test fun testSlidingWindow() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) assertThat(postured).isFalse() advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() // The 0.2 confidence will have fallen out of the sliding window, and we should now flip // to true. assertThat(postured).isTrue() advanceTimeBy(9999.hours) // We should remain postured if no other updates are received. assertThat(postured).isTrue() } @Test fun testLiftGesture_afterSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured. triggerSensor(sensor) assertThat(postured).isFalse() advanceTimeBy(9999.hours) assertThat(postured).isFalse() } @Test fun testLiftGesture_overridesSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() // Add multiple stationary + postured events to the sliding window. repeat(100) { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) } assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured immediately. triggerSensor(sensor) assertThat(postured).isFalse() } @Test fun testSignificantMotion_afterSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() // If we detect motion, we should transition back to not postured. triggerSensor(sensor) assertThat(postured).isFalse() advanceTimeBy(9999.hours) assertThat(postured).isFalse() } @Test Loading @@ -55,12 +230,51 @@ class PosturingInteractorTest : SysuiTestCase() { assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.NotPostured) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) // Repository value is overridden by debug value assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.Unknown) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() } private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit { val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>() val pickupSensor = mock<Sensor>() val motionSensor = mock<Sensor>() asyncSensorManager.stub { on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor on { getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION) } doReturn motionSensor on { requestTriggerSensor(any(), any()) } doAnswer { val callback = it.arguments[0] as TriggerEventListener val sensor = it.arguments[1] as Sensor callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } + callback true } on { cancelTriggerSensor(any(), any()) } doAnswer { val callback = it.arguments[0] as TriggerEventListener val sensor = it.arguments[1] as Sensor callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } - callback true } } return { sensor: Sensor -> val list = callbacks.getOrElse(sensor) { emptyList() } // Simulate a trigger sensor which unregisters callbacks after triggering. callbacks[sensor] = emptyList() list.forEach { it.onTrigger(mock()) } } } } packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +28 −4 Original line number Diff line number Diff line Loading @@ -21,8 +21,11 @@ import android.app.DreamManager import android.service.dreams.Flags.allowDreamWhenPostured import com.android.app.tracing.coroutines.launchInTraced import com.android.systemui.CoreStartable import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.dagger.CommunalTableLog Loading @@ -30,10 +33,14 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @SysUISingleton Loading @@ -42,19 +49,36 @@ class DevicePosturingListener constructor( private val commandRegistry: CommandRegistry, private val dreamManager: DreamManager, private val interactor: PosturingInteractor, private val posturingInteractor: PosturingInteractor, communalSettingsInteractor: CommunalSettingsInteractor, batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, ) : CoreStartable { private val command = DevicePosturingCommand() // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp // if posturing signal is not needed. private val postured = allOf( batteryInteractor.isDevicePluggedIn, communalSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED }, ) .flatMapLatestConflated { shouldListen -> if (shouldListen) { posturingInteractor.postured } else { flowOf(false) } } @SuppressLint("MissingPermission") override fun start() { if (!allowDreamWhenPostured()) { return } interactor.postured postured .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, Loading @@ -78,7 +102,7 @@ constructor( val state = when (arg.lowercase()) { "true" -> PosturedState.Postured(confidence = 1f) "true" -> PosturedState.Postured "false" -> PosturedState.NotPostured "clear" -> PosturedState.Unknown else -> { Loading @@ -87,7 +111,7 @@ constructor( null } } state?.let { interactor.setValueForDebug(it) } state?.let { posturingInteractor.setValueForDebug(it) } } override fun help(pw: PrintWriter) { Loading packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt +9 −1 Original line number Diff line number Diff line Loading @@ -57,7 +57,15 @@ constructor( allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked()) } WhenToStartHub.WHILE_CHARGING_AND_POSTURED -> { allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured) // Only listen to posturing if applicable to avoid running the posturing // CHRE nanoapp when not needed. batteryInteractor.isDevicePluggedIn.flatMapLatestConflated { isCharging -> if (isCharging) { posturingInteractor.postured } else { flowOf(false) } } } WhenToStartHub.NEVER -> flowOf(false) } Loading packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt 0 → 100644 +48 −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.systemui.communal.posturing.data.model import androidx.annotation.FloatRange data class PositionState( val stationary: StationaryState = StationaryState.Unknown, val orientation: OrientationState = OrientationState.Unknown, ) { sealed interface StationaryState { @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float data object Unknown : StationaryState { override val confidence: Float = 0f } data class Stationary(override val confidence: Float) : StationaryState data class NotStationary(override val confidence: Float) : StationaryState } sealed interface OrientationState { @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float data object Unknown : OrientationState { override val confidence: Float = 0f } data class Postured(override val confidence: Float) : OrientationState data class NotPostured(override val confidence: Float) : OrientationState } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +16 −4 Original line number Diff line number Diff line Loading @@ -25,9 +25,10 @@ import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.posturing.domain.interactor.advanceTimeBySlidingWindowAndRun import com.android.systemui.dock.DockManager import com.android.systemui.dock.fakeDockManager import com.android.systemui.kosmos.Kosmos Loading Loading @@ -127,7 +128,12 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.setPosturedState(PosturedState.NotPostured) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.NotPostured(confidence = 1f), ) ) assertThat(shouldAutoOpen).isFalse() assertThat(suppressionReason) Loading @@ -135,7 +141,13 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) ) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(shouldAutoOpen).isTrue() assertThat(suppressionReason).isNull() } Loading @@ -153,7 +165,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState(PositionState()) fakeDockManager.setIsDocked(true) assertThat(shouldAutoOpen).isFalse() Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +216 −2 Original line number Diff line number Diff line Loading @@ -16,22 +16,39 @@ package com.android.systemui.communal.posturing.domain.interactor import android.hardware.Sensor import android.hardware.TriggerEventListener import android.platform.test.annotations.EnableFlags import android.service.dreams.Flags.FLAG_ALLOW_DREAM_WHEN_POSTURED import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.advanceTimeBy import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos import com.android.systemui.util.sensors.asyncSensorManager import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.stub @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED) class PosturingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() Loading @@ -44,8 +61,166 @@ class PosturingInteractorTest : SysuiTestCase() { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() } @Test fun testLowConfidenceOrientation() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isFalse() } @Test fun testLowConfidenceStationary() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isFalse() } @Test fun testSlidingWindow() = kosmos.runTest { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), ) ) advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) assertThat(postured).isFalse() advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() // The 0.2 confidence will have fallen out of the sliding window, and we should now flip // to true. assertThat(postured).isTrue() advanceTimeBy(9999.hours) // We should remain postured if no other updates are received. assertThat(postured).isTrue() } @Test fun testLiftGesture_afterSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured. triggerSensor(sensor) assertThat(postured).isFalse() advanceTimeBy(9999.hours) assertThat(postured).isFalse() } @Test fun testLiftGesture_overridesSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() // Add multiple stationary + postured events to the sliding window. repeat(100) { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) } assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured immediately. triggerSensor(sensor) assertThat(postured).isFalse() } @Test fun testSignificantMotion_afterSlidingWindow() = kosmos.runTest { val triggerSensor = stubSensorManager() val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)!! val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() // If we detect motion, we should transition back to not postured. triggerSensor(sensor) assertThat(postured).isFalse() advanceTimeBy(9999.hours) assertThat(postured).isFalse() } @Test Loading @@ -55,12 +230,51 @@ class PosturingInteractorTest : SysuiTestCase() { assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.NotPostured) posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), ) ) // Repository value is overridden by debug value assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.Unknown) advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() } private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit { val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>() val pickupSensor = mock<Sensor>() val motionSensor = mock<Sensor>() asyncSensorManager.stub { on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor on { getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION) } doReturn motionSensor on { requestTriggerSensor(any(), any()) } doAnswer { val callback = it.arguments[0] as TriggerEventListener val sensor = it.arguments[1] as Sensor callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } + callback true } on { cancelTriggerSensor(any(), any()) } doAnswer { val callback = it.arguments[0] as TriggerEventListener val sensor = it.arguments[1] as Sensor callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } - callback true } } return { sensor: Sensor -> val list = callbacks.getOrElse(sensor) { emptyList() } // Simulate a trigger sensor which unregisters callbacks after triggering. callbacks[sensor] = emptyList() list.forEach { it.onTrigger(mock()) } } } }
packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +28 −4 Original line number Diff line number Diff line Loading @@ -21,8 +21,11 @@ import android.app.DreamManager import android.service.dreams.Flags.allowDreamWhenPostured import com.android.app.tracing.coroutines.launchInTraced import com.android.systemui.CoreStartable import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.dagger.CommunalTableLog Loading @@ -30,10 +33,14 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @SysUISingleton Loading @@ -42,19 +49,36 @@ class DevicePosturingListener constructor( private val commandRegistry: CommandRegistry, private val dreamManager: DreamManager, private val interactor: PosturingInteractor, private val posturingInteractor: PosturingInteractor, communalSettingsInteractor: CommunalSettingsInteractor, batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, ) : CoreStartable { private val command = DevicePosturingCommand() // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp // if posturing signal is not needed. private val postured = allOf( batteryInteractor.isDevicePluggedIn, communalSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED }, ) .flatMapLatestConflated { shouldListen -> if (shouldListen) { posturingInteractor.postured } else { flowOf(false) } } @SuppressLint("MissingPermission") override fun start() { if (!allowDreamWhenPostured()) { return } interactor.postured postured .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, Loading @@ -78,7 +102,7 @@ constructor( val state = when (arg.lowercase()) { "true" -> PosturedState.Postured(confidence = 1f) "true" -> PosturedState.Postured "false" -> PosturedState.NotPostured "clear" -> PosturedState.Unknown else -> { Loading @@ -87,7 +111,7 @@ constructor( null } } state?.let { interactor.setValueForDebug(it) } state?.let { posturingInteractor.setValueForDebug(it) } } override fun help(pw: PrintWriter) { Loading
packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt +9 −1 Original line number Diff line number Diff line Loading @@ -57,7 +57,15 @@ constructor( allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked()) } WhenToStartHub.WHILE_CHARGING_AND_POSTURED -> { allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured) // Only listen to posturing if applicable to avoid running the posturing // CHRE nanoapp when not needed. batteryInteractor.isDevicePluggedIn.flatMapLatestConflated { isCharging -> if (isCharging) { posturingInteractor.postured } else { flowOf(false) } } } WhenToStartHub.NEVER -> flowOf(false) } Loading
packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt 0 → 100644 +48 −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.systemui.communal.posturing.data.model import androidx.annotation.FloatRange data class PositionState( val stationary: StationaryState = StationaryState.Unknown, val orientation: OrientationState = OrientationState.Unknown, ) { sealed interface StationaryState { @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float data object Unknown : StationaryState { override val confidence: Float = 0f } data class Stationary(override val confidence: Float) : StationaryState data class NotStationary(override val confidence: Float) : StationaryState } sealed interface OrientationState { @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float data object Unknown : OrientationState { override val confidence: Float = 0f } data class Postured(override val confidence: Float) : OrientationState data class NotPostured(override val confidence: Float) : OrientationState } }