Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a21b506c authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement different thresholds for entering and exiting posturing" into main

parents eb9c2875 0bf08bf6
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -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()
@@ -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)
@@ -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()
        }

@@ -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)
@@ -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),
@@ -262,6 +271,8 @@ class PosturingInteractorTest : SysuiTestCase() {
                )
            )

            advanceTimeByBatchingDuration()

            assertThat(mayBePostured).isFalse()
            assertThat(postured).isFalse()

@@ -274,6 +285,8 @@ class PosturingInteractorTest : SysuiTestCase() {
                )
            )

            advanceTimeByBatchingDuration()

            assertThat(mayBePostured).isFalse()
            assertThat(postured).isFalse()

@@ -286,6 +299,8 @@ class PosturingInteractorTest : SysuiTestCase() {
                )
            )

            advanceTimeByBatchingDuration()

            assertThat(mayBePostured).isTrue()
            assertThat(postured).isFalse()

+11 −11
Original line number Diff line number Diff line
@@ -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,
@@ -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()
@@ -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)

@@ -129,11 +132,6 @@ constructor(
                }
                .dropWhile { !it }
                .distinctUntilChanged()
                .logDiffsForTable(
                    tableLogBuffer = tableLogBuffer,
                    columnName = "mayBePosturedSoon",
                    initialValue = false,
                )
                .collect { mayBePosturedSoon ->
                    if (mayBePosturedSoon) {
                        wakeLock.acquire(TAG)
@@ -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!")
+88 −32
Original line number Diff line number Diff line
@@ -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
@@ -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)

@@ -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,
@@ -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
@@ -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
    }
}

+33 −5
Original line number Diff line number Diff line
@@ -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"
    }
}
+10 −2
Original line number Diff line number Diff line
@@ -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> {
@@ -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()
}