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

Commit 68c80f9a authored by Bryce Lee's avatar Bryce Lee Committed by Android (Google) Code Review
Browse files

Merge changes Ie6b4dd45,Id10a59d6 into main

* changes:
  Listen to DisplayStateInteractor for LowLightMonitor display changes.
  Convert LowLightMonitor to kotlin.
parents cda83329 bdb510af
Loading
Loading
Loading
Loading
+0 −198
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 static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.testing.TestableLooper;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.dream.lowlight.LowLightDreamManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
import com.android.systemui.shared.condition.Monitor;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

import dagger.Lazy;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Set;

@SmallTest
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper()
public class LowLightMonitorTest extends SysuiTestCase {

    @Mock
    private Lazy<LowLightDreamManager> mLowLightDreamManagerLazy;
    @Mock
    private LowLightDreamManager mLowLightDreamManager;
    @Mock
    private Monitor mMonitor;
    @Mock
    private ScreenLifecycle mScreenLifecycle;
    @Mock
    private LowLightLogger mLogger;

    private LowLightMonitor mLowLightMonitor;

    @Mock
    Lazy<Set<Condition>> mLazyConditions;

    @Mock
    private PackageManager mPackageManager;

    @Mock
    private ComponentName mDreamComponent;

    FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());

    Condition mCondition = mock(Condition.class);
    Set<Condition> mConditionSet = Set.of(mCondition);

    @Captor
    ArgumentCaptor<Monitor.Subscription> mPreconditionsSubscriptionCaptor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mLowLightDreamManagerLazy.get()).thenReturn(mLowLightDreamManager);
        when(mLazyConditions.get()).thenReturn(mConditionSet);
        mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
            mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent,
                mPackageManager, mBackgroundExecutor);
    }

    @Test
    public void testSetAmbientLowLightWhenInLowLight() {
        mLowLightMonitor.onConditionsChanged(true);
        mBackgroundExecutor.runAllReady();
        // Verify setting low light when condition is true
        verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
    }

    @Test
    public void testExitAmbientLowLightWhenNotInLowLight() {
        mLowLightMonitor.onConditionsChanged(true);
        mLowLightMonitor.onConditionsChanged(false);
        mBackgroundExecutor.runAllReady();
        // Verify ambient light toggles back to light mode regular
        verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
    }

    @Test
    public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
        mLowLightMonitor.onScreenTurnedOn();
        mBackgroundExecutor.runAllReady();

        // Verify subscribing to low light conditions monitor when screen turns on.
        verify(mMonitor).addSubscription(any());
    }

    @Test
    public void testStopMonitorLowLightConditionsWhenScreenTurnsOff() {
        final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
        when(mMonitor.addSubscription(any())).thenReturn(token);
        mLowLightMonitor.onScreenTurnedOn();

        // Verify removing subscription when screen turns off.
        mLowLightMonitor.onScreenTurnedOff();
        mBackgroundExecutor.runAllReady();
        verify(mMonitor).removeSubscription(token);
    }

    @Test
    public void testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() {
        final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
        when(mMonitor.addSubscription(any())).thenReturn(token);

        mLowLightMonitor.onScreenTurnedOn();
        mLowLightMonitor.onScreenTurnedOn();
        mBackgroundExecutor.runAllReady();
        // Verify subscription is only added once.
        verify(mMonitor, times(1)).addSubscription(any());
    }

    @Test
    public void testSubscribedToExpectedConditions() {
        final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
        when(mMonitor.addSubscription(any())).thenReturn(token);

        mLowLightMonitor.onScreenTurnedOn();
        mLowLightMonitor.onScreenTurnedOn();
        mBackgroundExecutor.runAllReady();
        Set<Condition> conditions = captureConditions();
        // Verify Monitor is subscribed to the expected conditions
        assertThat(conditions).isEqualTo(mConditionSet);
    }

    @Test
    public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
        mLowLightMonitor.onScreenTurnedOff();
        mBackgroundExecutor.runAllReady();
        // Verify doesn't remove subscription since there is none.
        verify(mMonitor, never()).removeSubscription(any());
    }

    @Test
    public void testSubscribeIfScreenIsOnWhenStarting() {
        when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
        mLowLightMonitor.start();
        mBackgroundExecutor.runAllReady();
        // Verify to add subscription on start if the screen state is on
        verify(mMonitor, times(1)).addSubscription(any());
    }

    @Test
    public void testNoSubscribeIfDreamNotPresent() {
        LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
                mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager,
                mBackgroundExecutor);
        when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
        lowLightMonitor.start();
        mBackgroundExecutor.runAllReady();
        verify(mScreenLifecycle, never()).addObserver(any());
    }

    private Set<Condition> captureConditions() {
        verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture());
        return mPreconditionsSubscriptionCaptor.getValue().getConditions();
    }
}
+271 −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.lowlightclock

import android.content.ComponentName
import android.content.pm.PackageManager
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
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.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shared.condition.Condition
import com.android.systemui.shared.condition.Monitor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
import dagger.Lazy
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()

    @Mock private lateinit var lowLightDreamManagerLazy: Lazy<LowLightDreamManager>

    @Mock private lateinit var lowLightDreamManager: LowLightDreamManager

    private val monitor: Monitor = prepareMonitor()

    @Mock private lateinit var logger: LowLightLogger

    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 condition = mock<Condition>()

    private val conditionSet = setOf(condition)

    @Captor
    private lateinit var preconditionsSubscriptionCaptor: ArgumentCaptor<Monitor.Subscription>

    private fun prepareMonitor(): Monitor {
        val monitor = mock<Monitor>()
        whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())

        return monitor
    }

    private fun setDisplayOn(screenOn: Boolean) {
        kosmos.displayRepository.setDefaultDisplayOff(!screenOn)
    }

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

    private fun getConditionCallback(monitor: Monitor): Monitor.Callback {
        val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
        verify(monitor).addSubscription(subscriptionCaptor.capture())
        return subscriptionCaptor.firstValue.callback
    }

    @Test
    fun testSetAmbientLowLightWhenInLowLight() =
        kosmos.runTest {
            // Turn on screen
            setDisplayOn(true)

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

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

    @Test
    fun testExitAmbientLowLightWhenNotInLowLight() =
        kosmos.runTest {
            // Turn on screen
            setDisplayOn(true)

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

            // Verify ambient light toggles back to light mode regular
            Mockito.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)

            setDisplayOn(true)

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

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

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

    @Test
    fun testSubscribedToExpectedConditions() =
        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)
        }

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

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

            setDisplayOn(true)

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

            // start
            targetMonitor.start()

            val callback = getConditionCallback(monitor)
            clearInvocations(monitor)
            callback.onConditionsChanged(true)

            // Verify to add subscription on start and when the screen state is on
            Mockito.verify(monitor).addSubscription(ArgumentMatchers.any())
        }

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

            setDisplayOn(true)

            val lowLightMonitor =
                LowLightMonitor(
                    lowLightDreamManagerLazy,
                    monitor,
                    lazyConditions,
                    displayStateInteractor,
                    logger,
                    null,
                    packageManager,
                    testScope,
                )

            // start
            lowLightMonitor.start()

            val callback = getConditionCallback(monitor)
            clearInvocations(monitor)
            callback.onConditionsChanged(true)

            // Verify to add subscription on start and when the screen state is on
            Mockito.verify(monitor, never()).addSubscription(ArgumentMatchers.any())
        }

    private fun captureConditions(): Set<Condition?> {
        Mockito.verify(monitor).addSubscription(preconditionsSubscriptionCaptor.capture())
        return preconditionsSubscriptionCaptor.value.conditions
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.lowlightclock.dagger.LowLightModule.LIGHT_SENSOR
import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR
import com.android.systemui.util.sensors.AsyncSensorManager
import java.io.PrintWriter
import java.util.Optional
+0 −148
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 static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
import static com.android.systemui.dreams.dagger.DreamModule.LOW_LIGHT_DREAM_SERVICE;
import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
import static com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS;

import android.content.ComponentName;
import android.content.pm.PackageManager;

import androidx.annotation.Nullable;

import com.android.dream.lowlight.LowLightDreamManager;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.SystemUser;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
import com.android.systemui.shared.condition.Monitor;
import com.android.systemui.util.condition.ConditionalCoreStartable;

import dagger.Lazy;

import java.util.Set;
import java.util.concurrent.Executor;

import javax.inject.Inject;
import javax.inject.Named;

/**
 * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while
 * dreaming.
 */
public class LowLightMonitor extends ConditionalCoreStartable implements Monitor.Callback,
        ScreenLifecycle.Observer {
    private static final String TAG = "LowLightMonitor";

    private final Lazy<LowLightDreamManager> mLowLightDreamManager;
    private final Monitor mConditionsMonitor;
    private final Lazy<Set<Condition>> mLowLightConditions;
    private Monitor.Subscription.Token mSubscriptionToken;
    private ScreenLifecycle mScreenLifecycle;
    private final LowLightLogger mLogger;

    private final ComponentName mLowLightDreamService;

    private final PackageManager mPackageManager;

    private final Executor mExecutor;

    @Inject
    public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager,
            @SystemUser Monitor conditionsMonitor,
            @Named(LOW_LIGHT_PRECONDITIONS) Lazy<Set<Condition>> lowLightConditions,
            ScreenLifecycle screenLifecycle,
            LowLightLogger lowLightLogger,
            @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService,
            PackageManager packageManager,
            @Background Executor backgroundExecutor) {
        super(conditionsMonitor);
        mLowLightDreamManager = lowLightDreamManager;
        mConditionsMonitor = conditionsMonitor;
        mLowLightConditions = lowLightConditions;
        mScreenLifecycle = screenLifecycle;
        mLogger = lowLightLogger;
        mLowLightDreamService = lowLightDreamService;
        mPackageManager = packageManager;
        mExecutor = backgroundExecutor;
    }

    @Override
    public void onConditionsChanged(boolean allConditionsMet) {
        mExecutor.execute(() -> {
            mLogger.d(TAG, "Low light enabled: " + allConditionsMet);

            mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
                    ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
        });
    }

    @Override
    public void onScreenTurnedOn() {
        mExecutor.execute(() -> {
            if (mSubscriptionToken == null) {
                mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");

                mSubscriptionToken = mConditionsMonitor.addSubscription(
                        new Monitor.Subscription.Builder(this)
                                .addConditions(mLowLightConditions.get())
                                .build());
            }
        });
    }


    @Override
    public void onScreenTurnedOff() {
        mExecutor.execute(() -> {
            if (mSubscriptionToken != null) {
                mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");

                mConditionsMonitor.removeSubscription(mSubscriptionToken);
                mSubscriptionToken = null;
            }
        });
    }

    @Override
    protected void onStart() {
        mExecutor.execute(() -> {
            if (mLowLightDreamService != null) {
                // Note that the dream service is disabled by default. This prevents the dream from
                // appearing in settings on devices that don't have it explicitly excluded (done in
                // the settings overlay). Therefore, the component is enabled if it is to be used
                // here.
                mPackageManager.setComponentEnabledSetting(
                        mLowLightDreamService,
                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                        PackageManager.DONT_KILL_APP
                );
            } else {
                // If there is no low light dream service, do not observe conditions.
                return;
            }

            mScreenLifecycle.addObserver(this);
            if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
                onScreenTurnedOn();
            }
        });

    }
}
+116 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading