summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/AndroidManifest.xml11
-rw-r--r--packages/SystemUI/res/layout/low_light_clock_dream.xml39
-rw-r--r--packages/SystemUI/res/values/colors.xml4
-rw-r--r--packages/SystemUI/res/values/dimens.xml12
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java101
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt137
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java258
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java137
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java127
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java77
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java133
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java87
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java150
-rw-r--r--packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java161
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt110
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java226
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt102
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java110
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt75
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java160
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java143
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java183
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java108
30 files changed, 2832 insertions, 4 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 6491bf5c8f5b..72ae76a45cac 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1171,5 +1171,16 @@
android:exported="false"
/>
+ <service
+ android:name="com.google.android.systemui.lowlightclock.LowLightClockDreamService"
+ android:enabled="false"
+ android:exported="false"
+ android:directBootAware="true"
+ android:permission="android.permission.BIND_DREAM_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.dreams.DreamService" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/packages/SystemUI/res/layout/low_light_clock_dream.xml b/packages/SystemUI/res/layout/low_light_clock_dream.xml
new file mode 100644
index 000000000000..3d74a9fd8ae3
--- /dev/null
+++ b/packages/SystemUI/res/layout/low_light_clock_dream.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/low_light_clock_dream"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/low_light_clock_background_color">
+
+ <TextClock
+ android:id="@+id/low_light_text_clock"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/low_light_clock_text_size"
+ android:layout_gravity="center"
+ android:fontFamily="google-sans-clock"
+ android:gravity="center_horizontal"
+ android:textColor="@color/low_light_clock_text_color"
+ android:autoSizeTextType="uniform"
+ android:autoSizeMaxTextSize="@dimen/low_light_clock_text_size"
+ android:format12Hour="h:mm"
+ android:format24Hour="H:mm"/>
+
+ <TextView
+ android:id="@+id/charging_status_text_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/keyguard_indication_margin_bottom"
+ android:gravity="center"
+ android:minHeight="@dimen/low_light_clock_charging_text_min_height"
+ android:layout_gravity="center_horizontal|bottom"
+ android:paddingStart="@dimen/keyguard_indication_text_padding"
+ android:paddingEnd="@dimen/keyguard_indication_text_padding"
+ android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
+ android:textSize="@dimen/low_light_clock_charging_text_size"
+ android:textFontWeight="@integer/low_light_clock_charging_text_font_weight"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:accessibilityLiveRegion="polite" />
+ </FrameLayout>
+
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 36ede64f91d9..db8bbdb534a0 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -260,4 +260,8 @@
<!-- Rear Display Education -->
<color name="rear_display_overlay_animation_background_color">#1E1B17</color>
<color name="rear_display_overlay_dialog_background_color">#1E1B17</color>
+
+ <!-- Low light Dream -->
+ <color name="low_light_clock_background_color">#000000</color>
+ <color name="low_light_clock_text_color">#CCCCCC</color>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 724a12c12490..c7f037f3d619 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1614,6 +1614,18 @@
<!-- GLANCEABLE_HUB -> DREAMING transition: Amount to shift dream overlay on entering -->
<dimen name="hub_to_dreaming_transition_dream_overlay_translation_x">824dp</dimen>
+ <!-- Low light clock -->
+ <!-- The text size of the low light clock is intentionally defined in dp to avoid scaling -->
+ <dimen name="low_light_clock_text_size">260dp</dimen>
+ <dimen name="low_light_clock_charging_text_size">14sp</dimen>
+ <dimen name="low_light_clock_charging_text_min_height">48dp</dimen>
+ <integer name="low_light_clock_charging_text_font_weight">500</integer>
+
+ <dimen name="low_light_clock_translate_animation_offset">40dp</dimen>
+ <integer name="low_light_clock_translate_animation_duration_ms">1167</integer>
+ <integer name="low_light_clock_alpha_animation_in_start_delay_ms">233</integer>
+ <integer name="low_light_clock_alpha_animation_duration_ms">250</integer>
+
<!-- Distance that the full shade transition takes in order for media to fully transition to
the shade -->
<dimen name="lockscreen_shade_media_transition_distance">120dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
new file mode 100644
index 000000000000..2e1b5ad177b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
@@ -0,0 +1,101 @@
+/*
+ * 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.communal;
+
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import javax.inject.Inject;
+
+/**
+ * Condition which estimates device inactivity in order to avoid launching a full-screen activity
+ * while the user is actively using the device.
+ */
+public class DeviceInactiveCondition extends Condition {
+ private final KeyguardStateController mKeyguardStateController;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final KeyguardStateController.Callback mKeyguardStateCallback =
+ new KeyguardStateController.Callback() {
+ @Override
+ public void onKeyguardShowingChanged() {
+ updateState();
+ }
+ };
+ private final WakefulnessLifecycle.Observer mWakefulnessObserver =
+ new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onStartedGoingToSleep() {
+ updateState();
+ }
+ };
+ private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onDreamingStateChanged(boolean dreaming) {
+ updateState();
+ }
+ };
+
+ @Inject
+ public DeviceInactiveCondition(@Application CoroutineScope scope,
+ KeyguardStateController keyguardStateController,
+ WakefulnessLifecycle wakefulnessLifecycle,
+ KeyguardUpdateMonitor keyguardUpdateMonitor) {
+ super(scope);
+ mKeyguardStateController = keyguardStateController;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ }
+
+ @Override
+ protected void start() {
+ updateState();
+ mKeyguardStateController.addCallback(mKeyguardStateCallback);
+ mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+ }
+
+ @Override
+ protected void stop() {
+ mKeyguardStateController.removeCallback(mKeyguardStateCallback);
+ mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
+ mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
+ }
+
+ @Override
+ protected int getStartStrategy() {
+ return START_EAGERLY;
+ }
+
+ private void updateState() {
+ final boolean asleep =
+ mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
+ || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP;
+ updateCondition(asleep || mKeyguardStateController.isShowing()
+ || mKeyguardUpdateMonitor.isDreaming());
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index c02784dfab1b..fe5a82cb5b8c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -81,6 +81,7 @@ import com.android.systemui.keyguard.ui.composable.LockscreenContent;
import com.android.systemui.log.dagger.LogModule;
import com.android.systemui.log.dagger.MonitorLog;
import com.android.systemui.log.table.TableLogBuffer;
+import com.android.systemui.lowlightclock.dagger.LowLightModule;
import com.android.systemui.mediaprojection.MediaProjectionModule;
import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule;
import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
@@ -285,7 +286,8 @@ import javax.inject.Named;
UserModule.class,
UtilModule.class,
NoteTaskModule.class,
- WalletModule.class
+ WalletModule.class,
+ LowLightModule.class
},
subcomponents = {
ComplicationComponent.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index faab31eff4f7..15f73ee0eda6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -48,6 +48,8 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig;
import com.android.systemui.res.R;
import com.android.systemui.touch.TouchInsetManager;
+import com.google.android.systemui.lowlightclock.LowLightClockDreamService;
+
import dagger.Binds;
import dagger.BindsOptionalOf;
import dagger.Module;
@@ -238,15 +240,24 @@ public interface DreamModule {
ComponentName bindsLowLightClockDream();
/**
+ * Provides low light clock dream service component.
+ */
+ @Provides
+ @Named(LOW_LIGHT_CLOCK_DREAM)
+ static ComponentName providesLowLightClockDream(Context context) {
+ return new ComponentName(context, LowLightClockDreamService.class);
+ }
+
+ /**
* Provides the component name of the low light dream, or null if not configured.
*/
@Provides
@Nullable
@Named(LOW_LIGHT_DREAM_SERVICE)
static ComponentName providesLowLightDreamService(Context context,
- @Named(LOW_LIGHT_CLOCK_DREAM) Optional<ComponentName> clockDream) {
- if (Flags.lowLightClockDream() && clockDream.isPresent()) {
- return clockDream.get();
+ @Named(LOW_LIGHT_CLOCK_DREAM) ComponentName clockDream) {
+ if (Flags.lowLightClockDream()) {
+ return clockDream;
}
String lowLightDreamComponent = context.getResources().getString(
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
new file mode 100644
index 000000000000..ece97bd27df7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.annotation.IntDef
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+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.util.sensors.AsyncSensorManager
+import java.io.PrintWriter
+import java.util.Optional
+import javax.inject.Inject
+import javax.inject.Named
+
+/**
+ * Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient
+ * light mode.
+ *
+ * @property algorithm the debounce algorithm which transforms light sensor events into an ambient
+ * light mode.
+ * @property sensorManager the sensor manager used to register sensor event updates.
+ */
+class AmbientLightModeMonitor
+@Inject
+constructor(
+ private val algorithm: Optional<DebounceAlgorithm>,
+ private val sensorManager: AsyncSensorManager,
+ @Named(LIGHT_SENSOR) private val lightSensor: Optional<Sensor>,
+) : Dumpable {
+ companion object {
+ private const val TAG = "AmbientLightModeMonitor"
+ private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+ const val AMBIENT_LIGHT_MODE_LIGHT = 0
+ const val AMBIENT_LIGHT_MODE_DARK = 1
+ const val AMBIENT_LIGHT_MODE_UNDECIDED = 2
+ }
+
+ // Represents all ambient light modes.
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED)
+ annotation class AmbientLightMode
+
+ /**
+ * Start monitoring the current ambient light mode.
+ *
+ * @param callback callback that gets triggered when the ambient light mode changes.
+ */
+ fun start(callback: Callback) {
+ if (DEBUG) Log.d(TAG, "start monitoring ambient light mode")
+
+ if (lightSensor.isEmpty) {
+ if (DEBUG) Log.w(TAG, "light sensor not available")
+ return
+ }
+
+ if (algorithm.isEmpty) {
+ if (DEBUG) Log.w(TAG, "debounce algorithm not available")
+ return
+ }
+
+ algorithm.get().start(callback)
+ sensorManager.registerListener(
+ mSensorEventListener,
+ lightSensor.get(),
+ SensorManager.SENSOR_DELAY_NORMAL,
+ )
+ }
+
+ /** Stop monitoring the current ambient light mode. */
+ fun stop() {
+ if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode")
+
+ if (algorithm.isPresent) {
+ algorithm.get().stop()
+ }
+ sensorManager.unregisterListener(mSensorEventListener)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println()
+ pw.println("Ambient light mode monitor:")
+ pw.println(" lightSensor=$lightSensor")
+ pw.println()
+ }
+
+ private val mSensorEventListener: SensorEventListener =
+ object : SensorEventListener {
+ override fun onSensorChanged(event: SensorEvent) {
+ if (event.values.isEmpty()) {
+ if (DEBUG) Log.w(TAG, "SensorEvent doesn't have any value")
+ return
+ }
+
+ if (algorithm.isPresent) {
+ algorithm.get().onUpdateLightSensorEvent(event.values[0])
+ }
+ }
+
+ override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
+ // Do nothing.
+ }
+ }
+
+ /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */
+ interface Callback {
+ fun onChange(@AmbientLightMode mode: Int)
+ }
+
+ /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */
+ interface DebounceAlgorithm {
+ // Setting Callback to nullable so mockito can verify without throwing NullPointerException.
+ fun start(callback: Callback?)
+
+ fun stop()
+
+ fun onUpdateLightSensorEvent(value: Float)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java
new file mode 100644
index 000000000000..8cc399b0a22b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java
@@ -0,0 +1,258 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.os.BatteryManager;
+import android.os.RemoteException;
+import android.text.format.Formatter;
+import android.util.Log;
+
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.util.Preconditions;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.res.R;
+
+import java.text.NumberFormat;
+
+import javax.inject.Inject;
+
+/**
+ * Provides charging status as a string to a registered callback such that it can be displayed to
+ * the user (e.g. on the low-light clock).
+ * TODO(b/223681352): Make this code shareable with {@link KeyguardIndicationController}.
+ */
+public class ChargingStatusProvider {
+ private static final String TAG = "ChargingStatusProvider";
+
+ private final Resources mResources;
+ private final Context mContext;
+ private final IBatteryStats mBatteryInfo;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final BatteryState mBatteryState = new BatteryState();
+ // This callback is registered with KeyguardUpdateMonitor, which only keeps weak references to
+ // its callbacks. Therefore, an explicit reference needs to be kept here to avoid the
+ // callback being GC'd.
+ private ChargingStatusCallback mChargingStatusCallback;
+
+ private Callback mCallback;
+
+ @Inject
+ public ChargingStatusProvider(
+ Context context,
+ @Main Resources resources,
+ IBatteryStats iBatteryStats,
+ KeyguardUpdateMonitor keyguardUpdateMonitor) {
+ mContext = context;
+ mResources = resources;
+ mBatteryInfo = iBatteryStats;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ }
+
+ /**
+ * Start using the {@link ChargingStatusProvider}.
+ * @param callback A callback to be called when the charging status changes.
+ */
+ public void startUsing(Callback callback) {
+ Preconditions.checkState(
+ mCallback == null, "ChargingStatusProvider already started!");
+ mCallback = callback;
+ mChargingStatusCallback = new ChargingStatusCallback();
+ mKeyguardUpdateMonitor.registerCallback(mChargingStatusCallback);
+ reportStatusToCallback();
+ }
+
+ /**
+ * Stop using the {@link ChargingStatusProvider}.
+ */
+ public void stopUsing() {
+ mCallback = null;
+
+ if (mChargingStatusCallback != null) {
+ mKeyguardUpdateMonitor.removeCallback(mChargingStatusCallback);
+ mChargingStatusCallback = null;
+ }
+ }
+
+ private String computeChargingString() {
+ if (!mBatteryState.isValid()) {
+ return null;
+ }
+
+ int chargingId;
+
+ if (mBatteryState.isBatteryDefender()) {
+ return mResources.getString(
+ R.string.keyguard_plugged_in_charging_limited,
+ mBatteryState.getBatteryLevelAsPercentage());
+ } else if (mBatteryState.isPowerCharged()) {
+ return mResources.getString(R.string.keyguard_charged);
+ }
+
+ final long chargingTimeRemaining = mBatteryState.getChargingTimeRemaining(mBatteryInfo);
+ final boolean hasChargingTime = chargingTimeRemaining > 0;
+ if (mBatteryState.isPowerPluggedInWired()) {
+ switch (mBatteryState.getChargingSpeed(mContext)) {
+ case BatteryStatus.CHARGING_FAST:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_fast
+ : R.string.keyguard_plugged_in_charging_fast;
+ break;
+ case BatteryStatus.CHARGING_SLOWLY:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_slowly
+ : R.string.keyguard_plugged_in_charging_slowly;
+ break;
+ default:
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time
+ : R.string.keyguard_plugged_in;
+ break;
+ }
+ } else if (mBatteryState.isPowerPluggedInWireless()) {
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_wireless
+ : R.string.keyguard_plugged_in_wireless;
+ } else if (mBatteryState.isPowerPluggedInDocked()) {
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time_dock
+ : R.string.keyguard_plugged_in_dock;
+ } else {
+ chargingId = hasChargingTime
+ ? R.string.keyguard_indication_charging_time
+ : R.string.keyguard_plugged_in;
+ }
+
+ final String percentage = mBatteryState.getBatteryLevelAsPercentage();
+ if (hasChargingTime) {
+ final String chargingTimeFormatted =
+ Formatter.formatShortElapsedTimeRoundingUpToMinutes(
+ mContext, chargingTimeRemaining);
+ return mResources.getString(chargingId, chargingTimeFormatted,
+ percentage);
+ } else {
+ return mResources.getString(chargingId, percentage);
+ }
+ }
+
+ private void reportStatusToCallback() {
+ if (mCallback != null) {
+ final boolean shouldShowStatus =
+ mBatteryState.isPowerPluggedIn() || mBatteryState.isBatteryDefenderEnabled();
+ mCallback.onChargingStatusChanged(shouldShowStatus, computeChargingString());
+ }
+ }
+
+ private class ChargingStatusCallback extends KeyguardUpdateMonitorCallback {
+ @Override
+ public void onRefreshBatteryInfo(BatteryStatus status) {
+ mBatteryState.setBatteryStatus(status);
+ reportStatusToCallback();
+ }
+ }
+
+ /***
+ * A callback to be called when the charging status changes.
+ */
+ public interface Callback {
+ /***
+ * Called when the charging status changes.
+ * @param shouldShowStatus Whether or not to show a charging status message.
+ * @param statusMessage A charging status message.
+ */
+ void onChargingStatusChanged(boolean shouldShowStatus, String statusMessage);
+ }
+
+ /***
+ * A wrapper around {@link BatteryStatus} for fetching various properties of the current
+ * battery and charging state.
+ */
+ private static class BatteryState {
+ private BatteryStatus mBatteryStatus;
+
+ public void setBatteryStatus(BatteryStatus batteryStatus) {
+ mBatteryStatus = batteryStatus;
+ }
+
+ public boolean isValid() {
+ return mBatteryStatus != null;
+ }
+
+ public long getChargingTimeRemaining(IBatteryStats batteryInfo) {
+ try {
+ return isPowerPluggedIn() ? batteryInfo.computeChargeTimeRemaining() : -1;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling IBatteryStats: ", e);
+ return -1;
+ }
+ }
+
+ public boolean isBatteryDefenderEnabled() {
+ return isValid() && mBatteryStatus.isPluggedIn() && isBatteryDefender();
+ }
+
+ public boolean isBatteryDefender() {
+ return isValid() && mBatteryStatus.isBatteryDefender();
+ }
+
+ public int getBatteryLevel() {
+ return isValid() ? mBatteryStatus.level : 0;
+ }
+
+ public int getChargingSpeed(Context context) {
+ return isValid() ? mBatteryStatus.getChargingSpeed(context) : 0;
+ }
+
+ public boolean isPowerCharged() {
+ return isValid() && mBatteryStatus.isCharged();
+ }
+
+ public boolean isPowerPluggedIn() {
+ return isValid() && mBatteryStatus.isPluggedIn() && isChargingOrFull();
+ }
+
+ public boolean isPowerPluggedInWired() {
+ return isValid()
+ && mBatteryStatus.isPluggedInWired()
+ && isChargingOrFull();
+ }
+
+ public boolean isPowerPluggedInWireless() {
+ return isValid()
+ && mBatteryStatus.isPluggedInWireless()
+ && isChargingOrFull();
+ }
+
+ public boolean isPowerPluggedInDocked() {
+ return isValid() && mBatteryStatus.isPluggedInDock() && isChargingOrFull();
+ }
+
+ private boolean isChargingOrFull() {
+ return isValid()
+ && (mBatteryStatus.status == BatteryManager.BATTERY_STATUS_CHARGING
+ || mBatteryStatus.isCharged());
+ }
+
+ private String getBatteryLevelAsPercentage() {
+ return NumberFormat.getPercentInstance().format(getBatteryLevel() / 100f);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt
new file mode 100644
index 000000000000..4c1da0198498
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.Application
+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,
+ @Application 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 fun start() {
+ job = coroutineScope.launch { directBootFlow.collect { updateCondition(it) } }
+ updateCondition(!userManager.isUserUnlocked)
+ }
+
+ override fun stop() {
+ job?.cancel()
+ }
+
+ override fun getStartStrategy(): Int {
+ return START_EAGERLY
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java
new file mode 100644
index 000000000000..7f21d0707f63
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java
@@ -0,0 +1,137 @@
+/*
+ * 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.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.statusbar.commandline.Command;
+import com.android.systemui.statusbar.commandline.CommandRegistry;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * This condition registers for and fulfills cmd shell commands to force a device into or out of
+ * low-light conditions.
+ */
+public class ForceLowLightCondition extends Condition {
+ /**
+ * Command root
+ */
+ public static final String COMMAND_ROOT = "low-light";
+ /**
+ * Command for forcing device into low light.
+ */
+ public static final String COMMAND_ENABLE_LOW_LIGHT = "enable";
+
+ /**
+ * Command for preventing a device from entering low light.
+ */
+ public static final String COMMAND_DISABLE_LOW_LIGHT = "disable";
+
+ /**
+ * Command for clearing previously forced low-light conditions.
+ */
+ public static final String COMMAND_CLEAR_LOW_LIGHT = "clear";
+
+ private static final String TAG = "ForceLowLightCondition";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * Default Constructor.
+ *
+ * @param commandRegistry command registry to register commands with.
+ */
+ @Inject
+ public ForceLowLightCondition(
+ @Application CoroutineScope scope,
+ CommandRegistry commandRegistry
+ ) {
+ super(scope, null, true);
+
+ if (DEBUG) {
+ Log.d(TAG, "registering commands");
+ }
+ commandRegistry.registerCommand(COMMAND_ROOT, () -> new Command() {
+ @Override
+ public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) {
+ if (args.size() != 1) {
+ pw.println("no command specified");
+ help(pw);
+ return;
+ }
+
+ final String cmd = args.get(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
+ public void help(@NonNull PrintWriter pw) {
+ 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 void logAndPrint(PrintWriter pw, String message) {
+ pw.println(message);
+ if (DEBUG) {
+ Log.d(TAG, message);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void start() {
+ }
+
+ @Override
+ protected void stop() {
+ }
+
+ @Override
+ protected int getStartStrategy() {
+ return START_EAGERLY;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java
new file mode 100644
index 000000000000..6de599803a57
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java
@@ -0,0 +1,127 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.app.animation.Interpolators;
+import com.android.dream.lowlight.util.TruncatedInterpolator;
+import com.android.systemui.lowlightclock.dagger.LowLightModule;
+import com.android.systemui.statusbar.CrossFadeHelper;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/***
+ * A class that provides the animations used by the low-light clock.
+ *
+ * The entry and exit animations are opposites, with the only difference being a delay before the
+ * text fades in on entry.
+ */
+public class LowLightClockAnimationProvider {
+ private final int mYTranslationAnimationInStartOffset;
+ private final long mYTranslationAnimationInDurationMillis;
+ private final long mAlphaAnimationInStartDelayMillis;
+ private final long mAlphaAnimationDurationMillis;
+
+ /**
+ * Custom interpolator used for the translate out animation, which uses an emphasized easing
+ * like the translate in animation, but is scaled to match the length of the alpha animation.
+ */
+ private final Interpolator mTranslationOutInterpolator;
+
+ @Inject
+ public LowLightClockAnimationProvider(
+ @Named(LowLightModule.Y_TRANSLATION_ANIMATION_OFFSET)
+ int yTranslationAnimationInStartOffset,
+ @Named(LowLightModule.Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
+ long yTranslationAnimationInDurationMillis,
+ @Named(LowLightModule.ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
+ long alphaAnimationInStartDelayMillis,
+ @Named(LowLightModule.ALPHA_ANIMATION_DURATION_MILLIS)
+ long alphaAnimationDurationMillis) {
+ mYTranslationAnimationInStartOffset = yTranslationAnimationInStartOffset;
+ mYTranslationAnimationInDurationMillis = yTranslationAnimationInDurationMillis;
+ mAlphaAnimationInStartDelayMillis = alphaAnimationInStartDelayMillis;
+ mAlphaAnimationDurationMillis = alphaAnimationDurationMillis;
+
+ mTranslationOutInterpolator = new TruncatedInterpolator(Interpolators.EMPHASIZED,
+ /*originalDuration=*/ mYTranslationAnimationInDurationMillis,
+ /*newDuration=*/ mAlphaAnimationDurationMillis);
+ }
+
+ /***
+ * Provides an animation for when the given views become visible.
+ * @param views Any number of views to animate in together.
+ */
+ public Animator provideAnimationIn(View... views) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+
+ for (View view : views) {
+ if (view == null) continue;
+ // Set the alpha to 0 to start because the alpha animation has a start delay.
+ CrossFadeHelper.fadeOut(view, 0f, false);
+
+ final Animator alphaAnimator =
+ ObjectAnimator.ofFloat(view, View.ALPHA, 1f);
+ alphaAnimator.setStartDelay(mAlphaAnimationInStartDelayMillis);
+ alphaAnimator.setDuration(mAlphaAnimationDurationMillis);
+ alphaAnimator.setInterpolator(Interpolators.LINEAR);
+
+ final Animator positionAnimator = ObjectAnimator
+ .ofFloat(view, View.TRANSLATION_Y, mYTranslationAnimationInStartOffset, 0f);
+ positionAnimator.setDuration(mYTranslationAnimationInDurationMillis);
+ positionAnimator.setInterpolator(Interpolators.EMPHASIZED);
+
+ // The position animator must be started first since the alpha animator has a start
+ // delay.
+ animatorSet.playTogether(positionAnimator, alphaAnimator);
+ }
+
+ return animatorSet;
+ }
+
+ /***
+ * Provides an animation for when the given views are going out of view.
+ * @param views Any number of views to animate out.
+ */
+ public Animator provideAnimationOut(View... views) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+
+ for (View view : views) {
+ if (view == null) continue;
+ final Animator alphaAnimator =
+ ObjectAnimator.ofFloat(view, View.ALPHA, 0f);
+ alphaAnimator.setDuration(mAlphaAnimationDurationMillis);
+ alphaAnimator.setInterpolator(Interpolators.LINEAR);
+
+ final Animator positionAnimator = ObjectAnimator
+ .ofFloat(view, View.TRANSLATION_Y, mYTranslationAnimationInStartOffset);
+ // Use the same duration as the alpha animation plus our custom interpolator.
+ positionAnimator.setDuration(mAlphaAnimationDurationMillis);
+ positionAnimator.setInterpolator(mTranslationOutInterpolator);
+ animatorSet.playTogether(alphaAnimator, positionAnimator);
+ }
+
+ return animatorSet;
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java
new file mode 100644
index 000000000000..e91be5028777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.android.internal.logging.UiEventLogger;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.shared.condition.Condition;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import javax.inject.Inject;
+
+/**
+ * Condition for monitoring when the device enters and exits lowlight mode.
+ */
+public class LowLightCondition extends Condition {
+ private final AmbientLightModeMonitor mAmbientLightModeMonitor;
+ private final UiEventLogger mUiEventLogger;
+
+ @Inject
+ public LowLightCondition(@Application CoroutineScope scope,
+ AmbientLightModeMonitor ambientLightModeMonitor,
+ UiEventLogger uiEventLogger) {
+ super(scope);
+ mAmbientLightModeMonitor = ambientLightModeMonitor;
+ mUiEventLogger = uiEventLogger;
+ }
+
+ @Override
+ protected void start() {
+ mAmbientLightModeMonitor.start(this::onLowLightChanged);
+ }
+
+ @Override
+ protected void stop() {
+ mAmbientLightModeMonitor.stop();
+
+ // Reset condition met to false.
+ updateCondition(false);
+ }
+
+ @Override
+ protected int getStartStrategy() {
+ // As this condition keeps the lowlight sensor active, it should only run when needed.
+ return START_WHEN_NEEDED;
+ }
+
+ private void onLowLightChanged(int lowLightMode) {
+ if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) {
+ // Ignore undecided mode changes.
+ return;
+ }
+
+ final boolean isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK;
+ if (isLowLight == isConditionMet()) {
+ // No change in condition, don't do anything.
+ return;
+ }
+ mUiEventLogger.log(isLowLight ? LowLightDockEvent.AMBIENT_LIGHT_TO_DARK
+ : LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT);
+ updateCondition(isLowLight);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt
new file mode 100644
index 000000000000..9a9d813b18c5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+interface LowLightDisplayController {
+ fun isDisplayBrightnessModeSupported(): Boolean
+
+ fun setDisplayBrightnessModeEnabled(enabled: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt
new file mode 100644
index 000000000000..b99aeb6eeacc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+
+enum class LowLightDockEvent(private val id: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Ambient light changed from light to dark") AMBIENT_LIGHT_TO_DARK(999),
+ @UiEvent(doc = "The low light mode has started") LOW_LIGHT_STARTED(1000),
+ @UiEvent(doc = "Ambient light changed from dark to light") AMBIENT_LIGHT_TO_LIGHT(1001),
+ @UiEvent(doc = "The low light mode has stopped") LOW_LIGHT_STOPPED(1002);
+
+ override fun getId(): Int {
+ return id
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt
new file mode 100644
index 000000000000..11d75215edf5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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 com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.lowlightclock.dagger.LowLightLog
+import javax.inject.Inject
+
+/** Logs to a {@link LogBuffer} anything related to low-light features. */
+class LowLightLogger @Inject constructor(@LowLightLog private val buffer: LogBuffer) {
+ /** Logs a debug message to the buffer. */
+ fun d(tag: String, message: String) = buffer.log(tag, LogLevel.DEBUG, message)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
new file mode 100644
index 000000000000..912ace7675d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
@@ -0,0 +1,133 @@
+/*
+ * 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.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 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;
+
+ @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) {
+ super(conditionsMonitor);
+ mLowLightDreamManager = lowLightDreamManager;
+ mConditionsMonitor = conditionsMonitor;
+ mLowLightConditions = lowLightConditions;
+ mScreenLifecycle = screenLifecycle;
+ mLogger = lowLightLogger;
+ mLowLightDreamService = lowLightDreamService;
+ mPackageManager = packageManager;
+ }
+
+ @Override
+ public void onConditionsChanged(boolean allConditionsMet) {
+ mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
+
+ mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
+ ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
+ }
+
+ @Override
+ public void onScreenTurnedOn() {
+ 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() {
+ if (mSubscriptionToken != null) {
+ mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
+
+ mConditionsMonitor.removeSubscription(mSubscriptionToken);
+ mSubscriptionToken = null;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ 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();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java
new file mode 100644
index 000000000000..fd6ce1762a28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java
@@ -0,0 +1,87 @@
+/*
+ * 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.res.Resources;
+import android.database.ContentObserver;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.util.settings.SecureSettings;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import javax.inject.Inject;
+
+/**
+ * Condition for monitoring if the screensaver setting is enabled.
+ */
+public class ScreenSaverEnabledCondition extends Condition {
+ private static final String TAG = ScreenSaverEnabledCondition.class.getSimpleName();
+
+ private final boolean mScreenSaverEnabledByDefaultConfig;
+ private final SecureSettings mSecureSettings;
+
+ private final ContentObserver mScreenSaverSettingObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateScreenSaverEnabledSetting();
+ }
+ };
+
+ @Inject
+ public ScreenSaverEnabledCondition(@Application CoroutineScope scope, @Main Resources resources,
+ SecureSettings secureSettings) {
+ super(scope);
+ mScreenSaverEnabledByDefaultConfig = resources.getBoolean(
+ com.android.internal.R.bool.config_dreamsEnabledByDefault);
+ mSecureSettings = secureSettings;
+ }
+
+ @Override
+ protected void start() {
+ mSecureSettings.registerContentObserverForUserSync(
+ Settings.Secure.SCREENSAVER_ENABLED,
+ mScreenSaverSettingObserver, UserHandle.USER_CURRENT);
+ updateScreenSaverEnabledSetting();
+ }
+
+ @Override
+ protected void stop() {
+ mSecureSettings.unregisterContentObserverSync(mScreenSaverSettingObserver);
+ }
+
+ @Override
+ protected int getStartStrategy() {
+ return START_EAGERLY;
+ }
+
+ private void updateScreenSaverEnabledSetting() {
+ final boolean enabled = mSecureSettings.getIntForUser(
+ Settings.Secure.SCREENSAVER_ENABLED,
+ mScreenSaverEnabledByDefaultConfig ? 1 : 0,
+ UserHandle.USER_CURRENT) != 0;
+ if (!enabled) {
+ Log.i(TAG, "Disabling low-light clock because screen saver has been disabled");
+ }
+ updateCondition(enabled);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt
new file mode 100644
index 000000000000..0819664c921c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for logging related to low light features. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class LowLightLog
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
new file mode 100644
index 000000000000..c08be51c0699
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
@@ -0,0 +1,150 @@
+/*
+ * 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.dagger;
+
+import android.content.res.Resources;
+import android.hardware.Sensor;
+
+import com.android.dream.lowlight.dagger.LowLightDreamModule;
+import com.android.systemui.CoreStartable;
+import com.android.systemui.communal.DeviceInactiveCondition;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.LogBufferFactory;
+import com.android.systemui.lowlightclock.AmbientLightModeMonitor;
+import com.android.systemui.lowlightclock.DirectBootCondition;
+import com.android.systemui.lowlightclock.ForceLowLightCondition;
+import com.android.systemui.lowlightclock.LowLightCondition;
+import com.android.systemui.lowlightclock.LowLightDisplayController;
+import com.android.systemui.lowlightclock.LowLightMonitor;
+import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition;
+import com.android.systemui.res.R;
+import com.android.systemui.shared.condition.Condition;
+
+import dagger.Binds;
+import dagger.BindsOptionalOf;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+import dagger.multibindings.IntoSet;
+
+import javax.inject.Named;
+
+@Module(includes = LowLightDreamModule.class)
+public abstract class LowLightModule {
+ public static final String Y_TRANSLATION_ANIMATION_OFFSET =
+ "y_translation_animation_offset";
+ public static final String Y_TRANSLATION_ANIMATION_DURATION_MILLIS =
+ "y_translation_animation_duration_millis";
+ public static final String ALPHA_ANIMATION_IN_START_DELAY_MILLIS =
+ "alpha_animation_in_start_delay_millis";
+ public static final String ALPHA_ANIMATION_DURATION_MILLIS =
+ "alpha_animation_duration_millis";
+ public static final String LOW_LIGHT_PRECONDITIONS = "low_light_preconditions";
+ public static final String LIGHT_SENSOR = "low_light_monitor_light_sensor";
+
+
+ /**
+ * Provides a {@link LogBuffer} for logs related to low-light features.
+ */
+ @Provides
+ @SysUISingleton
+ @LowLightLog
+ public static LogBuffer provideLowLightLogBuffer(LogBufferFactory factory) {
+ return factory.create("LowLightLog", 250);
+ }
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract Condition bindScreenSaverEnabledCondition(ScreenSaverEnabledCondition condition);
+
+ @Provides
+ @IntoSet
+ @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS)
+ static Condition provideLowLightCondition(LowLightCondition lowLightCondition,
+ DirectBootCondition directBootCondition) {
+ // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
+ // conditions matters here since we don't want to start the lowlight condition if
+ // we are in direct boot mode.
+ return directBootCondition.or(lowLightCondition);
+ }
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract Condition bindForceLowLightCondition(ForceLowLightCondition condition);
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract Condition bindDeviceInactiveCondition(DeviceInactiveCondition condition);
+
+ @BindsOptionalOf
+ abstract LowLightDisplayController bindsLowLightDisplayController();
+
+ @BindsOptionalOf
+ @Named(LIGHT_SENSOR)
+ abstract Sensor bindsLightSensor();
+
+ @BindsOptionalOf
+ abstract AmbientLightModeMonitor.DebounceAlgorithm bindsDebounceAlgorithm();
+
+ /**
+ *
+ */
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_OFFSET)
+ static int providesAnimationInOffset(@Main Resources resources) {
+ return resources.getDimensionPixelOffset(
+ R.dimen.low_light_clock_translate_animation_offset);
+ }
+
+ /**
+ *
+ */
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
+ static long providesAnimationDurationMillis(@Main Resources resources) {
+ return resources.getInteger(R.integer.low_light_clock_translate_animation_duration_ms);
+ }
+
+ /**
+ *
+ */
+ @Provides
+ @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
+ static long providesAlphaAnimationInStartDelayMillis(@Main Resources resources) {
+ return resources.getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms);
+ }
+
+ /**
+ *
+ */
+ @Provides
+ @Named(ALPHA_ANIMATION_DURATION_MILLIS)
+ static long providesAlphaAnimationDurationMillis(@Main Resources resources) {
+ return resources.getInteger(R.integer.low_light_clock_alpha_animation_duration_ms);
+ }
+ /** Inject into LowLightMonitor. */
+ @Binds
+ @IntoMap
+ @ClassKey(LowLightMonitor.class)
+ abstract CoreStartable bindLowLightMonitor(LowLightMonitor lowLightMonitor);
+}
diff --git a/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java b/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java
new file mode 100644
index 000000000000..8a5f7eaf8776
--- /dev/null
+++ b/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java
@@ -0,0 +1,161 @@
+/*
+ * 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.google.android.systemui.lowlightclock;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.Nullable;
+import android.service.dreams.DreamService;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextClock;
+import android.widget.TextView;
+
+import com.android.dream.lowlight.LowLightTransitionCoordinator;
+import com.android.systemui.lowlightclock.ChargingStatusProvider;
+import com.android.systemui.lowlightclock.LowLightClockAnimationProvider;
+import com.android.systemui.lowlightclock.LowLightDisplayController;
+import com.android.systemui.res.R;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+/**
+ * A dark themed text clock dream to be shown when the device is in a low light environment.
+ */
+public class LowLightClockDreamService extends DreamService implements
+ LowLightTransitionCoordinator.LowLightExitListener {
+ private static final String TAG = "LowLightClockDreamService";
+
+ private final ChargingStatusProvider mChargingStatusProvider;
+ private final LowLightDisplayController mDisplayController;
+ private final LowLightClockAnimationProvider mAnimationProvider;
+ private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
+ private boolean mIsDimBrightnessSupported = false;
+
+ private TextView mChargingStatusTextView;
+ private TextClock mTextClock;
+ @Nullable
+ private Animator mAnimationIn;
+ @Nullable
+ private Animator mAnimationOut;
+
+ @Inject
+ public LowLightClockDreamService(
+ ChargingStatusProvider chargingStatusProvider,
+ LowLightClockAnimationProvider animationProvider,
+ LowLightTransitionCoordinator lowLightTransitionCoordinator,
+ Optional<Provider<LowLightDisplayController>> displayController) {
+ super();
+
+ mAnimationProvider = animationProvider;
+ mDisplayController = displayController.map(Provider::get).orElse(null);
+ mChargingStatusProvider = chargingStatusProvider;
+ mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ setInteractive(false);
+ setFullscreen(true);
+
+ setContentView(LayoutInflater.from(getApplicationContext()).inflate(
+ R.layout.low_light_clock_dream, null));
+
+ mTextClock = findViewById(R.id.low_light_text_clock);
+
+ mChargingStatusTextView = findViewById(R.id.charging_status_text_view);
+
+ mChargingStatusProvider.startUsing(this::updateChargingMessage);
+
+ mLowLightTransitionCoordinator.setLowLightExitListener(this);
+ }
+
+ @Override
+ public void onDreamingStarted() {
+ mAnimationIn = mAnimationProvider.provideAnimationIn(mTextClock, mChargingStatusTextView);
+ mAnimationIn.start();
+
+ if (mDisplayController != null) {
+ mIsDimBrightnessSupported = mDisplayController.isDisplayBrightnessModeSupported();
+
+ if (mIsDimBrightnessSupported) {
+ Log.v(TAG, "setting dim brightness state");
+ mDisplayController.setDisplayBrightnessModeEnabled(true);
+ } else {
+ Log.v(TAG, "dim brightness not supported");
+ }
+ }
+ }
+
+ @Override
+ public void onDreamingStopped() {
+ if (mIsDimBrightnessSupported) {
+ Log.v(TAG, "clearing dim brightness state");
+ mDisplayController.setDisplayBrightnessModeEnabled(false);
+ }
+ }
+
+ @Override
+ public void onWakeUp() {
+ if (mAnimationIn != null) {
+ mAnimationIn.cancel();
+ }
+ mAnimationOut = mAnimationProvider.provideAnimationOut(mTextClock, mChargingStatusTextView);
+ mAnimationOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ LowLightClockDreamService.super.onWakeUp();
+ }
+ });
+ mAnimationOut.start();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mAnimationOut != null) {
+ mAnimationOut.cancel();
+ }
+
+ mChargingStatusProvider.stopUsing();
+
+ mLowLightTransitionCoordinator.setLowLightExitListener(null);
+ }
+
+ private void updateChargingMessage(boolean showChargingStatus, String chargingStatusMessage) {
+ mChargingStatusTextView.setText(chargingStatusMessage);
+ mChargingStatusTextView.setVisibility(showChargingStatus ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ public Animator onBeforeExitLowLight() {
+ mAnimationOut = mAnimationProvider.provideAnimationOut(mTextClock, mChargingStatusTextView);
+ mAnimationOut.start();
+
+ // Return the animator so that the transition coordinator waits for the low light exit
+ // animations to finish before entering low light, as otherwise the default DreamActivity
+ // animation plays immediately and there's no time for this animation to play.
+ return mAnimationOut;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt
new file mode 100644
index 000000000000..43ee388e44a7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.hardware.Sensor
+import android.hardware.SensorEventListener
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.sensors.AsyncSensorManager
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AmbientLightModeMonitorTest : SysuiTestCase() {
+ @Mock private lateinit var sensorManager: AsyncSensorManager
+ @Mock private lateinit var sensor: Sensor
+ @Mock private lateinit var algorithm: AmbientLightModeMonitor.DebounceAlgorithm
+
+ private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ ambientLightModeMonitor =
+ AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.of(sensor))
+ }
+
+ @Test
+ fun shouldRegisterSensorEventListenerOnStart() {
+ val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+ ambientLightModeMonitor.start(callback)
+
+ verify(sensorManager).registerListener(any(), eq(sensor), anyInt())
+ }
+
+ @Test
+ fun shouldUnregisterSensorEventListenerOnStop() {
+ val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+ ambientLightModeMonitor.start(callback)
+
+ val sensorEventListener = captureSensorEventListener()
+
+ ambientLightModeMonitor.stop()
+
+ verify(sensorManager).unregisterListener(eq(sensorEventListener))
+ }
+
+ @Test
+ fun shouldStartDebounceAlgorithmOnStart() {
+ val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+ ambientLightModeMonitor.start(callback)
+
+ verify(algorithm).start(eq(callback))
+ }
+
+ @Test
+ fun shouldStopDebounceAlgorithmOnStop() {
+ val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+ ambientLightModeMonitor.start(callback)
+ ambientLightModeMonitor.stop()
+
+ verify(algorithm).stop()
+ }
+
+ @Test
+ fun shouldNotRegisterForSensorUpdatesIfSensorNotAvailable() {
+ val ambientLightModeMonitor =
+ AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.empty())
+
+ val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+ ambientLightModeMonitor.start(callback)
+
+ verify(sensorManager, never()).registerListener(any(), any(Sensor::class.java), anyInt())
+ }
+
+ // Captures [SensorEventListener], assuming it has been registered with [sensorManager].
+ private fun captureSensorEventListener(): SensorEventListener {
+ val captor = ArgumentCaptor.forClass(SensorEventListener::class.java)
+ verify(sensorManager).registerListener(captor.capture(), any(), anyInt())
+ return captor.value
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java
new file mode 100644
index 000000000000..2c8c1e1e70b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+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.res.Resources;
+import android.os.BatteryManager;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.app.IBatteryStats;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.res.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ChargingStatusProviderTest extends SysuiTestCase {
+ @Mock
+ private Resources mResources;
+ @Mock
+ private IBatteryStats mBatteryInfo;
+ @Mock
+ private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ @Mock
+ private ChargingStatusProvider.Callback mCallback;
+
+ private ChargingStatusProvider mProvider;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mProvider = new ChargingStatusProvider(
+ mContext, mResources, mBatteryInfo, mKeyguardUpdateMonitor);
+ }
+
+ @Test
+ public void testStartUsingReportsStatusToCallback() {
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ }
+
+ @Test
+ public void testStartUsingRegistersCallbackWithKeyguardUpdateMonitor() {
+ mProvider.startUsing(mCallback);
+ verify(mKeyguardUpdateMonitor).registerCallback(any());
+ }
+
+ @Test
+ public void testCallbackNotCalledAfterStopUsing() {
+ mProvider.startUsing(mCallback);
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ mProvider.stopUsing();
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getChargingBattery());
+ verify(mCallback, never()).onChargingStatusChanged(eq(true), any());
+ }
+
+ @Test
+ public void testKeyguardUpdateMonitorCallbackRemovedAfterStopUsing() {
+ mProvider.startUsing(mCallback);
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ mProvider.stopUsing();
+ verify(mKeyguardUpdateMonitor)
+ .removeCallback(keyguardUpdateMonitorCallbackArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void testChargingStatusReportsHideWhenNotPluggedIn() {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getUnpluggedBattery());
+ // Once for init() and once for the status change.
+ verify(mCallback, times(2)).onChargingStatusChanged(false, null);
+ }
+
+ @Test
+ public void testChargingStatusReportsShowWhenBatteryOverheated() {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getBatteryDefender());
+ verify(mCallback).onChargingStatusChanged(eq(true), any());
+ }
+
+ @Test
+ public void testChargingStatusReportsShowWhenPluggedIn() {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getChargingBattery());
+ verify(mCallback).onChargingStatusChanged(eq(true), any());
+ }
+
+ @Test
+ public void testChargingStatusReportsChargingLimitedWhenOverheated() {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getBatteryDefender());
+ verify(mResources).getString(eq(R.string.keyguard_plugged_in_charging_limited), any());
+ }
+
+ @Test
+ public void testChargingStatusReportsChargedWhenCharged() {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getChargedBattery());
+ verify(mResources).getString(R.string.keyguard_charged);
+ }
+
+ @Test
+ public void testChargingStatusReportsPluggedInWhenDockedAndChargingTimeUnknown() throws
+ RemoteException {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ when(mBatteryInfo.computeChargeTimeRemaining()).thenReturn(-1L);
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getChargingBattery());
+ verify(mResources).getString(
+ eq(R.string.keyguard_plugged_in_dock), any());
+ }
+
+ @Test
+ public void testChargingStatusReportsTimeRemainingWhenDockedAndCharging() throws
+ RemoteException {
+ ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ mProvider.startUsing(mCallback);
+ verify(mCallback).onChargingStatusChanged(false, null);
+ verify(mKeyguardUpdateMonitor)
+ .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture());
+ when(mBatteryInfo.computeChargeTimeRemaining()).thenReturn(1L);
+ keyguardUpdateMonitorCallbackArgumentCaptor.getValue()
+ .onRefreshBatteryInfo(getChargingBattery());
+ verify(mResources).getString(
+ eq(R.string.keyguard_indication_charging_time_dock), any(), any());
+ }
+
+ private BatteryStatus getUnpluggedBattery() {
+ return new BatteryStatus(BatteryManager.BATTERY_STATUS_NOT_CHARGING,
+ 80, BatteryManager.BATTERY_PLUGGED_ANY, BatteryManager.BATTERY_HEALTH_GOOD,
+ 0, true);
+ }
+
+ private BatteryStatus getChargingBattery() {
+ return new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING,
+ 80, BatteryManager.BATTERY_PLUGGED_DOCK,
+ BatteryManager.BATTERY_HEALTH_GOOD, 0, true);
+ }
+
+ private BatteryStatus getChargedBattery() {
+ return new BatteryStatus(BatteryManager.BATTERY_STATUS_FULL,
+ 100, BatteryManager.BATTERY_PLUGGED_DOCK,
+ BatteryManager.BATTERY_HEALTH_GOOD, 0, true);
+ }
+
+ private BatteryStatus getBatteryDefender() {
+ return new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING,
+ 80, BatteryManager.BATTERY_PLUGGED_DOCK,
+ BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE, 0, true);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt
new file mode 100644
index 000000000000..173f243cb2b0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.condition.Condition
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DirectBootConditionTest : SysuiTestCase() {
+ @Mock private lateinit var userManager: UserManager
+ @Mock private lateinit var callback: Condition.Callback
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun receiverRegisteredOnStart() = runTest {
+ val condition = buildCondition(this)
+ // No receivers are registered yet
+ assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0)
+ condition.addCallback(callback)
+ advanceUntilIdle()
+ // Receiver is registered after a callback is added
+ assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1)
+ condition.removeCallback(callback)
+ }
+
+ @Test
+ fun unregisterReceiverOnStop() = runTest {
+ val condition = buildCondition(this)
+
+ condition.addCallback(callback)
+ advanceUntilIdle()
+
+ assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1)
+
+ condition.removeCallback(callback)
+ advanceUntilIdle()
+
+ // Receiver is unregistered when nothing is listening to the condition
+ assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0)
+ }
+
+ @Test
+ fun callbackTriggeredWhenUserUnlocked() = runTest {
+ val condition = buildCondition(this)
+
+ setUserUnlocked(false)
+ condition.addCallback(callback)
+ advanceUntilIdle()
+
+ assertThat(condition.isConditionMet).isTrue()
+
+ setUserUnlocked(true)
+ advanceUntilIdle()
+
+ assertThat(condition.isConditionMet).isFalse()
+ condition.removeCallback(callback)
+ }
+
+ private fun buildCondition(scope: CoroutineScope): DirectBootCondition {
+ return DirectBootCondition(fakeBroadcastDispatcher, userManager, scope)
+ }
+
+ private fun setUserUnlocked(unlocked: Boolean) {
+ whenever(userManager.isUserUnlocked).thenReturn(unlocked)
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java
new file mode 100644
index 000000000000..7297e0f3bff5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.statusbar.commandline.Command;
+import com.android.systemui.statusbar.commandline.CommandRegistry;
+
+import kotlin.jvm.functions.Function0;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ForceLowLightConditionTest extends SysuiTestCase {
+ @Mock
+ private CommandRegistry mCommandRegistry;
+
+ @Mock
+ private Condition.Callback mCallback;
+
+ @Mock
+ private PrintWriter mPrintWriter;
+
+ @Mock
+ CoroutineScope mScope;
+
+ private ForceLowLightCondition mCondition;
+ private Command mCommand;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mCondition = new ForceLowLightCondition(mScope, mCommandRegistry);
+ mCondition.addCallback(mCallback);
+ ArgumentCaptor<Function0<Command>> commandCaptor =
+ ArgumentCaptor.forClass(Function0.class);
+ verify(mCommandRegistry).registerCommand(eq(ForceLowLightCondition.COMMAND_ROOT),
+ commandCaptor.capture());
+ mCommand = commandCaptor.getValue().invoke();
+ }
+
+ @Test
+ public void testEnableLowLight() {
+ mCommand.execute(mPrintWriter,
+ Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT));
+ verify(mCallback).onConditionChanged(mCondition);
+ assertThat(mCondition.isConditionSet()).isTrue();
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ @Test
+ public void testDisableLowLight() {
+ mCommand.execute(mPrintWriter,
+ Arrays.asList(ForceLowLightCondition.COMMAND_DISABLE_LOW_LIGHT));
+ verify(mCallback).onConditionChanged(mCondition);
+ assertThat(mCondition.isConditionSet()).isTrue();
+ assertThat(mCondition.isConditionMet()).isFalse();
+ }
+
+ @Test
+ public void testClearEnableLowLight() {
+ mCommand.execute(mPrintWriter,
+ Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT));
+ verify(mCallback).onConditionChanged(mCondition);
+ assertThat(mCondition.isConditionSet()).isTrue();
+ assertThat(mCondition.isConditionMet()).isTrue();
+ Mockito.clearInvocations(mCallback);
+ mCommand.execute(mPrintWriter,
+ Arrays.asList(ForceLowLightCondition.COMMAND_CLEAR_LOW_LIGHT));
+ verify(mCallback).onConditionChanged(mCondition);
+ assertThat(mCondition.isConditionSet()).isFalse();
+ assertThat(mCondition.isConditionMet()).isFalse();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt
new file mode 100644
index 000000000000..663880f098cd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.animation.Animator
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+class LowLightClockAnimationProviderTest : SysuiTestCase() {
+
+ private val underTest by lazy {
+ LowLightClockAnimationProvider(
+ Y_TRANSLATION_ANIMATION_OFFSET,
+ Y_TRANSLATION_ANIMATION_DURATION_MILLIS,
+ ALPHA_ANIMATION_IN_START_DELAY_MILLIS,
+ ALPHA_ANIMATION_DURATION_MILLIS,
+ )
+ }
+
+ @Test
+ fun animationOutEndsImmediatelyIfViewIsNull() {
+ val animator = underTest.provideAnimationOut(null, null)
+
+ val listener = mock<Animator.AnimatorListener>()
+ animator.addListener(listener)
+
+ animator.start()
+ verify(listener).onAnimationStart(any(), eq(false))
+ verify(listener).onAnimationEnd(any(), eq(false))
+ }
+
+ @Test
+ fun animationInEndsImmediatelyIfViewIsNull() {
+ val animator = underTest.provideAnimationIn(null, null)
+
+ val listener = mock<Animator.AnimatorListener>()
+ animator.addListener(listener)
+
+ animator.start()
+ verify(listener).onAnimationStart(any(), eq(false))
+ verify(listener).onAnimationEnd(any(), eq(false))
+ }
+
+ private companion object {
+ const val Y_TRANSLATION_ANIMATION_OFFSET = 100
+ const val Y_TRANSLATION_ANIMATION_DURATION_MILLIS = 100L
+ const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS = 200L
+ const val ALPHA_ANIMATION_DURATION_MILLIS = 300L
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java
new file mode 100644
index 000000000000..22a13cc41425
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.Animator;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.dream.lowlight.LowLightTransitionCoordinator;
+import com.android.systemui.SysuiTestCase;
+
+import com.google.android.systemui.lowlightclock.LowLightClockDreamService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class LowLightClockDreamServiceTest extends SysuiTestCase {
+ @Mock
+ private ChargingStatusProvider mChargingStatusProvider;
+ @Mock
+ private LowLightDisplayController mDisplayController;
+ @Mock
+ private LowLightClockAnimationProvider mAnimationProvider;
+ @Mock
+ private LowLightTransitionCoordinator mLowLightTransitionCoordinator;
+ @Mock
+ Animator mAnimationInAnimator;
+ @Mock
+ Animator mAnimationOutAnimator;
+
+ private LowLightClockDreamService mService;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mService = new LowLightClockDreamService(
+ mChargingStatusProvider,
+ mAnimationProvider,
+ mLowLightTransitionCoordinator,
+ Optional.of(() -> mDisplayController));
+
+ when(mAnimationProvider.provideAnimationIn(any(), any())).thenReturn(mAnimationInAnimator);
+ when(mAnimationProvider.provideAnimationOut(any())).thenReturn(
+ mAnimationOutAnimator);
+ }
+
+ @Test
+ public void testSetDbmStateWhenSupported() throws RemoteException {
+ when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(true);
+
+ mService.onDreamingStarted();
+
+ verify(mDisplayController).setDisplayBrightnessModeEnabled(true);
+ }
+
+ @Test
+ public void testNotSetDbmStateWhenNotSupported() throws RemoteException {
+ when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(false);
+
+ mService.onDreamingStarted();
+
+ verify(mDisplayController, never()).setDisplayBrightnessModeEnabled(anyBoolean());
+ }
+
+ @Test
+ public void testClearDbmState() throws RemoteException {
+ when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(true);
+
+ mService.onDreamingStarted();
+ clearInvocations(mDisplayController);
+
+ mService.onDreamingStopped();
+
+ verify(mDisplayController).setDisplayBrightnessModeEnabled(false);
+ }
+
+ @Test
+ public void testAnimationsStartedOnDreamingStarted() {
+ mService.onDreamingStarted();
+
+ // Entry animation started.
+ verify(mAnimationInAnimator).start();
+ }
+
+ @Test
+ public void testAnimationsStartedOnWakeUp() {
+ // Start dreaming then wake up.
+ mService.onDreamingStarted();
+ mService.onWakeUp();
+
+ // Entry animation started.
+ verify(mAnimationInAnimator).cancel();
+
+ // Exit animation started.
+ verify(mAnimationOutAnimator).start();
+ }
+
+ @Test
+ public void testAnimationsStartedBeforeExitingLowLight() {
+ mService.onBeforeExitLowLight();
+
+ // Exit animation started.
+ verify(mAnimationOutAnimator).start();
+ }
+
+ @Test
+ public void testWakeUpAnimationCancelledOnDetach() {
+ mService.onWakeUp();
+
+ // Exit animation started.
+ verify(mAnimationOutAnimator).start();
+
+ mService.onDetachedFromWindow();
+
+ verify(mAnimationOutAnimator).cancel();
+ }
+
+ @Test
+ public void testExitLowLightAnimationCancelledOnDetach() {
+ mService.onBeforeExitLowLight();
+
+ // Exit animation started.
+ verify(mAnimationOutAnimator).start();
+
+ mService.onDetachedFromWindow();
+
+ verify(mAnimationOutAnimator).cancel();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java
new file mode 100644
index 000000000000..2c216244985e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.SysuiTestCase;
+
+import kotlinx.coroutines.CoroutineScope;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class LowLightConditionTest extends SysuiTestCase {
+ @Mock
+ private AmbientLightModeMonitor mAmbientLightModeMonitor;
+ @Mock
+ private UiEventLogger mUiEventLogger;
+ @Mock
+ CoroutineScope mScope;
+ private LowLightCondition mCondition;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mCondition = new LowLightCondition(mScope, mAmbientLightModeMonitor, mUiEventLogger);
+ mCondition.start();
+ }
+
+ @Test
+ public void testLowLightFalse() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+ assertThat(mCondition.isConditionMet()).isFalse();
+ }
+
+ @Test
+ public void testLowLightTrue() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ @Test
+ public void testUndecidedLowLightStateIgnored() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ assertThat(mCondition.isConditionMet()).isTrue();
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED);
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ @Test
+ public void testLowLightChange() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+ assertThat(mCondition.isConditionMet()).isFalse();
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ @Test
+ public void testResetIsConditionMetUponStop() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ assertThat(mCondition.isConditionMet()).isTrue();
+
+ mCondition.stop();
+ assertThat(mCondition.isConditionMet()).isFalse();
+ }
+
+ @Test
+ public void testLoggingAmbientLightNotLowToLow() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ // Only logged once.
+ verify(mUiEventLogger, times(1)).log(any());
+ // Logged with the correct state.
+ verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_DARK);
+ }
+
+ @Test
+ public void testLoggingAmbientLightLowToLow() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ reset(mUiEventLogger);
+
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ // Doesn't log.
+ verify(mUiEventLogger, never()).log(any());
+ }
+
+ @Test
+ public void testLoggingAmbientLightNotLowToNotLow() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+ // Doesn't log.
+ verify(mUiEventLogger, never()).log(any());
+ }
+
+ @Test
+ public void testLoggingAmbientLightLowToNotLow() {
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
+ reset(mUiEventLogger);
+
+ changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+ // Only logged once.
+ verify(mUiEventLogger).log(any());
+ // Logged with the correct state.
+ verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT);
+ }
+
+ private void changeLowLightMode(int mode) {
+ ArgumentCaptor<AmbientLightModeMonitor.Callback> ambientLightCallbackCaptor =
+ ArgumentCaptor.forClass(AmbientLightModeMonitor.Callback.class);
+ verify(mAmbientLightModeMonitor).start(ambientLightCallbackCaptor.capture());
+ ambientLightCallbackCaptor.getValue().onChange(mode);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
new file mode 100644
index 000000000000..69485e848a6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.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.AndroidTestingRunner;
+
+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 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(AndroidTestingRunner.class)
+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;
+
+ 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);
+ }
+
+ @Test
+ public void testSetAmbientLowLightWhenInLowLight() {
+ mLowLightMonitor.onConditionsChanged(true);
+ // 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);
+ // Verify ambient light toggles back to light mode regular
+ verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
+ }
+
+ @Test
+ public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
+ mLowLightMonitor.onScreenTurnedOn();
+
+ // 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();
+ 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();
+ // 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();
+ Set<Condition> conditions = captureConditions();
+ // Verify Monitor is subscribed to the expected conditions
+ assertThat(conditions).isEqualTo(mConditionSet);
+ }
+
+ @Test
+ public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
+ mLowLightMonitor.onScreenTurnedOff();
+
+ // 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();
+ // 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);
+ when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
+ lowLightMonitor.start();
+ verify(mScreenLifecycle, never()).addObserver(any());
+ }
+
+ private Set<Condition> captureConditions() {
+ verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture());
+ return mPreconditionsSubscriptionCaptor.getValue().getConditions();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java
new file mode 100644
index 000000000000..366c071fb93f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.settings.SecureSettings;
+
+import kotlinx.coroutines.CoroutineScope;
+
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ScreenSaverEnabledConditionTest extends SysuiTestCase {
+ @Mock
+ private Resources mResources;
+ @Mock
+ private SecureSettings mSecureSettings;
+ @Mock
+ CoroutineScope mScope;
+ @Captor
+ private ArgumentCaptor<ContentObserver> mSettingsObserverCaptor;
+ private ScreenSaverEnabledCondition mCondition;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ // Default dreams to enabled by default
+ doReturn(true).when(mResources).getBoolean(
+ com.android.internal.R.bool.config_dreamsEnabledByDefault);
+
+ mCondition = new ScreenSaverEnabledCondition(mScope, mResources, mSecureSettings);
+ }
+
+ @Test
+ public void testScreenSaverInitiallyEnabled() {
+ setScreenSaverEnabled(true);
+ mCondition.start();
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ @Test
+ public void testScreenSaverInitiallyDisabled() {
+ setScreenSaverEnabled(false);
+ mCondition.start();
+ assertThat(mCondition.isConditionMet()).isFalse();
+ }
+
+ @Test
+ public void testScreenSaverStateChanges() {
+ setScreenSaverEnabled(false);
+ mCondition.start();
+ assertThat(mCondition.isConditionMet()).isFalse();
+
+ setScreenSaverEnabled(true);
+ final ContentObserver observer = captureSettingsObserver();
+ observer.onChange(/* selfChange= */ false);
+ assertThat(mCondition.isConditionMet()).isTrue();
+ }
+
+ private void setScreenSaverEnabled(boolean enabled) {
+ when(mSecureSettings.getIntForUser(eq(Settings.Secure.SCREENSAVER_ENABLED), anyInt(),
+ eq(UserHandle.USER_CURRENT))).thenReturn(enabled ? 1 : 0);
+ }
+
+ private ContentObserver captureSettingsObserver() {
+ verify(mSecureSettings).registerContentObserverForUserSync(
+ eq(Settings.Secure.SCREENSAVER_ENABLED),
+ mSettingsObserverCaptor.capture(), eq(UserHandle.USER_CURRENT));
+ return mSettingsObserverCaptor.getValue();
+ }
+}