diff options
| author | 2025-01-27 10:49:49 -0800 | |
|---|---|---|
| committer | 2025-01-27 10:49:49 -0800 | |
| commit | c62c11ac55ff114adecc2c86a42e96c16165c77f (patch) | |
| tree | 3843524e0d997a136c4b42286bda56afdbde1333 | |
| parent | df18c63b70e00417b916cf4b90030f522e2e9477 (diff) | |
| parent | 42950578e7068f31085929cd0357c73283f9bf59 (diff) | |
Merge "Introduce lowlightclock module." into main
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(); + } +} |