Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +16 −1 Original line number Diff line number Diff line Loading @@ -134,6 +134,8 @@ class PosturingInteractorTest : SysuiTestCase() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() advanceTimeByBatchingDuration() // The 0.2 confidence will have fallen out of the sliding window, and we should now flip // to true. assertThat(postured).isTrue() Loading Loading @@ -164,6 +166,7 @@ class PosturingInteractorTest : SysuiTestCase() { // If we detect a lift gesture, we should transition back to not postured. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() advanceTimeBy(9999.hours) Loading @@ -190,10 +193,13 @@ class PosturingInteractorTest : SysuiTestCase() { ) } advanceTimeByBatchingDuration() assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured immediately. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() } Loading @@ -218,6 +224,7 @@ class PosturingInteractorTest : SysuiTestCase() { // If we detect motion, we should transition back to not postured. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() advanceTimeBy(9999.hours) Loading @@ -230,7 +237,9 @@ class PosturingInteractorTest : SysuiTestCase() { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.NotPostured) underTest.setValueForDebug( PosturedState.NotPostured(isStationary = false, inOrientation = false) ) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 1f), Loading Loading @@ -262,6 +271,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() Loading @@ -274,6 +285,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() Loading @@ -286,6 +299,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isTrue() assertThat(postured).isFalse() Loading packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +11 −11 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ constructor( private val dreamManager: DreamManager, private val posturingInteractor: PosturingInteractor, dreamSettingsInteractor: DreamSettingsInteractor, batteryInteractor: BatteryInteractor, private val batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val wakeLockBuilder: WakeLock.Builder, Loading @@ -69,7 +69,7 @@ constructor( private val wakeLock by lazy { wakeLockBuilder .setMaxTimeout(SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setMaxTimeout(2 * SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_DIM_WAKE_LOCK) .build() Loading Loading @@ -107,13 +107,16 @@ constructor( return } postured .distinctUntilChanged() batteryInteractor.isDevicePluggedIn .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "postured", columnName = "isDevicePluggedIn", initialValue = false, ) .launchInTraced("$TAG#collectIsDevicePluggedIn", bgScope) postured .distinctUntilChanged() .onEach { postured -> dreamManager.setDevicePostured(postured) } .launchInTraced("$TAG#collectPostured", bgScope) Loading @@ -129,11 +132,6 @@ constructor( } .dropWhile { !it } .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "mayBePosturedSoon", initialValue = false, ) .collect { mayBePosturedSoon -> if (mayBePosturedSoon) { wakeLock.acquire(TAG) Loading @@ -158,7 +156,9 @@ constructor( val state = when (arg.lowercase()) { "true" -> PosturedState.Postured "false" -> PosturedState.NotPostured "false" -> PosturedState.NotPostured(isStationary = false, inOrientation = false) "clear" -> PosturedState.Unknown else -> { pw.println("Invalid argument!") Loading packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +88 −32 Original line number Diff line number Diff line Loading @@ -27,18 +27,26 @@ 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.log.dagger.CommunalTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.util.kotlin.observeTriggerSensor import com.android.systemui.util.kotlin.pairwiseBy import com.android.systemui.util.kotlin.slidingWindow import com.android.systemui.util.sensors.AsyncSensorManager import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOn Loading @@ -56,7 +64,8 @@ constructor( @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @CommunalLog private val logBuffer: LogBuffer, clock: SystemClock, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val clock: SystemClock, ) { private val logger = Logger(logBuffer, TAG) Loading @@ -74,64 +83,60 @@ constructor( 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. // window by emitting a negative confidence. .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. // window by emitting a negative confidence. .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("significant motion detected") }, repository.positionState .map { it.stationary } .filterNot { it is ConfidenceLevel.Unknown }, repository.positionState.map { it.stationary }, ) .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> window.toConfidenceState() } .aggregateConfidences() /** * Detects whether or not the device is in an upright orientation, applying a sliding window * smoothing algorithm. */ private val orientationSmoothed: Flow<AggregatedConfidenceState> = repository.positionState .map { it.orientation } .filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> window.toConfidenceState() } repository.positionState.map { it.orientation }.aggregateConfidences() /** * 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> = combine(stationarySmoothed, orientationSmoothed) { stationaryConfidence, orientationConfidence -> logger.i({ "stationary: $str1 | orientation: $str2" }) { private val posturedSmoothed: StateFlow<PosturedState> = combine(stationarySmoothed, orientationSmoothed, ::Pair) // Add small debounce to batch the processing of stationary and orientation changes // which come in very close together. .debounce(BATCHING_DEBOUNCE_DURATION) .map { (stationaryConfidence, orientationConfidence) -> val isStationary = stationaryConfidence.isStationary() val isInOrientation = orientationConfidence.isInOrientation() logger.i({ "stationary ($bool1): $str1 | orientation ($bool2): $str2" }) { bool1 = isStationary str1 = stationaryConfidence.toString() bool2 = isInOrientation 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 stationaryConfidence.latestConfidence >= ENTER_CONFIDENCE_THRESHOLD && orientationConfidence.latestConfidence >= ENTER_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 PosturedState.MayBePostured(isStationary, isInOrientation) } else { PosturedState.NotPostured PosturedState.NotPostured(isStationary, isInOrientation) } } .logDiffsForTable(tableLogBuffer = tableLogBuffer, initialValue = PosturedState.Unknown) .flowOn(bgDispatcher) .stateIn( scope = applicationScope, Loading @@ -156,7 +161,23 @@ constructor( } /** Whether the device may become postured soon. */ val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it == PosturedState.MayBePostured } val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it is PosturedState.MayBePostured } /** Helper for aggregating the confidence levels in the sliding window. */ private fun Flow<ConfidenceLevel>.aggregateConfidences(): Flow<AggregatedConfidenceState> = filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .pairwiseBy(emptyList()) { old, new -> // If all elements have expired out of the window, then maintain only the last and // most recent element. if (old.isNotEmpty() && new.isEmpty()) { old.subList(old.lastIndex, old.lastIndex + 1) } else { new } } .distinctUntilChanged() .map { window -> window.toConfidenceState() } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it Loading @@ -167,20 +188,55 @@ constructor( return asyncSensorManager.observeTriggerSensor(sensor) } private fun AggregatedConfidenceState.isStationary(): Boolean { return avgConfidence >= getThreshold(posturedSmoothed.value.isStationary) } private fun AggregatedConfidenceState.isInOrientation(): Boolean { return avgConfidence >= getThreshold(posturedSmoothed.value.inOrientation) } private fun getThreshold(currentlyMeetsThreshold: Boolean) = if (currentlyMeetsThreshold) { EXIT_CONFIDENCE_THRESHOLD } else { ENTER_CONFIDENCE_THRESHOLD } companion object { const val TAG = "PosturingInteractor" val SLIDING_WINDOW_DURATION = 10.seconds const val CONFIDENCE_THRESHOLD = 0.8f /** * The confidence threshold required to enter a stationary / orientation state. If the * confidence is greater than this, we may enter a postured state. */ const val ENTER_CONFIDENCE_THRESHOLD = 0.8f /** * The confidence threshold required to exit a stationary / orientation state. If the * confidence is less than this, we may exit the postured state. This is smaller than * [ENTER_CONFIDENCE_THRESHOLD] to help ensure we don't exit the state due to small amounts * of motion, such as the user tapping on the screen. */ const val EXIT_CONFIDENCE_THRESHOLD = 0.5f /** * Amount of time to keep the posturing algorithm running after the last subscriber * unsubscribes. This helps ensure that if the charging connection is flaky, we don't lose * the posturing state. */ val STOP_TIMEOUT_AFTER_UNSUBSCRIBE = 5.seconds /** Debounce duration to batch the processing of events. */ val BATCHING_DEBOUNCE_DURATION = 10.milliseconds } } fun PosturedState.asBoolean(): Boolean? { return when (this) { is PosturedState.Postured -> true PosturedState.NotPostured -> false PosturedState.MayBePostured -> false PosturedState.Unknown -> null else -> isStationary && inOrientation } } Loading packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt +33 −5 Original line number Diff line number Diff line Loading @@ -16,16 +16,44 @@ package com.android.systemui.communal.posturing.shared.model sealed interface PosturedState { import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger sealed interface PosturedState : Diffable<PosturedState> { val isStationary: Boolean val inOrientation: Boolean override fun logDiffs(prevVal: PosturedState, row: TableRowLogger) { if (prevVal != this) { row.logChange(COL_POSTURED_STATE, toString()) } } /** Represents postured state */ data object Postured : PosturedState data object Postured : PosturedState { override val isStationary: Boolean = true override val inOrientation: Boolean = true } /** Represents state where we may be postured but we aren't sure yet */ data object MayBePostured : PosturedState data class MayBePostured( override val isStationary: Boolean, override val inOrientation: Boolean, ) : PosturedState /** Represents unknown/uninitialized state */ data object Unknown : PosturedState data object Unknown : PosturedState { override val isStationary: Boolean = false override val inOrientation: Boolean = false } /** Represents state where we are not postured */ data object NotPostured : PosturedState data class NotPostured( override val isStationary: Boolean, override val inOrientation: Boolean, ) : PosturedState companion object { const val COL_POSTURED_STATE = "posturedState" } } packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt +10 −2 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.util.sensors.asyncSensorManager import com.android.systemui.util.time.systemClock import kotlin.time.Duration.Companion.milliseconds val Kosmos.posturingInteractor by Kosmos.Fixture<PosturingInteractor> { Loading @@ -34,11 +36,17 @@ val Kosmos.posturingInteractor by applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, logBuffer = logcatLogBuffer("PosturingInteractor"), tableLogBuffer = logcatTableLogBuffer(systemClock, "PosturingInteractor"), clock = systemClock, ) } fun Kosmos.advanceTimeBySlidingWindowAndRun() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION) fun Kosmos.advanceTimeByBatchingDuration() { advanceTimeBy(PosturingInteractor.BATCHING_DEBOUNCE_DURATION) runCurrent() } fun Kosmos.advanceTimeBySlidingWindowAndRun() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION + 10.milliseconds) advanceTimeByBatchingDuration() } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +16 −1 Original line number Diff line number Diff line Loading @@ -134,6 +134,8 @@ class PosturingInteractorTest : SysuiTestCase() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) runCurrent() advanceTimeByBatchingDuration() // The 0.2 confidence will have fallen out of the sliding window, and we should now flip // to true. assertThat(postured).isTrue() Loading Loading @@ -164,6 +166,7 @@ class PosturingInteractorTest : SysuiTestCase() { // If we detect a lift gesture, we should transition back to not postured. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() advanceTimeBy(9999.hours) Loading @@ -190,10 +193,13 @@ class PosturingInteractorTest : SysuiTestCase() { ) } advanceTimeByBatchingDuration() assertThat(postured).isTrue() // If we detect a lift gesture, we should transition back to not postured immediately. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() } Loading @@ -218,6 +224,7 @@ class PosturingInteractorTest : SysuiTestCase() { // If we detect motion, we should transition back to not postured. triggerSensor(sensor) advanceTimeByBatchingDuration() assertThat(postured).isFalse() advanceTimeBy(9999.hours) Loading @@ -230,7 +237,9 @@ class PosturingInteractorTest : SysuiTestCase() { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.NotPostured) underTest.setValueForDebug( PosturedState.NotPostured(isStationary = false, inOrientation = false) ) posturingRepository.fake.emitPositionState( PositionState( stationary = ConfidenceLevel.Positive(confidence = 1f), Loading Loading @@ -262,6 +271,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() Loading @@ -274,6 +285,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isFalse() assertThat(postured).isFalse() Loading @@ -286,6 +299,8 @@ class PosturingInteractorTest : SysuiTestCase() { ) ) advanceTimeByBatchingDuration() assertThat(mayBePostured).isTrue() assertThat(postured).isFalse() Loading
packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +11 −11 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ constructor( private val dreamManager: DreamManager, private val posturingInteractor: PosturingInteractor, dreamSettingsInteractor: DreamSettingsInteractor, batteryInteractor: BatteryInteractor, private val batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val wakeLockBuilder: WakeLock.Builder, Loading @@ -69,7 +69,7 @@ constructor( private val wakeLock by lazy { wakeLockBuilder .setMaxTimeout(SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setMaxTimeout(2 * SLIDING_WINDOW_DURATION.inWholeMilliseconds) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_DIM_WAKE_LOCK) .build() Loading Loading @@ -107,13 +107,16 @@ constructor( return } postured .distinctUntilChanged() batteryInteractor.isDevicePluggedIn .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "postured", columnName = "isDevicePluggedIn", initialValue = false, ) .launchInTraced("$TAG#collectIsDevicePluggedIn", bgScope) postured .distinctUntilChanged() .onEach { postured -> dreamManager.setDevicePostured(postured) } .launchInTraced("$TAG#collectPostured", bgScope) Loading @@ -129,11 +132,6 @@ constructor( } .dropWhile { !it } .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, columnName = "mayBePosturedSoon", initialValue = false, ) .collect { mayBePosturedSoon -> if (mayBePosturedSoon) { wakeLock.acquire(TAG) Loading @@ -158,7 +156,9 @@ constructor( val state = when (arg.lowercase()) { "true" -> PosturedState.Postured "false" -> PosturedState.NotPostured "false" -> PosturedState.NotPostured(isStationary = false, inOrientation = false) "clear" -> PosturedState.Unknown else -> { pw.println("Invalid argument!") Loading
packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +88 −32 Original line number Diff line number Diff line Loading @@ -27,18 +27,26 @@ 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.log.dagger.CommunalTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.util.kotlin.observeTriggerSensor import com.android.systemui.util.kotlin.pairwiseBy import com.android.systemui.util.kotlin.slidingWindow import com.android.systemui.util.sensors.AsyncSensorManager import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOn Loading @@ -56,7 +64,8 @@ constructor( @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @CommunalLog private val logBuffer: LogBuffer, clock: SystemClock, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, private val clock: SystemClock, ) { private val logger = Logger(logBuffer, TAG) Loading @@ -74,64 +83,60 @@ constructor( 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. // window by emitting a negative confidence. .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. // window by emitting a negative confidence. .map { ConfidenceLevel.Negative(1f) } .onEach { logger.i("significant motion detected") }, repository.positionState .map { it.stationary } .filterNot { it is ConfidenceLevel.Unknown }, repository.positionState.map { it.stationary }, ) .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> window.toConfidenceState() } .aggregateConfidences() /** * Detects whether or not the device is in an upright orientation, applying a sliding window * smoothing algorithm. */ private val orientationSmoothed: Flow<AggregatedConfidenceState> = repository.positionState .map { it.orientation } .filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .filterNot { it.isEmpty() } .map { window -> window.toConfidenceState() } repository.positionState.map { it.orientation }.aggregateConfidences() /** * 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> = combine(stationarySmoothed, orientationSmoothed) { stationaryConfidence, orientationConfidence -> logger.i({ "stationary: $str1 | orientation: $str2" }) { private val posturedSmoothed: StateFlow<PosturedState> = combine(stationarySmoothed, orientationSmoothed, ::Pair) // Add small debounce to batch the processing of stationary and orientation changes // which come in very close together. .debounce(BATCHING_DEBOUNCE_DURATION) .map { (stationaryConfidence, orientationConfidence) -> val isStationary = stationaryConfidence.isStationary() val isInOrientation = orientationConfidence.isInOrientation() logger.i({ "stationary ($bool1): $str1 | orientation ($bool2): $str2" }) { bool1 = isStationary str1 = stationaryConfidence.toString() bool2 = isInOrientation 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 stationaryConfidence.latestConfidence >= ENTER_CONFIDENCE_THRESHOLD && orientationConfidence.latestConfidence >= ENTER_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 PosturedState.MayBePostured(isStationary, isInOrientation) } else { PosturedState.NotPostured PosturedState.NotPostured(isStationary, isInOrientation) } } .logDiffsForTable(tableLogBuffer = tableLogBuffer, initialValue = PosturedState.Unknown) .flowOn(bgDispatcher) .stateIn( scope = applicationScope, Loading @@ -156,7 +161,23 @@ constructor( } /** Whether the device may become postured soon. */ val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it == PosturedState.MayBePostured } val mayBePostured: Flow<Boolean> = posturedSmoothed.map { it is PosturedState.MayBePostured } /** Helper for aggregating the confidence levels in the sliding window. */ private fun Flow<ConfidenceLevel>.aggregateConfidences(): Flow<AggregatedConfidenceState> = filterNot { it is ConfidenceLevel.Unknown } .slidingWindow(SLIDING_WINDOW_DURATION, clock) .pairwiseBy(emptyList()) { old, new -> // If all elements have expired out of the window, then maintain only the last and // most recent element. if (old.isNotEmpty() && new.isEmpty()) { old.subList(old.lastIndex, old.lastIndex + 1) } else { new } } .distinctUntilChanged() .map { window -> window.toConfidenceState() } /** * Helper for observing a trigger sensor, which automatically unregisters itself after it Loading @@ -167,20 +188,55 @@ constructor( return asyncSensorManager.observeTriggerSensor(sensor) } private fun AggregatedConfidenceState.isStationary(): Boolean { return avgConfidence >= getThreshold(posturedSmoothed.value.isStationary) } private fun AggregatedConfidenceState.isInOrientation(): Boolean { return avgConfidence >= getThreshold(posturedSmoothed.value.inOrientation) } private fun getThreshold(currentlyMeetsThreshold: Boolean) = if (currentlyMeetsThreshold) { EXIT_CONFIDENCE_THRESHOLD } else { ENTER_CONFIDENCE_THRESHOLD } companion object { const val TAG = "PosturingInteractor" val SLIDING_WINDOW_DURATION = 10.seconds const val CONFIDENCE_THRESHOLD = 0.8f /** * The confidence threshold required to enter a stationary / orientation state. If the * confidence is greater than this, we may enter a postured state. */ const val ENTER_CONFIDENCE_THRESHOLD = 0.8f /** * The confidence threshold required to exit a stationary / orientation state. If the * confidence is less than this, we may exit the postured state. This is smaller than * [ENTER_CONFIDENCE_THRESHOLD] to help ensure we don't exit the state due to small amounts * of motion, such as the user tapping on the screen. */ const val EXIT_CONFIDENCE_THRESHOLD = 0.5f /** * Amount of time to keep the posturing algorithm running after the last subscriber * unsubscribes. This helps ensure that if the charging connection is flaky, we don't lose * the posturing state. */ val STOP_TIMEOUT_AFTER_UNSUBSCRIBE = 5.seconds /** Debounce duration to batch the processing of events. */ val BATCHING_DEBOUNCE_DURATION = 10.milliseconds } } fun PosturedState.asBoolean(): Boolean? { return when (this) { is PosturedState.Postured -> true PosturedState.NotPostured -> false PosturedState.MayBePostured -> false PosturedState.Unknown -> null else -> isStationary && inOrientation } } Loading
packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt +33 −5 Original line number Diff line number Diff line Loading @@ -16,16 +16,44 @@ package com.android.systemui.communal.posturing.shared.model sealed interface PosturedState { import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger sealed interface PosturedState : Diffable<PosturedState> { val isStationary: Boolean val inOrientation: Boolean override fun logDiffs(prevVal: PosturedState, row: TableRowLogger) { if (prevVal != this) { row.logChange(COL_POSTURED_STATE, toString()) } } /** Represents postured state */ data object Postured : PosturedState data object Postured : PosturedState { override val isStationary: Boolean = true override val inOrientation: Boolean = true } /** Represents state where we may be postured but we aren't sure yet */ data object MayBePostured : PosturedState data class MayBePostured( override val isStationary: Boolean, override val inOrientation: Boolean, ) : PosturedState /** Represents unknown/uninitialized state */ data object Unknown : PosturedState data object Unknown : PosturedState { override val isStationary: Boolean = false override val inOrientation: Boolean = false } /** Represents state where we are not postured */ data object NotPostured : PosturedState data class NotPostured( override val isStationary: Boolean, override val inOrientation: Boolean, ) : PosturedState companion object { const val COL_POSTURED_STATE = "posturedState" } }
packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt +10 −2 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.util.sensors.asyncSensorManager import com.android.systemui.util.time.systemClock import kotlin.time.Duration.Companion.milliseconds val Kosmos.posturingInteractor by Kosmos.Fixture<PosturingInteractor> { Loading @@ -34,11 +36,17 @@ val Kosmos.posturingInteractor by applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, logBuffer = logcatLogBuffer("PosturingInteractor"), tableLogBuffer = logcatTableLogBuffer(systemClock, "PosturingInteractor"), clock = systemClock, ) } fun Kosmos.advanceTimeBySlidingWindowAndRun() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION) fun Kosmos.advanceTimeByBatchingDuration() { advanceTimeBy(PosturingInteractor.BATCHING_DEBOUNCE_DURATION) runCurrent() } fun Kosmos.advanceTimeBySlidingWindowAndRun() { advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION + 10.milliseconds) advanceTimeByBatchingDuration() }