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

Commit 8ef4bffe authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Reapply "Update posturing detection to reduce false positives"" into main

parents 5bbdd77b ed7c2ea7
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