Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +5 −4 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ 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.domain.interactor.advanceTimeBySlidingWindowAndRun import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel import com.android.systemui.dock.DockManager import com.android.systemui.dock.fakeDockManager import com.android.systemui.kosmos.Kosmos Loading Loading @@ -136,8 +137,8 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.NotPostured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Negative(confidence = 1f), ) ) Loading @@ -150,8 +151,8 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +66 −18 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ 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.ConfidenceLevel import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.advanceTimeBy Loading Loading @@ -63,8 +64,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -80,8 +81,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -97,8 +98,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -114,8 +115,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -125,8 +126,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(postured).isFalse() Loading @@ -153,8 +154,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading Loading @@ -183,8 +184,8 @@ class PosturingInteractorTest : SysuiTestCase() { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) } Loading @@ -207,8 +208,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -232,8 +233,8 @@ class PosturingInteractorTest : SysuiTestCase() { underTest.setValueForDebug(PosturedState.NotPostured) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -246,6 +247,53 @@ class PosturingInteractorTest : SysuiTestCase() { assertThat(postured).isTrue() } @Test fun testMayBePostured() = kosmos.runTest { val mayBePostured by collectLastValue(underTest.mayBePostured) val postured by collectLastValue(underTest.postured) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Negative(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() // 10ms later, the device becomes stationary with 0.5 confidence. advanceTimeBy(10.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 0.5f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() // 20ms later, the device becomes fully stationary with 1.0 confidence. advanceTimeBy(20.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isTrue() assertThat(postured).isFalse() advanceTimeBySlidingWindowAndRun() assertThat(mayBePostured).isFalse() assertThat(postured).isTrue() } private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit { val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>() val pickupSensor = mock<Sensor>() Loading packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +65 −10 Original line number Diff line number Diff line Loading @@ -18,11 +18,14 @@ package com.android.systemui.communal import android.annotation.SuppressLint import android.app.DreamManager import android.os.PowerManager import android.service.dreams.Flags.allowDreamWhenPostured import com.android.app.tracing.coroutines.launchInTraced import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.CoreStartable import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor.Companion.SLIDING_WINDOW_DURATION import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background Loading @@ -31,14 +34,19 @@ import com.android.systemui.dreams.shared.model.WhenToDream import com.android.systemui.log.dagger.CommunalTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.power.domain.interactor.PowerInteractor 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.util.wakelock.WakeLock import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.io.PrintWriter import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach Loading @@ -54,17 +62,29 @@ constructor( batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val wakeLockBuilder: WakeLock.Builder, private val powerInteractor: PowerInteractor, ) : CoreStartable { private val command = DevicePosturingCommand() private val wakeLock by lazy { wakeLockBuilder .setMaxTimeout(SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_DIM_WAKE_LOCK) .build() } // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp // if posturing signal is not needed. private val postured = private val preconditions = allOf( batteryInteractor.isDevicePluggedIn, dreamSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED }, ) .flatMapLatestConflated { shouldListen -> private val postured = preconditions.flatMapLatestConflated { shouldListen -> if (shouldListen) { posturingInteractor.postured } else { Loading @@ -72,6 +92,15 @@ constructor( } } private val mayBePosturedSoon = preconditions.flatMapLatestConflated { shouldListen -> if (shouldListen) { allOf(posturingInteractor.mayBePostured, powerInteractor.isAwake) } else { flowOf(false) } } @SuppressLint("MissingPermission") override fun start() { if (!allowDreamWhenPostured()) { Loading @@ -88,6 +117,32 @@ constructor( .onEach { postured -> dreamManager.setDevicePostured(postured) } .launchInTraced("$TAG#collectPostured", bgScope) bgScope.launchTraced("$TAG#collectMayBePosturedSoon") { mayBePosturedSoon .debounce { mayBePostured -> // Wait to release the WakeLock so we have time to update the dream state. if (!mayBePostured) { 500.milliseconds } else { 0.milliseconds } } .dropWhile { !it } .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "mayBePosturedSoon", initialValue = false, ) .collect { mayBePosturedSoon -> if (mayBePosturedSoon) { wakeLock.acquire(TAG) } else { wakeLock.release(TAG) } } } commandRegistry.registerCommand(COMMAND_ROOT) { command } } Loading packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt +4 −28 Original line number Diff line number Diff line Loading @@ -16,33 +16,9 @@ package com.android.systemui.communal.posturing.data.model import androidx.annotation.FloatRange import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel 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 } } val stationary: ConfidenceLevel = ConfidenceLevel.Unknown, val orientation: ConfidenceLevel = ConfidenceLevel.Unknown, ) packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +36 −49 Original line number Diff line number Diff line Loading @@ -16,10 +16,10 @@ package com.android.systemui.communal.posturing.domain.interactor import android.annotation.SuppressLint import android.hardware.Sensor import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.PosturingRepository import com.android.systemui.communal.posturing.domain.model.AggregatedConfidenceState import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application Loading @@ -27,7 +27,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.observeTriggerSensor import com.android.systemui.util.kotlin.slidingWindow import com.android.systemui.util.sensors.AsyncSensorManager Loading Loading @@ -71,79 +70,64 @@ constructor( * Detects whether or not the device is stationary, applying a sliding window smoothing * algorithm. */ private val stationarySmoothed: Flow<Boolean> = private val stationarySmoothed: Flow<AggregatedConfidenceState> = merge( observeTriggerSensor(Sensor.TYPE_PICK_UP_GESTURE) // If pickup detected, avoid triggering posturing at all within the sliding // window by emitting a negative infinity value. .map { Float.NEGATIVE_INFINITY } .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("pickup gesture detected") }, observeTriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION) // If motion detected, avoid triggering posturing at all within the sliding // window by emitting a negative infinity value. .map { Float.NEGATIVE_INFINITY } .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("significant motion detected") }, repository.positionState .map { it.stationary } .filterNot { it is PositionState.StationaryState.Unknown } .map { stationaryState -> if (stationaryState is PositionState.StationaryState.Stationary) { stationaryState.confidence } else { // If not stationary, then we should effectively disable posturing by // emitting the lowest possible confidence. Float.NEGATIVE_INFINITY } }, .filterNot { it is ConfidenceLevel.Unknown }, ) .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> val avgStationaryConfidence = window.average() logger.i({ "stationary confidence: $double1 | window: $str1" }) { str1 = window.formatWindowForDebugging() double1 = avgStationaryConfidence } avgStationaryConfidence > CONFIDENCE_THRESHOLD } .map { window -> window.toConfidenceState() } /** * Detects whether or not the device is in an upright orientation, applying a sliding window * smoothing algorithm. */ private val orientationSmoothed: Flow<Boolean> = private val orientationSmoothed: Flow<AggregatedConfidenceState> = repository.positionState .map { it.orientation } .filterNot { it is PositionState.OrientationState.Unknown } .map { orientationState -> if (orientationState is PositionState.OrientationState.Postured) { orientationState.confidence } else { // If not postured, then we should effectively disable posturing by // emitting the lowest possible confidence. Float.NEGATIVE_INFINITY } } .filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> val avgOrientationConfidence = window.average() logger.i({ "orientation confidence: $double1 | window: $str1" }) { str1 = window.formatWindowForDebugging() double1 = avgOrientationConfidence } avgOrientationConfidence > CONFIDENCE_THRESHOLD } .map { window -> window.toConfidenceState() } /** * Posturing is composed of the device being stationary and in the correct orientation. If both * conditions are met, then consider it postured. */ private val posturedSmoothed: Flow<PosturedState> = allOf(stationarySmoothed, orientationSmoothed) .map { postured -> if (postured) { combine(stationarySmoothed, orientationSmoothed) { stationaryConfidence, orientationConfidence -> logger.i({ "stationary: $str1 | orientation: $str2" }) { str1 = stationaryConfidence.toString() str2 = orientationConfidence.toString() } val isStationary = stationaryConfidence.avgConfidence >= CONFIDENCE_THRESHOLD val isInOrientation = orientationConfidence.avgConfidence >= CONFIDENCE_THRESHOLD if (isStationary && isInOrientation) { PosturedState.Postured } else if ( stationaryConfidence.latestConfidence >= CONFIDENCE_THRESHOLD && orientationConfidence.latestConfidence >= CONFIDENCE_THRESHOLD ) { // We may be postured soon since the latest confidence is above the threshold. // If no new events come in, we will eventually transition to postured at the // end of the sliding window. PosturedState.MayBePostured } else { PosturedState.NotPostured } Loading Loading @@ -171,6 +155,9 @@ constructor( debugValue.asBoolean() ?: postured.asBoolean() ?: false } /** Whether the device may become postured soon. */ val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it == PosturedState.MayBePostured } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it * executes once. Loading @@ -192,11 +179,11 @@ fun PosturedState.asBoolean(): Boolean? { return when (this) { is PosturedState.Postured -> true PosturedState.NotPostured -> false PosturedState.MayBePostured -> false PosturedState.Unknown -> null } } @SuppressLint("DefaultLocale") fun List<Float>.formatWindowForDebugging(): String { return joinToString(prefix = "[", postfix = "]") { String.format("%.2f", it) } private fun List<ConfidenceLevel>.toConfidenceState(): AggregatedConfidenceState { return AggregatedConfidenceState(rawWindow = this.toList()) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +5 −4 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ 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.domain.interactor.advanceTimeBySlidingWindowAndRun import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel import com.android.systemui.dock.DockManager import com.android.systemui.dock.fakeDockManager import com.android.systemui.kosmos.Kosmos Loading Loading @@ -136,8 +137,8 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { batteryRepository.fake.setDevicePluggedIn(true) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.NotPostured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Negative(confidence = 1f), ) ) Loading @@ -150,8 +151,8 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) advanceTimeBySlidingWindowAndRun() Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +66 −18 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ 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.ConfidenceLevel import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.advanceTimeBy Loading Loading @@ -63,8 +64,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -80,8 +81,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -97,8 +98,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -114,8 +115,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 0.2f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 0.2f), ) ) Loading @@ -125,8 +126,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(postured).isFalse() Loading @@ -153,8 +154,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading Loading @@ -183,8 +184,8 @@ class PosturingInteractorTest : SysuiTestCase() { advanceTimeBy(1.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) } Loading @@ -207,8 +208,8 @@ class PosturingInteractorTest : SysuiTestCase() { posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -232,8 +233,8 @@ class PosturingInteractorTest : SysuiTestCase() { underTest.setValueForDebug(PosturedState.NotPostured) posturingRepository.fake.emitPositionState( PositionState( stationary = PositionState.StationaryState.Stationary(confidence = 1f), orientation = PositionState.OrientationState.Postured(confidence = 1f), stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) Loading @@ -246,6 +247,53 @@ class PosturingInteractorTest : SysuiTestCase() { assertThat(postured).isTrue() } @Test fun testMayBePostured() = kosmos.runTest { val mayBePostured by collectLastValue(underTest.mayBePostured) val postured by collectLastValue(underTest.postured) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Negative(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() // 10ms later, the device becomes stationary with 0.5 confidence. advanceTimeBy(10.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 0.5f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() // 20ms later, the device becomes fully stationary with 1.0 confidence. advanceTimeBy(20.milliseconds) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 1f), orientation = ConfidenceLevel.Positive(confidence = 1f), ) ) assertThat(mayBePostured).isTrue() assertThat(postured).isFalse() advanceTimeBySlidingWindowAndRun() assertThat(mayBePostured).isFalse() assertThat(postured).isTrue() } private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit { val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>() val pickupSensor = mock<Sensor>() Loading
packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +65 −10 Original line number Diff line number Diff line Loading @@ -18,11 +18,14 @@ package com.android.systemui.communal import android.annotation.SuppressLint import android.app.DreamManager import android.os.PowerManager import android.service.dreams.Flags.allowDreamWhenPostured import com.android.app.tracing.coroutines.launchInTraced import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.CoreStartable import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor.Companion.SLIDING_WINDOW_DURATION import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background Loading @@ -31,14 +34,19 @@ import com.android.systemui.dreams.shared.model.WhenToDream import com.android.systemui.log.dagger.CommunalTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.power.domain.interactor.PowerInteractor 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.util.wakelock.WakeLock import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.io.PrintWriter import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach Loading @@ -54,17 +62,29 @@ constructor( batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val wakeLockBuilder: WakeLock.Builder, private val powerInteractor: PowerInteractor, ) : CoreStartable { private val command = DevicePosturingCommand() private val wakeLock by lazy { wakeLockBuilder .setMaxTimeout(SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_DIM_WAKE_LOCK) .build() } // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp // if posturing signal is not needed. private val postured = private val preconditions = allOf( batteryInteractor.isDevicePluggedIn, dreamSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED }, ) .flatMapLatestConflated { shouldListen -> private val postured = preconditions.flatMapLatestConflated { shouldListen -> if (shouldListen) { posturingInteractor.postured } else { Loading @@ -72,6 +92,15 @@ constructor( } } private val mayBePosturedSoon = preconditions.flatMapLatestConflated { shouldListen -> if (shouldListen) { allOf(posturingInteractor.mayBePostured, powerInteractor.isAwake) } else { flowOf(false) } } @SuppressLint("MissingPermission") override fun start() { if (!allowDreamWhenPostured()) { Loading @@ -88,6 +117,32 @@ constructor( .onEach { postured -> dreamManager.setDevicePostured(postured) } .launchInTraced("$TAG#collectPostured", bgScope) bgScope.launchTraced("$TAG#collectMayBePosturedSoon") { mayBePosturedSoon .debounce { mayBePostured -> // Wait to release the WakeLock so we have time to update the dream state. if (!mayBePostured) { 500.milliseconds } else { 0.milliseconds } } .dropWhile { !it } .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "mayBePosturedSoon", initialValue = false, ) .collect { mayBePosturedSoon -> if (mayBePosturedSoon) { wakeLock.acquire(TAG) } else { wakeLock.release(TAG) } } } commandRegistry.registerCommand(COMMAND_ROOT) { command } } Loading
packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt +4 −28 Original line number Diff line number Diff line Loading @@ -16,33 +16,9 @@ package com.android.systemui.communal.posturing.data.model import androidx.annotation.FloatRange import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel 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 } } val stationary: ConfidenceLevel = ConfidenceLevel.Unknown, val orientation: ConfidenceLevel = ConfidenceLevel.Unknown, )
packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +36 −49 Original line number Diff line number Diff line Loading @@ -16,10 +16,10 @@ package com.android.systemui.communal.posturing.domain.interactor import android.annotation.SuppressLint import android.hardware.Sensor import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.PosturingRepository import com.android.systemui.communal.posturing.domain.model.AggregatedConfidenceState import com.android.systemui.communal.posturing.shared.model.ConfidenceLevel import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application Loading @@ -27,7 +27,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.observeTriggerSensor import com.android.systemui.util.kotlin.slidingWindow import com.android.systemui.util.sensors.AsyncSensorManager Loading Loading @@ -71,79 +70,64 @@ constructor( * Detects whether or not the device is stationary, applying a sliding window smoothing * algorithm. */ private val stationarySmoothed: Flow<Boolean> = private val stationarySmoothed: Flow<AggregatedConfidenceState> = merge( observeTriggerSensor(Sensor.TYPE_PICK_UP_GESTURE) // If pickup detected, avoid triggering posturing at all within the sliding // window by emitting a negative infinity value. .map { Float.NEGATIVE_INFINITY } .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("pickup gesture detected") }, observeTriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION) // If motion detected, avoid triggering posturing at all within the sliding // window by emitting a negative infinity value. .map { Float.NEGATIVE_INFINITY } .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("significant motion detected") }, repository.positionState .map { it.stationary } .filterNot { it is PositionState.StationaryState.Unknown } .map { stationaryState -> if (stationaryState is PositionState.StationaryState.Stationary) { stationaryState.confidence } else { // If not stationary, then we should effectively disable posturing by // emitting the lowest possible confidence. Float.NEGATIVE_INFINITY } }, .filterNot { it is ConfidenceLevel.Unknown }, ) .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> val avgStationaryConfidence = window.average() logger.i({ "stationary confidence: $double1 | window: $str1" }) { str1 = window.formatWindowForDebugging() double1 = avgStationaryConfidence } avgStationaryConfidence > CONFIDENCE_THRESHOLD } .map { window -> window.toConfidenceState() } /** * Detects whether or not the device is in an upright orientation, applying a sliding window * smoothing algorithm. */ private val orientationSmoothed: Flow<Boolean> = private val orientationSmoothed: Flow<AggregatedConfidenceState> = repository.positionState .map { it.orientation } .filterNot { it is PositionState.OrientationState.Unknown } .map { orientationState -> if (orientationState is PositionState.OrientationState.Postured) { orientationState.confidence } else { // If not postured, then we should effectively disable posturing by // emitting the lowest possible confidence. Float.NEGATIVE_INFINITY } } .filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> val avgOrientationConfidence = window.average() logger.i({ "orientation confidence: $double1 | window: $str1" }) { str1 = window.formatWindowForDebugging() double1 = avgOrientationConfidence } avgOrientationConfidence > CONFIDENCE_THRESHOLD } .map { window -> window.toConfidenceState() } /** * Posturing is composed of the device being stationary and in the correct orientation. If both * conditions are met, then consider it postured. */ private val posturedSmoothed: Flow<PosturedState> = allOf(stationarySmoothed, orientationSmoothed) .map { postured -> if (postured) { combine(stationarySmoothed, orientationSmoothed) { stationaryConfidence, orientationConfidence -> logger.i({ "stationary: $str1 | orientation: $str2" }) { str1 = stationaryConfidence.toString() str2 = orientationConfidence.toString() } val isStationary = stationaryConfidence.avgConfidence >= CONFIDENCE_THRESHOLD val isInOrientation = orientationConfidence.avgConfidence >= CONFIDENCE_THRESHOLD if (isStationary && isInOrientation) { PosturedState.Postured } else if ( stationaryConfidence.latestConfidence >= CONFIDENCE_THRESHOLD && orientationConfidence.latestConfidence >= CONFIDENCE_THRESHOLD ) { // We may be postured soon since the latest confidence is above the threshold. // If no new events come in, we will eventually transition to postured at the // end of the sliding window. PosturedState.MayBePostured } else { PosturedState.NotPostured } Loading Loading @@ -171,6 +155,9 @@ constructor( debugValue.asBoolean() ?: postured.asBoolean() ?: false } /** Whether the device may become postured soon. */ val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it == PosturedState.MayBePostured } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it * executes once. Loading @@ -192,11 +179,11 @@ fun PosturedState.asBoolean(): Boolean? { return when (this) { is PosturedState.Postured -> true PosturedState.NotPostured -> false PosturedState.MayBePostured -> false PosturedState.Unknown -> null } } @SuppressLint("DefaultLocale") fun List<Float>.formatWindowForDebugging(): String { return joinToString(prefix = "[", postfix = "]") { String.format("%.2f", it) } private fun List<ConfidenceLevel>.toConfidenceState(): AggregatedConfidenceState { return AggregatedConfidenceState(rawWindow = this.toList()) }