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

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

Merge changes Icf248616,I617530ac,I1e4ea387,If4a7b93b,Ib0f689d5 into main

* changes:
  Move direct boot logic out of condition monitor framework
  Move lowlight forcing logic out of condition monitor
  Move dream condition to LowLightMonitor
  Modernize LowLightMonitorTest to follow best practices
  Remove ScreenSaverEnabledCondition
parents 8196e171 31a8a537
Loading
Loading
Loading
Loading
+198 −145
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@
package com.android.systemui.lowlightclock

import android.content.ComponentName
import android.content.pm.PackageManager
import android.content.packageManager
import android.content.res.mainResources
import android.provider.Settings
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -24,248 +26,299 @@ import com.android.dream.lowlight.LowLightDreamManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.dreams.domain.interactor.dreamSettingsInteractorKosmos
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.shared.condition.Condition
import com.android.systemui.shared.condition.Monitor
import com.android.systemui.statusbar.commandline.commandRegistry
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
import dagger.Lazy
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
class LowLightMonitorTest : SysuiTestCase() {
    val kosmos = testKosmos().useUnconfinedTestDispatcher()
    val kosmos =
        testKosmos()
            .apply { mainResources = mContext.orCreateTestableResources.resources }
            .useUnconfinedTestDispatcher()

    @Mock private lateinit var lowLightDreamManagerLazy: Lazy<LowLightDreamManager>
    private val Kosmos.lowLightDreamManager: LowLightDreamManager by
        Kosmos.Fixture { mock<LowLightDreamManager>() }

    @Mock private lateinit var lowLightDreamManager: LowLightDreamManager
    private val Kosmos.monitor: Monitor by Kosmos.Fixture { Monitor(testDispatcher.asExecutor()) }

    private val monitor: Monitor = prepareMonitor()
    private val Kosmos.logger: LowLightLogger by
        Kosmos.Fixture { LowLightLogger(logcatLogBuffer()) }

    @Mock private lateinit var logger: LowLightLogger
    private val Kosmos.condition: FakeCondition by
        Kosmos.Fixture { FakeCondition(scope = applicationCoroutineScope, initialValue = null) }

    private lateinit var lowLightMonitor: LowLightMonitor

    @Mock private lateinit var lazyConditions: Lazy<Set<Condition>>

    @Mock private lateinit var packageManager: PackageManager

    @Mock private lateinit var dreamComponent: ComponentName
    private val Kosmos.underTest: LowLightMonitor by
        Kosmos.Fixture {
            LowLightMonitor(
                lowLightDreamManager = { lowLightDreamManager },
                conditionsMonitor = monitor,
                lowLightConditions = { setOf(condition) },
                dreamSettingsInteractor = dreamSettingsInteractorKosmos,
                displayStateInteractor = displayStateInteractor,
                logger = logger,
                lowLightDreamService = dreamComponent,
                packageManager = packageManager,
                scope = backgroundScope,
                commandRegistry = commandRegistry,
                userLockedInteractor = userLockedInteractor,
            )
        }

    private val condition = mock<Condition>()
    private var Kosmos.dreamComponent: ComponentName? by
        Kosmos.Fixture { ComponentName("test", "test.LowLightDream") }

    private val conditionSet = setOf(condition)
    private val Kosmos.printWriter: PrintWriter by Kosmos.Fixture { PrintWriter(StringWriter()) }

    @Captor
    private lateinit var preconditionsSubscriptionCaptor: ArgumentCaptor<Monitor.Subscription>
    private fun Kosmos.setDisplayOn(screenOn: Boolean) {
        displayRepository.setDefaultDisplayOff(!screenOn)
    }

    private fun prepareMonitor(): Monitor {
        val monitor = mock<Monitor>()
        whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
    private fun Kosmos.setDreamEnabled(enabled: Boolean) {
        fakeSettings.putBoolForUser(
            Settings.Secure.SCREENSAVER_ENABLED,
            enabled,
            selectedUserInteractor.getSelectedUserId(),
        )
    }

        return monitor
    private fun Kosmos.sendDebugCommand(enable: Boolean?) {
        val value: String =
            when (enable) {
                true -> "enable"
                false -> "disable"
                null -> "clear"
            }
        commandRegistry.onShellCommand(printWriter, arrayOf(LowLightMonitor.COMMAND_ROOT, value))
    }

    private fun setDisplayOn(screenOn: Boolean) {
        kosmos.displayRepository.setDefaultDisplayOff(!screenOn)
    private fun Kosmos.setUserUnlocked(unlocked: Boolean) {
        fakeUserRepository.setUserUnlocked(selectedUserInteractor.getSelectedUserId(), unlocked)
    }

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        whenever(lowLightDreamManagerLazy.get()).thenReturn(lowLightDreamManager)
        whenever(lazyConditions.get()).thenReturn(conditionSet)
        lowLightMonitor =
            LowLightMonitor(
                lowLightDreamManagerLazy,
                monitor,
                lazyConditions,
                kosmos.displayStateInteractor,
                logger,
                dreamComponent,
                packageManager,
                kosmos.testScope.backgroundScope,
            )
        whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
        val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()

        setDisplayOn(false)

        lowLightMonitor.start()
        verify(monitor).addSubscription(subscriptionCaptor.capture())
        clearInvocations(monitor)

        subscriptionCaptor.firstValue.callback.onConditionsChanged(true)
    }
        kosmos.setDisplayOn(false)
        kosmos.setUserUnlocked(true)

    private fun getConditionCallback(monitor: Monitor): Monitor.Callback {
        val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
        verify(monitor).addSubscription(subscriptionCaptor.capture())
        return subscriptionCaptor.firstValue.callback
        // Activate dreams on charge by default
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsEnabledByDefault,
            true,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
            true,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
            false,
        )
        mContext.orCreateTestableResources.addOverride(
            com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
            false,
        )
    }

    @Test
    fun testSetAmbientLowLightWhenInLowLight() =
        kosmos.runTest {
            underTest.start()

            // Turn on screen
            setDisplayOn(true)

            // Set conditions to true
            val callback = getConditionCallback(monitor)
            callback.onConditionsChanged(true)
            condition.setValue(true)

            // Verify setting low light when condition is true
            Mockito.verify(lowLightDreamManager)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
        }

    @Test
    fun testExitAmbientLowLightWhenNotInLowLight() =
        kosmos.runTest {
            underTest.start()

            // Turn on screen
            setDisplayOn(true)

            // Set conditions to true then false
            val callback = getConditionCallback(monitor)
            callback.onConditionsChanged(true)
            condition.setValue(true)
            clearInvocations(lowLightDreamManager)
            callback.onConditionsChanged(false)
            condition.setValue(false)

            // Verify ambient light toggles back to light mode regular
            Mockito.verify(lowLightDreamManager)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
        }

    @Test
    fun testStopMonitorLowLightConditionsWhenScreenTurnsOff() =
        kosmos.runTest {
            val token = mock<Monitor.Subscription.Token>()
            whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
            underTest.start()

            setDisplayOn(true)
            assertThat(condition.started).isTrue()

            // Verify removing subscription when screen turns off.
            setDisplayOn(false)
            Mockito.verify(monitor).removeSubscription(token)
            assertThat(condition.started).isFalse()
        }

    @Test
    fun testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() =
    fun testStopMonitorLowLightConditionsWhenDreamDisabled() =
        kosmos.runTest {
            val token = mock<Monitor.Subscription.Token>()
            whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
            underTest.start()

            setDisplayOn(true)
            setDisplayOn(true)
            // Verify subscription is only added once.
            Mockito.verify(monitor, Mockito.times(1)).addSubscription(ArgumentMatchers.any())
            setDreamEnabled(true)

            assertThat(condition.started).isTrue()

            setDreamEnabled(false)
            // Verify removing subscription when dream disabled.
            assertThat(condition.started).isFalse()
        }

    @Test
    fun testSubscribedToExpectedConditions() =
    fun testSubscribeIfScreenIsOnWhenStarting() =
        kosmos.runTest {
            val token = mock<Monitor.Subscription.Token>()
            whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)

            setDisplayOn(true)

            val conditions = captureConditions()
            // Verify Monitor is subscribed to the expected conditions
            Truth.assertThat(conditions).isEqualTo(conditionSet)
            underTest.start()
            assertThat(condition.started).isTrue()
        }

    @Test
    fun testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() =
    fun testNoSubscribeIfDreamNotPresent() =
        kosmos.runTest {
            setDisplayOn(true)
            clearInvocations(monitor)
            setDisplayOn(false)
            runCurrent()
            // Verify doesn't remove subscription since there is none.
            Mockito.verify(monitor).removeSubscription(ArgumentMatchers.any())
            dreamComponent = null

            underTest.start()
            assertThat(condition.started).isFalse()
        }

    @Test
    fun testSubscribeIfScreenIsOnWhenStarting() =
    fun testForceLowlightToTrue() =
        kosmos.runTest {
            val monitor = prepareMonitor()

            setDisplayOn(true)
            // low-light condition not met
            condition.setValue(false)

            val targetMonitor =
                LowLightMonitor(
                    lowLightDreamManagerLazy,
                    monitor,
                    lazyConditions,
                    displayStateInteractor,
                    logger,
                    dreamComponent,
                    packageManager,
                    testScope.backgroundScope,
                )

            // start
            targetMonitor.start()
            underTest.start()
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
            clearInvocations(lowLightDreamManager)

            val callback = getConditionCallback(monitor)
            clearInvocations(monitor)
            callback.onConditionsChanged(true)
            // force state to true
            sendDebugCommand(true)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
            clearInvocations(lowLightDreamManager)

            // Verify to add subscription on start and when the screen state is on
            Mockito.verify(monitor).addSubscription(ArgumentMatchers.any())
            // clear forced state
            sendDebugCommand(null)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
        }

    @Test
    fun testNoSubscribeIfDreamNotPresent() =
    fun testForceLowlightToFalse() =
        kosmos.runTest {
            val monitor = prepareMonitor()
            setDisplayOn(true)
            // low-light condition is met
            condition.setValue(true)

            underTest.start()
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
            clearInvocations(lowLightDreamManager)

            // force state to false
            sendDebugCommand(false)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
            clearInvocations(lowLightDreamManager)

            // clear forced state and ensure we go back to low-light
            sendDebugCommand(null)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
        }

    @Test
    fun testLowlightForcedToTrueWhenUserLocked() =
        kosmos.runTest {
            setDisplayOn(true)
            // low-light condition is false
            condition.setValue(false)

            val lowLightMonitor =
                LowLightMonitor(
                    lowLightDreamManagerLazy,
                    monitor,
                    lazyConditions,
                    displayStateInteractor,
                    logger,
                    null,
                    packageManager,
                    testScope,
                )
            underTest.start()
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
            clearInvocations(lowLightDreamManager)

            // locked user forces lowlight
            setUserUnlocked(false)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
            clearInvocations(lowLightDreamManager)

            // start
            lowLightMonitor.start()
            // clear forced state and ensure we go back to regular mode
            setUserUnlocked(true)
            verify(lowLightDreamManager)
                .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
        }

            val callback = getConditionCallback(monitor)
            clearInvocations(monitor)
            callback.onConditionsChanged(true)
    private class FakeCondition(
        scope: CoroutineScope,
        initialValue: Boolean?,
        overriding: Boolean = false,
        @StartStrategy override val startStrategy: Int = START_EAGERLY,
    ) : Condition(scope, initialValue, overriding) {
        private var _started = false
        val started: Boolean
            get() = _started

        override suspend fun start() {
            _started = true
        }

            // Verify to add subscription on start and when the screen state is on
            Mockito.verify(monitor, never()).addSubscription(ArgumentMatchers.any())
        override fun stop() {
            _started = false
        }

    private fun captureConditions(): Set<Condition?> {
        Mockito.verify(monitor).addSubscription(preconditionsSubscriptionCaptor.capture())
        return preconditionsSubscriptionCaptor.value.conditions
        fun setValue(value: Boolean?) {
            value?.also { updateCondition(value) } ?: clearCondition()
        }
    }
}
+4 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal.domain.suppression.dagger

import android.os.UserHandle
import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.communal.data.model.SuppressionReason
import com.android.systemui.communal.domain.interactor.CarProjectionInteractor
@@ -77,9 +78,9 @@ interface CommunalSuppressionModule {
        @Provides
        @IntoSet
        fun bindUserLockedSuppressor(interactor: UserLockedInteractor): Flow<SuppressionReason?> {
            return interactor.currentUserUnlocked.mapToReasonIfNotAllowed(
                SuppressionReason.ReasonUserLocked
            )
            return interactor
                .isUserUnlocked(UserHandle.CURRENT)
                .mapToReasonIfNotAllowed(SuppressionReason.ReasonUserLocked)
        }

        @Provides
+0 −59
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.lowlightclock

import android.content.Intent
import android.content.IntentFilter
import android.os.UserManager
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.shared.condition.Condition
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

class DirectBootCondition
@Inject
constructor(
    broadcastDispatcher: BroadcastDispatcher,
    private val userManager: UserManager,
    @Background private val coroutineScope: CoroutineScope,
) : Condition(coroutineScope) {
    private var job: Job? = null
    private val directBootFlow =
        broadcastDispatcher
            .broadcastFlow(IntentFilter(Intent.ACTION_USER_UNLOCKED))
            .map { !userManager.isUserUnlocked }
            .cancellable()
            .distinctUntilChanged()

    override suspend fun start() {
        job = coroutineScope.launch { directBootFlow.collect { updateCondition(it) } }
        updateCondition(!userManager.isUserUnlocked)
    }

    override fun stop() {
        job?.cancel()
    }

    override val startStrategy: Int
        get() = START_EAGERLY
}
+0 −115
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.lowlightclock

import android.text.TextUtils
import android.util.Log
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.shared.condition.Condition
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

/**
 * This condition registers for and fulfills cmd shell commands to force a device into or out of
 * low-light conditions.
 */
class ForceLowLightCondition
@Inject
constructor(@Background scope: CoroutineScope, commandRegistry: CommandRegistry) :
    Condition(scope, null, true) {
    /**
     * Default Constructor.
     *
     * @param commandRegistry command registry to register commands with.
     */
    init {
        if (DEBUG) {
            Log.d(TAG, "registering commands")
        }
        commandRegistry.registerCommand(COMMAND_ROOT) {
            object : Command {
                override fun execute(pw: PrintWriter, args: List<String>) {
                    if (args.size != 1) {
                        pw.println("no command specified")
                        help(pw)
                        return
                    }

                    val cmd = args[0]

                    if (TextUtils.equals(cmd, COMMAND_ENABLE_LOW_LIGHT)) {
                        logAndPrint(pw, "forcing low light")
                        updateCondition(true)
                    } else if (TextUtils.equals(cmd, COMMAND_DISABLE_LOW_LIGHT)) {
                        logAndPrint(pw, "forcing to not enter low light")
                        updateCondition(false)
                    } else if (TextUtils.equals(cmd, COMMAND_CLEAR_LOW_LIGHT)) {
                        logAndPrint(pw, "clearing any forced low light")
                        clearCondition()
                    } else {
                        pw.println("invalid command")
                        help(pw)
                    }
                }

                override fun help(pw: PrintWriter) {
                    pw.println("Usage: adb shell cmd statusbar low-light <cmd>")
                    pw.println("Supported commands:")
                    pw.println("  - enable")
                    pw.println("    forces device into low-light")
                    pw.println("  - disable")
                    pw.println("    forces device to not enter low-light")
                    pw.println("  - clear")
                    pw.println("    clears any previously forced state")
                }

                private fun logAndPrint(pw: PrintWriter, message: String) {
                    pw.println(message)
                    if (DEBUG) {
                        Log.d(TAG, message)
                    }
                }
            }
        }
    }

    override suspend fun start() {}

    override fun stop() {}

    override val startStrategy: Int
        get() = START_EAGERLY

    companion object {
        /** Command root */
        const val COMMAND_ROOT: String = "low-light"

        /** Command for forcing device into low light. */
        const val COMMAND_ENABLE_LOW_LIGHT: String = "enable"

        /** Command for preventing a device from entering low light. */
        const val COMMAND_DISABLE_LOW_LIGHT: String = "disable"

        /** Command for clearing previously forced low-light conditions. */
        const val COMMAND_CLEAR_LOW_LIGHT: String = "clear"

        private const val TAG = "ForceLowLightCondition"
        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
    }
}
+85 −6

File changed.

Preview size limit exceeded, changes collapsed.

Loading