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

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

Merge "Wake up from dreams when lift gesture detected" into main

parents cef447e8 d54fd760
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -74,6 +74,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
import com.android.systemui.navigationbar.gestural.domain.TaskInfo
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
@@ -265,6 +266,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
                mDreamOverlayCallbackController,
                kosmos.keyguardInteractor,
                gestureInteractor,
                kosmos.wakeGestureMonitor,
                kosmos.powerInteractor,
                WINDOW_NAME,
            )
    }
+101 −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.dreams

import android.hardware.Sensor
import android.hardware.TriggerEventListener
import android.hardware.display.ambientDisplayConfiguration
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectValues
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 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.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub

@SmallTest
@RunWith(AndroidJUnit4::class)
class WakeGestureMonitorTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val Kosmos.underTest by Kosmos.Fixture { wakeGestureMonitor }

    @Test
    fun testPickupGestureNotEnabled_doesNotSubscribeToSensor() =
        kosmos.runTest {
            ambientDisplayConfiguration.fakePickupGestureEnabled = false
            val triggerSensor = stubSensorManager()

            val wakeUpDetected by collectValues(underTest.wakeUpDetected)
            triggerSensor()
            assertThat(wakeUpDetected).isEmpty()
        }

    @Test
    fun testPickupGestureEnabled_subscribesToSensor() =
        kosmos.runTest {
            ambientDisplayConfiguration.fakePickupGestureEnabled = true
            val triggerSensor = stubSensorManager()

            val wakeUpDetected by collectValues(underTest.wakeUpDetected)
            triggerSensor()
            assertThat(wakeUpDetected).hasSize(1)
            triggerSensor()
            assertThat(wakeUpDetected).hasSize(2)
        }

    private fun Kosmos.stubSensorManager(): () -> Unit {
        val callbacks = mutableListOf<TriggerEventListener>()
        val pickupSensor = mock<Sensor>()

        asyncSensorManager.stub {
            on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor
            on { requestTriggerSensor(any(), eq(pickupSensor)) } doAnswer
                {
                    val callback = it.arguments[0] as TriggerEventListener
                    callbacks.add(callback)
                    true
                }
            on { cancelTriggerSensor(any(), any()) } doAnswer
                {
                    val callback = it.arguments[0] as TriggerEventListener
                    callbacks.remove(callback)
                    true
                }
        }

        return {
            val list = callbacks.toList()
            // Simulate a trigger sensor which unregisters callbacks after triggering.
            while (callbacks.isNotEmpty()) {
                callbacks.removeLast()
            }
            list.forEach { it.onTrigger(mock()) }
        }
    }
}
+5 −34
Original line number Diff line number Diff line
@@ -18,8 +18,6 @@ package com.android.systemui.communal.posturing.domain.interactor

import android.annotation.SuppressLint
import android.hardware.Sensor
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
import com.android.systemui.communal.posturing.data.model.PositionState
import com.android.systemui.communal.posturing.data.repository.PosturingRepository
import com.android.systemui.communal.posturing.shared.model.PosturedState
@@ -30,20 +28,19 @@ 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
import com.android.systemui.util.time.SystemClock
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -178,35 +175,9 @@ constructor(
     * Helper for observing a trigger sensor, which automatically unregisters itself after it
     * executes once.
     */
    private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow {
        val sensor = asyncSensorManager.getDefaultSensor(type)
        val isRegistered = AtomicBoolean(false)

        fun registerCallbackInternal(callback: TriggerEventListener) {
            if (isRegistered.compareAndSet(false, true)) {
                asyncSensorManager.requestTriggerSensor(callback, sensor)
            }
        }

        val callback =
            object : TriggerEventListener() {
                override fun onTrigger(event: TriggerEvent) {
                    trySend(Unit)
                    if (isRegistered.getAndSet(false)) {
                        registerCallbackInternal(this)
                    }
                }
            }

        if (sensor != null) {
            registerCallbackInternal(callback)
        }

        awaitClose {
            if (isRegistered.getAndSet(false)) {
                asyncSensorManager.cancelTriggerSensor(callback, sensor)
            }
        }
    private fun observeTriggerSensor(type: Int): Flow<Unit> {
        val sensor = asyncSensorManager.getDefaultSensor(type) ?: return emptyFlow()
        return asyncSensorManager.observeTriggerSensor(sensor)
    }

    companion object {
+23 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.dreams;

import static android.service.dreams.Flags.dreamWakeRedirect;
import static android.service.dreams.Flags.dreamsV2;

import static com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming;
import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
@@ -29,6 +30,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.PowerManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -69,6 +71,7 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.scene.shared.model.Overlays;
@@ -77,6 +80,8 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;

import kotlin.Unit;

import kotlinx.coroutines.Job;

import java.util.ArrayList;
@@ -105,6 +110,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
    private final Context mContext;
    // The Executor ensures actions and ui updates happen on the same thread.
    private final DelayableExecutor mExecutor;
    private final PowerInteractor mPowerInteractor;
    // A controller for the dream overlay container view (which contains both the status bar and the
    // content area).
    private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
@@ -230,6 +236,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
        }
    };

    private final Consumer<Unit> mPickupConsumer = new Consumer<>() {
        @Override
        public void accept(Unit unit) {
            mExecutor.execute(() ->
                    mPowerInteractor.wakeUpIfDreaming("pickupGesture",
                            PowerManager.WAKE_REASON_LIFT));
        }
    };

    /**
     * {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset
     * requests are processed before subsequent actions proceed. Requests themselves are also
@@ -398,6 +413,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
            DreamOverlayCallbackController dreamOverlayCallbackController,
            KeyguardInteractor keyguardInteractor,
            GestureInteractor gestureInteractor,
            WakeGestureMonitor wakeGestureMonitor,
            PowerInteractor powerInteractor,
            @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
        super(executor);
        mContext = context;
@@ -424,6 +441,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
        mTouchInsetManager = touchInsetManager;
        mLifecycleOwner = lifecycleOwner;
        mLifecycleRegistry = lifecycleOwner.getRegistry();
        mPowerInteractor = powerInteractor;

        mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));

@@ -438,6 +456,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
            mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
                    mBouncerShowingConsumer));
        }

        if (dreamsV2()) {
            mFlows.add(collectFlow(getLifecycle(), wakeGestureMonitor.getWakeUpDetected(),
                    mPickupConsumer));
        }
    }

    @NonNull
+74 −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.dreams

import android.hardware.Sensor
import android.hardware.display.AmbientDisplayConfiguration
import android.provider.Settings
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.kotlin.emitOnStart
import com.android.systemui.util.kotlin.observeTriggerSensor
import com.android.systemui.util.sensors.AsyncSensorManager
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map

@SysUISingleton
class WakeGestureMonitor
@Inject
constructor(
    private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
    private val asyncSensorManager: AsyncSensorManager,
    @Background bgContext: CoroutineContext,
    private val secureSettings: SecureSettings,
    selectedUserInteractor: SelectedUserInteractor,
) {

    private val pickupSensor by lazy {
        asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
    }

    private val pickupGestureEnabled: Flow<Boolean> =
        selectedUserInteractor.selectedUser.flatMapLatestConflated { userId ->
            isPickupEnabledForUser(userId)
        }

    private fun isPickupEnabledForUser(userId: Int): Flow<Boolean> =
        secureSettings
            .observerFlow(userId, Settings.Secure.DOZE_PICK_UP_GESTURE)
            .emitOnStart()
            .map { ambientDisplayConfiguration.pickupGestureEnabled(userId) }

    val wakeUpDetected: Flow<Unit> =
        pickupGestureEnabled
            .flatMapLatestConflated { enabled ->
                if (enabled && pickupSensor != null) {
                    asyncSensorManager.observeTriggerSensor(pickupSensor!!)
                } else {
                    emptyFlow()
                }
            }
            .flowOn(bgContext)
}
Loading