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

Commit ed7c2ea7 authored by Lucas Silva's avatar Lucas Silva
Browse files

Reapply "Update posturing detection to reduce false positives"

Roll-forward with bug fix which caused CTS tests to fail

This reverts commit 429f16c5.

Bug: 396460215
Test: CapoCHRERepositoryTest
Flag: com.android.systemui.glanceable_hub_v2
Change-Id: I529b698049d21e208337a2b0553633c25f1fc053
parent 470f5318
Loading
Loading
Loading
Loading
+16 −4
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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()
        }
@@ -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()
+216 −2
Original line number Diff line number Diff line
@@ -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()
@@ -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
@@ -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()) }
        }
    }
}
+28 −4
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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,
@@ -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 -> {
@@ -87,7 +111,7 @@ constructor(
                        null
                    }
                }
            state?.let { interactor.setValueForDebug(it) }
            state?.let { posturingInteractor.setValueForDebug(it) }
        }

        override fun help(pw: PrintWriter) {
+9 −1
Original line number Diff line number Diff line
@@ -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)
                }
+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