diff options
| -rw-r--r-- | api/current.txt | 6 | ||||
| -rw-r--r-- | api/system-lint-baseline.txt | 122 | ||||
| -rw-r--r-- | core/java/android/app/IUiModeManager.aidl | 22 | ||||
| -rw-r--r-- | core/java/android/app/UiModeManager.java | 91 | ||||
| -rw-r--r-- | core/java/android/provider/Settings.java | 24 | ||||
| -rw-r--r-- | core/java/android/util/TimeUtils.java | 23 | ||||
| -rw-r--r-- | core/proto/android/app/settings_enums.proto | 6 | ||||
| -rw-r--r-- | packages/SystemUI/res/values/strings.xml | 4 | ||||
| -rw-r--r-- | packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java | 19 | ||||
| -rw-r--r-- | services/core/java/com/android/server/UiModeManagerService.java | 274 | ||||
| -rw-r--r-- | services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java | 189 |
11 files changed, 696 insertions, 84 deletions
diff --git a/api/current.txt b/api/current.txt index 815bbf82fa71..276cd73ca638 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6470,7 +6470,11 @@ package android.app { method public void disableCarMode(int); method public void enableCarMode(int); method public int getCurrentModeType(); + method @NonNull public java.time.LocalTime getCustomNightModeEnd(); + method @NonNull public java.time.LocalTime getCustomNightModeStart(); method public int getNightMode(); + method public void setCustomNightModeEnd(@NonNull java.time.LocalTime); + method public void setCustomNightModeStart(@NonNull java.time.LocalTime); method public void setNightMode(int); field public static String ACTION_ENTER_CAR_MODE; field public static String ACTION_ENTER_DESK_MODE; @@ -6480,6 +6484,7 @@ package android.app { field public static final int ENABLE_CAR_MODE_ALLOW_SLEEP = 2; // 0x2 field public static final int ENABLE_CAR_MODE_GO_CAR_HOME = 1; // 0x1 field public static final int MODE_NIGHT_AUTO = 0; // 0x0 + field public static final int MODE_NIGHT_CUSTOM = 3; // 0x3 field public static final int MODE_NIGHT_NO = 1; // 0x1 field public static final int MODE_NIGHT_YES = 2; // 0x2 } @@ -50559,6 +50564,7 @@ package android.util { method public static java.util.TimeZone getTimeZone(int, boolean, long, String); method public static String getTimeZoneDatabaseVersion(); method @Nullable public static java.util.List<java.lang.String> getTimeZoneIdsForCountryCode(@NonNull String); + method public static boolean isTimeBetween(@NonNull java.time.LocalTime, @NonNull java.time.LocalTime, @NonNull java.time.LocalTime); } @Deprecated public class TimingLogger { diff --git a/api/system-lint-baseline.txt b/api/system-lint-baseline.txt index da0aae0f14ad..fde6bb3424f7 100644 --- a/api/system-lint-baseline.txt +++ b/api/system-lint-baseline.txt @@ -1,33 +1,48 @@ // Baseline format: 1.0 +AcronymName: android.net.NetworkCapabilities#setSSID(String): + Acronyms should not be capitalized in method names: was `setSSID`, should this be `setSsid`? + + ActionValue: android.location.Location#EXTRA_NO_GPS_LOCATION: ActionValue: android.net.wifi.WifiManager#ACTION_LINK_CONFIGURATION_CHANGED: - Inconsistent action value; expected `android.net.wifi.action.LINK_CONFIGURATION_CHANGED`, was `android.net.wifi.LINK_CONFIGURATION_CHANGED` + +ArrayReturn: android.bluetooth.BluetoothCodecStatus#BluetoothCodecStatus(android.bluetooth.BluetoothCodecConfig, android.bluetooth.BluetoothCodecConfig[], android.bluetooth.BluetoothCodecConfig[]) parameter #1: + Method parameter should be Collection<BluetoothCodecConfig> (or subclass) instead of raw array; was `android.bluetooth.BluetoothCodecConfig[]` +ArrayReturn: android.bluetooth.BluetoothCodecStatus#BluetoothCodecStatus(android.bluetooth.BluetoothCodecConfig, android.bluetooth.BluetoothCodecConfig[], android.bluetooth.BluetoothCodecConfig[]) parameter #2: + Method parameter should be Collection<BluetoothCodecConfig> (or subclass) instead of raw array; was `android.bluetooth.BluetoothCodecConfig[]` +ArrayReturn: android.bluetooth.BluetoothCodecStatus#getCodecsLocalCapabilities(): + Method should return Collection<BluetoothCodecConfig> (or subclass) instead of raw array; was `android.bluetooth.BluetoothCodecConfig[]` +ArrayReturn: android.bluetooth.BluetoothCodecStatus#getCodecsSelectableCapabilities(): + Method should return Collection<BluetoothCodecConfig> (or subclass) instead of raw array; was `android.bluetooth.BluetoothCodecConfig[]` +ArrayReturn: android.bluetooth.BluetoothUuid#containsAnyUuid(android.os.ParcelUuid[], android.os.ParcelUuid[]) parameter #0: + Method parameter should be Collection<ParcelUuid> (or subclass) instead of raw array; was `android.os.ParcelUuid[]` +ArrayReturn: android.bluetooth.BluetoothUuid#containsAnyUuid(android.os.ParcelUuid[], android.os.ParcelUuid[]) parameter #1: + Method parameter should be Collection<ParcelUuid> (or subclass) instead of raw array; was `android.os.ParcelUuid[]` +ArrayReturn: android.media.tv.tuner.Tuner.FilterCallback#onFilterEvent(android.media.tv.tuner.Tuner.Filter, android.media.tv.tuner.filter.FilterEvent[]) parameter #1: + Method parameter should be Collection<FilterEvent> (or subclass) instead of raw array; was `android.media.tv.tuner.filter.FilterEvent[]` +ArrayReturn: android.net.NetworkScoreManager#requestScores(android.net.NetworkKey[]) parameter #0: + Method parameter should be Collection<NetworkKey> (or subclass) instead of raw array; was `android.net.NetworkKey[]` ArrayReturn: android.view.contentcapture.ViewNode#getAutofillOptions(): ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#deletePersistentGroup(android.net.wifi.p2p.WifiP2pManager.Channel, int, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `deletePersistentGroup` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#factoryReset(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `factoryReset` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#listen(android.net.wifi.p2p.WifiP2pManager.Channel, boolean, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `listen` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#requestPersistentGroupInfo(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.WifiP2pManager.PersistentGroupInfoListener): - Registration methods should have overload that accepts delivery Executor: `requestPersistentGroupInfo` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#setDeviceName(android.net.wifi.p2p.WifiP2pManager.Channel, String, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `setDeviceName` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#setWfdInfo(android.net.wifi.p2p.WifiP2pManager.Channel, android.net.wifi.p2p.WifiP2pWfdInfo, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `setWfdInfo` + ExecutorRegistration: android.net.wifi.p2p.WifiP2pManager#setWifiP2pChannels(android.net.wifi.p2p.WifiP2pManager.Channel, int, int, android.net.wifi.p2p.WifiP2pManager.ActionListener): - Registration methods should have overload that accepts delivery Executor: `setWifiP2pChannels` - -HeavyBitSet: android.net.wifi.wificond.NativeScanResult#getCapabilities(): - Type must not be heavy BitSet (method android.net.wifi.wificond.NativeScanResult.getCapabilities()) -PairedRegistration: android.net.wifi.wificond.WifiCondManager#registerApCallback(String, java.util.concurrent.Executor, android.net.wifi.wificond.WifiCondManager.SoftApCallback): - Found registerApCallback but not unregisterApCallback in android.net.wifi.wificond.WifiCondManager + GenericException: android.app.prediction.AppPredictor#finalize(): @@ -40,13 +55,22 @@ GenericException: android.service.autofill.augmented.FillWindow#finalize(): +HeavyBitSet: android.net.wifi.wificond.NativeScanResult#getCapabilities(): + + +IntentBuilderName: android.content.Context#registerReceiverForAllUsers(android.content.BroadcastReceiver, android.content.IntentFilter, String, android.os.Handler): + Methods creating an Intent should be named `create<Foo>Intent()`, was `registerReceiverForAllUsers` KotlinKeyword: android.app.Notification#when: +KotlinOperator: android.telephony.CbGeoUtils.Geometry#contains(android.telephony.CbGeoUtils.LatLng): + Method can be invoked as a "in" operator from Kotlin: `contains` (this is usually desirable; just make sure it makes sense for this type of object) + + MissingNullability: android.hardware.soundtrigger.SoundTrigger.ModuleProperties#toString(): MissingNullability: android.hardware.soundtrigger.SoundTrigger.ModuleProperties#writeToParcel(android.os.Parcel, int) parameter #0: @@ -70,7 +94,7 @@ MissingNullability: android.media.tv.TvRecordingClient.RecordingCallback#onEvent MissingNullability: android.media.tv.TvRecordingClient.RecordingCallback#onEvent(String, String, android.os.Bundle) parameter #1: MissingNullability: android.media.tv.TvRecordingClient.RecordingCallback#onEvent(String, String, android.os.Bundle) parameter #2: - + MissingNullability: android.net.wifi.rtt.RangingRequest.Builder#addResponder(android.net.wifi.rtt.ResponderConfig): MissingNullability: android.printservice.recommendation.RecommendationService#attachBaseContext(android.content.Context) parameter #0: @@ -157,43 +181,60 @@ MissingNullability: android.telephony.mbms.DownloadRequest.Builder#setServiceId( - MutableBareField: android.net.IpConfiguration#httpProxy: - Bare field httpProxy must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.IpConfiguration#ipAssignment: - Bare field ipAssignment must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.IpConfiguration#proxySettings: - Bare field proxySettings must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.IpConfiguration#staticIpConfiguration: - Bare field staticIpConfiguration must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#allowAutojoin: MutableBareField: android.net.wifi.WifiConfiguration#apBand: - Bare field apBand must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#carrierId: MutableBareField: android.net.wifi.WifiConfiguration#fromWifiNetworkSpecifier: - Bare field fromWifiNetworkSpecifier must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#fromWifiNetworkSuggestion: - Bare field fromWifiNetworkSuggestion must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#macRandomizationSetting: - Bare field macRandomizationSetting must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#meteredOverride: - Bare field meteredOverride must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#requirePMF: - Bare field requirePMF must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#saePasswordId: - Bare field saePasswordId must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiConfiguration#shared: - Bare field shared must be marked final, or moved behind accessors if mutable + MutableBareField: android.net.wifi.WifiScanner.ScanSettings#type: - Bare field type must be marked final, or moved behind accessors if mutable + NoClone: android.service.contentcapture.ContentCaptureService#dump(java.io.FileDescriptor, java.io.PrintWriter, String[]) parameter #0: +NotCloseable: android.bluetooth.BluetoothA2dpSink: + Classes that release resources (finalize()) should implement AutoClosable and CloseGuard: class android.bluetooth.BluetoothA2dpSink +NotCloseable: android.bluetooth.BluetoothMap: + Classes that release resources (finalize()) should implement AutoClosable and CloseGuard: class android.bluetooth.BluetoothMap +NotCloseable: android.bluetooth.BluetoothPan: + Classes that release resources (finalize()) should implement AutoClosable and CloseGuard: class android.bluetooth.BluetoothPan +NotCloseable: android.bluetooth.BluetoothPbap: + Classes that release resources (finalize()) should implement AutoClosable and CloseGuard: class android.bluetooth.BluetoothPbap + + +OnNameExpected: android.content.ContentProvider#checkUriPermission(android.net.Uri, int, int): + If implemented by developer, should follow the on<Something> style; otherwise consider marking final + + +PairedRegistration: android.net.wifi.wificond.WifiCondManager#registerApCallback(String, java.util.concurrent.Executor, android.net.wifi.wificond.WifiCondManager.SoftApCallback): + + + ProtectedMember: android.printservice.recommendation.RecommendationService#attachBaseContext(android.content.Context): ProtectedMember: android.service.contentcapture.ContentCaptureService#dump(java.io.FileDescriptor, java.io.PrintWriter, String[]): @@ -201,6 +242,7 @@ ProtectedMember: android.service.contentcapture.ContentCaptureService#dump(java. ProtectedMember: android.service.notification.NotificationAssistantService#attachBaseContext(android.content.Context): + SamShouldBeLast: android.accounts.AccountManager#addAccount(String, String, String[], android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler): SamShouldBeLast: android.accounts.AccountManager#addOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener, android.os.Handler, boolean): @@ -246,9 +288,11 @@ SamShouldBeLast: android.app.AlarmManager#setExact(int, long, String, android.ap SamShouldBeLast: android.app.AlarmManager#setWindow(int, long, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler): SamShouldBeLast: android.app.WallpaperInfo#dump(android.util.Printer, String): - + +SamShouldBeLast: android.app.WallpaperManager#addOnColorsChangedListener(android.app.WallpaperManager.OnColorsChangedListener, android.os.Handler): + SAM-compatible parameters (such as parameter 1, "listener", in android.app.WallpaperManager.addOnColorsChangedListener) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.app.admin.DevicePolicyManager#installSystemUpdate(android.content.ComponentName, android.net.Uri, java.util.concurrent.Executor, android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback): - + SamShouldBeLast: android.content.Context#bindIsolatedService(android.content.Intent, int, String, java.util.concurrent.Executor, android.content.ServiceConnection): SamShouldBeLast: android.content.Context#bindService(android.content.Intent, int, java.util.concurrent.Executor, android.content.ServiceConnection): @@ -279,12 +323,20 @@ SamShouldBeLast: android.location.LocationManager#registerGnssNavigationMessageC SamShouldBeLast: android.location.LocationManager#registerGnssStatusCallback(java.util.concurrent.Executor, android.location.GnssStatus.Callback): +SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener, android.os.Looper): + SAM-compatible parameters (such as parameter 4, "listener", in android.location.LocationManager.requestLocationUpdates) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(String, long, float, java.util.concurrent.Executor, android.location.LocationListener): SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(android.location.LocationRequest, java.util.concurrent.Executor, android.location.LocationListener): +SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(long, float, android.location.Criteria, android.location.LocationListener, android.os.Looper): + SAM-compatible parameters (such as parameter 4, "listener", in android.location.LocationManager.requestLocationUpdates) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.location.LocationManager#requestLocationUpdates(long, float, android.location.Criteria, java.util.concurrent.Executor, android.location.LocationListener): +SamShouldBeLast: android.location.LocationManager#requestSingleUpdate(String, android.location.LocationListener, android.os.Looper): + SAM-compatible parameters (such as parameter 2, "listener", in android.location.LocationManager.requestSingleUpdate) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions +SamShouldBeLast: android.location.LocationManager#requestSingleUpdate(android.location.Criteria, android.location.LocationListener, android.os.Looper): + SAM-compatible parameters (such as parameter 2, "listener", in android.location.LocationManager.requestSingleUpdate) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.media.AudioFocusRequest.Builder#setOnAudioFocusChangeListener(android.media.AudioManager.OnAudioFocusChangeListener, android.os.Handler): SamShouldBeLast: android.media.AudioManager#requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, int, int): @@ -380,7 +432,7 @@ SamShouldBeLast: android.telephony.TelephonyManager#requestNetworkScan(android.t SamShouldBeLast: android.telephony.TelephonyManager#setPreferredOpportunisticDataSubscription(int, boolean, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>): SamShouldBeLast: android.telephony.TelephonyManager#updateAvailableNetworks(java.util.List<android.telephony.AvailableNetworkInfo>, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>): - + SamShouldBeLast: android.view.View#postDelayed(Runnable, long): SamShouldBeLast: android.view.View#postOnAnimationDelayed(Runnable, long): @@ -445,3 +497,11 @@ ServiceName: android.Manifest.permission#REQUEST_NOTIFICATION_ASSISTANT_SERVICE: ServiceName: android.provider.DeviceConfig#NAMESPACE_PACKAGE_MANAGER_SERVICE: + + +UserHandle: android.companion.CompanionDeviceManager#isDeviceAssociated(String, android.net.MacAddress, android.os.UserHandle): + When a method overload is needed to target a specific UserHandle, callers should be directed to use Context.createPackageContextAsUser() and re-obtain the relevant Manager, and no new API should be added + + +UserHandleName: android.telephony.CellBroadcastIntents#sendOrderedBroadcastForBackgroundReceivers(android.content.Context, android.os.UserHandle, android.content.Intent, String, String, android.content.BroadcastReceiver, android.os.Handler, int, String, android.os.Bundle): + Method taking UserHandle should be named `doFooAsUser` or `queryFooForUser`, was `sendOrderedBroadcastForBackgroundReceivers` diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index f5809ba627ff..41e2ec9e3572 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -70,7 +70,27 @@ interface IUiModeManager { boolean isNightModeLocked(); /** - * @hide + * [De]Activates night mode */ boolean setNightModeActivated(boolean active); + + /** + * Returns custom start clock time + */ + long getCustomNightModeStart(); + + /** + * Sets custom start clock time + */ + void setCustomNightModeStart(long time); + + /** + * Returns custom end clock time + */ + long getCustomNightModeEnd(); + + /** + * Sets custom end clock time + */ + void setCustomNightModeEnd(long time); } diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 363306483409..24873b86e32b 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -18,6 +18,7 @@ package android.app; import android.annotation.IntDef; import android.annotation.IntRange; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; @@ -32,6 +33,7 @@ import android.os.ServiceManager.ServiceNotFoundException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.time.LocalTime; /** * This class provides access to the system uimode services. These services @@ -163,6 +165,7 @@ public class UiModeManager { /** @hide */ @IntDef(prefix = { "MODE_" }, value = { MODE_NIGHT_AUTO, + MODE_NIGHT_CUSTOM, MODE_NIGHT_NO, MODE_NIGHT_YES }) @@ -173,19 +176,25 @@ public class UiModeManager { * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: * automatically switch night mode on and off based on the time. */ - public static final int MODE_NIGHT_AUTO = Configuration.UI_MODE_NIGHT_UNDEFINED >> 4; + public static final int MODE_NIGHT_AUTO = 0; + + /** + * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: + * automatically switch night mode on and off based on the time. + */ + public static final int MODE_NIGHT_CUSTOM = 3; /** * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: * never run in night mode. */ - public static final int MODE_NIGHT_NO = Configuration.UI_MODE_NIGHT_NO >> 4; + public static final int MODE_NIGHT_NO = 1; /** * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: * always run in night mode. */ - public static final int MODE_NIGHT_YES = Configuration.UI_MODE_NIGHT_YES >> 4; + public static final int MODE_NIGHT_YES = 2; private IUiModeManager mService; @@ -377,6 +386,8 @@ public class UiModeManager { * {@code notnight} mode</li> * <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into * {@code night} mode</li> + * <li><em>{@link #MODE_NIGHT_CUSTOM}</em> automatically switches between + * {@code night} and {@code notnight} based on the custom time set (or default)</li> * <li><em>{@link #MODE_NIGHT_AUTO}</em> automatically switches between * {@code night} and {@code notnight} based on the device's current * location and certain other sensors</li> @@ -418,6 +429,7 @@ public class UiModeManager { * <li>{@link #MODE_NIGHT_NO}</li> * <li>{@link #MODE_NIGHT_YES}</li> * <li>{@link #MODE_NIGHT_AUTO}</li> + * <li>{@link #MODE_NIGHT_CUSTOM}</li> * <li>{@code -1} on error</li> * </ul> * @@ -475,7 +487,7 @@ public class UiModeManager { } /** - * @hide* + * @hide */ public boolean setNightModeActivated(boolean active) { if (mService != null) { @@ -487,4 +499,75 @@ public class UiModeManager { } return false; } + + /** + * Returns the time of the day Dark theme activates + * <p> + * When night mode is {@link #MODE_NIGHT_CUSTOM}, the system uses + * this time set to activate it automatically. + */ + @NonNull + public LocalTime getCustomNightModeStart() { + if (mService != null) { + try { + return LocalTime.ofNanoOfDay(mService.getCustomNightModeStart() * 1000); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return LocalTime.MIDNIGHT; + } + + /** + * Sets the time of the day Dark theme activates + * <p> + * When night mode is {@link #MODE_NIGHT_CUSTOM}, the system uses + * this time set to activate it automatically + * @param time The time of the day Dark theme should activate + */ + public void setCustomNightModeStart(@NonNull LocalTime time) { + if (mService != null) { + try { + mService.setCustomNightModeStart(time.toNanoOfDay() / 1000); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns the time of the day Dark theme deactivates + * <p> + * When night mode is {@link #MODE_NIGHT_CUSTOM}, the system uses + * this time set to deactivate it automatically. + */ + @NonNull + public LocalTime getCustomNightModeEnd() { + if (mService != null) { + try { + return LocalTime.ofNanoOfDay(mService.getCustomNightModeEnd() * 1000); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return LocalTime.MIDNIGHT; + } + + /** + * Sets the time of the day Dark theme deactivates + * <p> + * When night mode is {@link #MODE_NIGHT_CUSTOM}, the system uses + * this time set to deactivate it automatically. + * @param time The time of the day Dark theme should deactivate + */ + public void setCustomNightModeEnd(@NonNull LocalTime time) { + if (mService != null) { + try { + mService.setCustomNightModeEnd(time.toNanoOfDay() / 1000); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index eee8fb13d3a6..0742a20a9fd6 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5883,6 +5883,22 @@ public final class Settings { "dark_mode_dialog_seen"; /** + * Custom time when Dark theme is scheduled to activate. + * Represented as milliseconds from midnight (e.g. 79200000 == 10pm). + * @hide + */ + public static final String DARK_THEME_CUSTOM_START_TIME = + "dark_theme_custom_start_time"; + + /** + * Custom time when Dark theme is scheduled to deactivate. + * Represented as milliseconds from midnight (e.g. 79200000 == 10pm). + * @hide + */ + public static final String DARK_THEME_CUSTOM_END_TIME = + "dark_theme_custom_end_time"; + + /** * Defines value returned by {@link android.service.autofill.UserData#getMaxUserDataSize()}. * * @hide @@ -7706,6 +7722,14 @@ public final class Settings { public static final String UI_NIGHT_MODE = "ui_night_mode"; /** + * The current night mode that has been overrided by the system. Owned + * and controlled by UiModeManagerService. Constants are as per + * UiModeManager. + * @hide + */ + public static final String UI_NIGHT_MODE_OVERRIDE = "ui_night_mode_override"; + + /** * Whether screensavers are enabled. * @hide */ diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 8439f5aad0b1..0558204af4c9 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -30,6 +30,7 @@ import libcore.timezone.ZoneInfoDB; import java.io.PrintWriter; import java.text.SimpleDateFormat; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -382,6 +383,28 @@ public class TimeUtils { } /** + * This method is used to find if a clock time is inclusively between two other clock times + * @param reference The time of the day we want check if it is between start and end + * @param start The start time reference + * @param end The end time + * @return true if the reference time is between the two clock times, and false otherwise. + */ + public static boolean isTimeBetween(@NonNull LocalTime reference, + @NonNull LocalTime start, + @NonNull LocalTime end) { + // ////////E----+-----S//////// + if ((reference.isBefore(start) && reference.isAfter(end) + // -----+----S//////////E------ + || (reference.isBefore(end) && reference.isBefore(start) && start.isBefore(end)) + // ---------S//////////E---+--- + || (reference.isAfter(end) && reference.isAfter(start)) && start.isBefore(end))) { + return false; + } else { + return true; + } + } + + /** * Dump a currentTimeMillis style timestamp for dumpsys, with the delta time from now. * * @hide diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto index a85c8f4e798b..ce03727314d1 100644 --- a/core/proto/android/app/settings_enums.proto +++ b/core/proto/android/app/settings_enums.proto @@ -2562,4 +2562,10 @@ enum PageId { // CATEGORY: SETTINGS // OS: R OPEN_SUPPORTED_LINKS = 1824; + + // OPEN: Settings > Display > Dark theme > Set start time dialog + DIALOG_DARK_THEME_SET_START_TIME = 1825; + + // OPEN: Settings > Display > Dark theme > Set end time dialog + DIALOG_DARK_THEME_SET_END_TIME = 1826; } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 91299386d68a..639005ba1c40 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -920,6 +920,10 @@ <string name="quick_settings_dark_mode_secondary_label_on_at_sunset">On at sunset</string> <!-- QuickSettings: Secondary text for when the Dark Mode will be on until sunrise. [CHAR LIMIT=20] --> <string name="quick_settings_dark_mode_secondary_label_until_sunrise">Until sunrise</string> + <!-- QuickSettings: Secondary text for when the Dark theme will be enabled at some user-selected time. [CHAR LIMIT=20] --> + <string name="quick_settings_dark_mode_secondary_label_on_at">On at <xliff:g id="time" example="10 pm">%s</xliff:g></string> + <!-- QuickSettings: Secondary text for when the Dark theme or some other tile will be on until some user-selected time. [CHAR LIMIT=20] --> + <string name="quick_settings_dark_mode_secondary_label_until">Until <xliff:g id="time" example="7 am">%s</xliff:g></string> <!-- QuickSettings: NFC tile [CHAR LIMIT=NONE] --> <string name="quick_settings_nfc_label">NFC</string> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index 7bc2a0d5c97d..8f1769b70d6c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -33,6 +33,8 @@ import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import javax.inject.Inject; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; /** * Quick Settings tile for: Night Mode / Dark Theme / Dark Mode. @@ -43,7 +45,7 @@ import javax.inject.Inject; public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements ConfigurationController.ConfigurationListener, BatteryController.BatteryStateChangeCallback { - + public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); private final Icon mIcon = ResourceIcon.get( com.android.internal.R.drawable.ic_qs_ui_mode_night); private final UiModeManager mUiModeManager; @@ -88,17 +90,28 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements protected void handleUpdateState(BooleanState state, Object arg) { int uiMode = mUiModeManager.getNightMode(); boolean powerSave = mBatteryController.isPowerSave(); - boolean isAuto = uiMode == UiModeManager.MODE_NIGHT_AUTO; boolean nightMode = (mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; if (powerSave) { state.secondaryLabel = mContext.getResources().getString( R.string.quick_settings_dark_mode_secondary_label_battery_saver); - } else if (isAuto) { + } else if (uiMode == UiModeManager.MODE_NIGHT_AUTO) { state.secondaryLabel = mContext.getResources().getString(nightMode ? R.string.quick_settings_dark_mode_secondary_label_until_sunrise : R.string.quick_settings_dark_mode_secondary_label_on_at_sunset); + } else if (uiMode == UiModeManager.MODE_NIGHT_CUSTOM) { + final boolean use24HourFormat = android.text.format.DateFormat.is24HourFormat(mContext); + final LocalTime time; + if (nightMode) { + time = mUiModeManager.getCustomNightModeEnd(); + } else { + time = mUiModeManager.getCustomNightModeStart(); + } + state.secondaryLabel = mContext.getResources().getString(nightMode + ? R.string.quick_settings_dark_mode_secondary_label_until + : R.string.quick_settings_dark_mode_secondary_label_on_at, + use24HourFormat ? time.toString() : formatter.format(time)); } else { state.secondaryLabel = null; } diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 9ffe89c61a44..b994e6c58e64 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.AlarmManager; import android.app.IUiModeManager; import android.app.Notification; import android.app.NotificationManager; @@ -70,10 +71,19 @@ import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; import java.util.Set; +import static android.app.UiModeManager.MODE_NIGHT_AUTO; +import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; +import static android.app.UiModeManager.MODE_NIGHT_YES; +import static android.util.TimeUtils.isTimeBetween; + final class UiModeManagerService extends SystemService { private static final String TAG = UiModeManager.class.getSimpleName(); private static final boolean LOG = false; @@ -90,7 +100,12 @@ final class UiModeManagerService extends SystemService { // we use the override auto mode // for example: force night mode off in the night time while in auto mode private int mNightModeOverride = mNightMode; - protected static final String OVERRIDE_NIGHT_MODE = Secure.UI_NIGHT_MODE + "_override"; + private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0); + private final LocalTime DEFAULT_CUSTOM_NIGHT_END_TIME = LocalTime.of(6, 0); + private LocalTime mCustomAutoNightModeStartMilliseconds = DEFAULT_CUSTOM_NIGHT_START_TIME; + private LocalTime mCustomAutoNightModeEndMilliseconds = DEFAULT_CUSTOM_NIGHT_END_TIME; + + protected static final String OVERRIDE_NIGHT_MODE = Secure.UI_NIGHT_MODE_OVERRIDE; private Map<Integer, String> mCarModePackagePriority = new HashMap<>(); private boolean mCarModeEnabled = false; @@ -131,6 +146,8 @@ final class UiModeManagerService extends SystemService { private NotificationManager mNotificationManager; private StatusBarManager mStatusBarManager; private WindowManagerInternal mWindowManager; + private AlarmManager mAlarmManager; + private PowerManager mPowerManager; private PowerManager.WakeLock mWakeLock; @@ -141,14 +158,16 @@ final class UiModeManagerService extends SystemService { } @VisibleForTesting - protected UiModeManagerService(Context context, WindowManagerInternal wm, - PowerManager.WakeLock wl, TwilightManager tm, + protected UiModeManagerService(Context context, WindowManagerInternal wm, AlarmManager am, + PowerManager pm, PowerManager.WakeLock wl, TwilightManager tm, boolean setupWizardComplete) { super(context); mWindowManager = wm; mWakeLock = wl; mTwilightManager = tm; mSetupWizardComplete = setupWizardComplete; + mAlarmManager = am; + mPowerManager = pm; } private static Intent buildHomeIntent(String category) { @@ -237,6 +256,21 @@ final class UiModeManagerService extends SystemService { } }; + private final BroadcastReceiver mOnTimeChangedHandler = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + updateCustomTimeLocked(); + } + } + }; + + private final AlarmManager.OnAlarmListener mCustomTimeListener = () -> { + synchronized (mLock) { + updateCustomTimeLocked(); + } + }; + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -270,8 +304,9 @@ final class UiModeManagerService extends SystemService { public void onChange(boolean selfChange, Uri uri) { int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE, mNightMode, 0); - mode = mode == UiModeManager.MODE_NIGHT_AUTO - ? UiModeManager.MODE_NIGHT_YES : UiModeManager.MODE_NIGHT_NO; + if (mode == MODE_NIGHT_AUTO || mode == MODE_NIGHT_CUSTOM) { + mode = MODE_NIGHT_YES; + } SystemProperties.set(SYSTEM_PROPERTY_DEVICE_THEME, Integer.toString(mode)); } }; @@ -287,10 +322,11 @@ final class UiModeManagerService extends SystemService { public void onStart() { final Context context = getContext(); - final PowerManager powerManager = + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); + mWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); mWindowManager = LocalServices.getService(WindowManagerInternal.class); + mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); // If setup isn't complete for this user listen for completion so we can unblock // being able to send a night mode configuration change event @@ -387,6 +423,16 @@ final class UiModeManagerService extends SystemService { Secure.USER_SETUP_COMPLETE, 0, UserHandle.getCallingUserId()) == 1; } + private void updateCustomTimeLocked() { + if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (shouldApplyAutomaticChangesImmediately()) { + updateLocked(0, 0); + } else { + registerScreenOffEvent(); + } + scheduleNextCustomTimeListener(); + } + /** * Updates the night mode setting in Settings.Global and returns if the value was successfully * changed. @@ -404,9 +450,19 @@ final class UiModeManagerService extends SystemService { Secure.UI_NIGHT_MODE, defaultNightMode, userId); mNightModeOverride = Secure.getIntForUser(context.getContentResolver(), OVERRIDE_NIGHT_MODE, defaultNightMode, userId); + mCustomAutoNightModeStartMilliseconds = LocalTime.ofNanoOfDay( + Secure.getLongForUser(context.getContentResolver(), + Secure.DARK_THEME_CUSTOM_START_TIME, + DEFAULT_CUSTOM_NIGHT_START_TIME.toNanoOfDay() / 1000L, userId) * 1000); + mCustomAutoNightModeEndMilliseconds = LocalTime.ofNanoOfDay( + Secure.getLongForUser(context.getContentResolver(), + Secure.DARK_THEME_CUSTOM_END_TIME, + DEFAULT_CUSTOM_NIGHT_END_TIME.toNanoOfDay() / 1000L, userId) * 1000); } else { mNightMode = defaultNightMode; mNightModeOverride = defaultNightMode; + mCustomAutoNightModeEndMilliseconds = DEFAULT_CUSTOM_NIGHT_END_TIME; + mCustomAutoNightModeStartMilliseconds = DEFAULT_CUSTOM_NIGHT_START_TIME; } return oldNightMode != mNightMode; @@ -419,6 +475,10 @@ final class UiModeManagerService extends SystemService { getContext().registerReceiver(mOnScreenOffHandler, intentFilter); } + private void cancelCustomAlarm() { + mAlarmManager.cancel(mCustomTimeListener); + } + private void unregisterScreenOffEvent() { mWaitForScreenOff = false; try { @@ -428,6 +488,21 @@ final class UiModeManagerService extends SystemService { } } + private void registerTimeChangeEvent() { + final IntentFilter intentFilter = + new IntentFilter(Intent.ACTION_TIME_CHANGED); + intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + getContext().registerReceiver(mOnTimeChangedHandler, intentFilter); + } + + private void unregisterTimeChangeEvent() { + try { + getContext().unregisterReceiver(mOnTimeChangedHandler); + } catch (IllegalArgumentException e) { + // we ignore this exception if the receiver is unregistered already. + } + } + private final IUiModeManager.Stub mService = new IUiModeManager.Stub() { @Override public void enableCarMode(@UiModeManager.EnableCarMode int flags, @@ -537,7 +612,8 @@ final class UiModeManagerService extends SystemService { switch (mode) { case UiModeManager.MODE_NIGHT_NO: case UiModeManager.MODE_NIGHT_YES: - case UiModeManager.MODE_NIGHT_AUTO: + case MODE_NIGHT_AUTO: + case MODE_NIGHT_CUSTOM: break; default: throw new IllegalArgumentException("Unknown mode: " + mode); @@ -548,8 +624,9 @@ final class UiModeManagerService extends SystemService { try { synchronized (mLock) { if (mNightMode != mode) { - if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { + if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { unregisterScreenOffEvent(); + cancelCustomAlarm(); } mNightMode = mode; @@ -559,7 +636,9 @@ final class UiModeManagerService extends SystemService { persistNightMode(user); } // on screen off will update configuration instead - if (mNightMode != UiModeManager.MODE_NIGHT_AUTO || mCar) { + if ((mNightMode != MODE_NIGHT_AUTO && mNightMode != MODE_NIGHT_CUSTOM) + || shouldApplyAutomaticChangesImmediately()) { + unregisterScreenOffEvent(); updateLocked(0, 0); } else { registerScreenOffEvent(); @@ -610,7 +689,7 @@ final class UiModeManagerService extends SystemService { final int user = UserHandle.getCallingUserId(); final long ident = Binder.clearCallingIdentity(); try { - if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { + if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) { unregisterScreenOffEvent(); mNightModeOverride = active ? UiModeManager.MODE_NIGHT_YES : UiModeManager.MODE_NIGHT_NO; @@ -630,8 +709,74 @@ final class UiModeManagerService extends SystemService { } } } + + @Override + public long getCustomNightModeStart() { + return mCustomAutoNightModeStartMilliseconds.toNanoOfDay() / 1000; + } + + @Override + public void setCustomNightModeStart(long time) { + if (isNightModeLocked() && getContext().checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + != PackageManager.PERMISSION_GRANTED) { + Slog.e(TAG, "Set custom time start, requires MODIFY_DAY_NIGHT_MODE permission"); + return; + } + final int user = UserHandle.getCallingUserId(); + final long ident = Binder.clearCallingIdentity(); + try { + LocalTime newTime = LocalTime.ofNanoOfDay(time * 1000); + if (newTime == null) return; + mCustomAutoNightModeStartMilliseconds = newTime; + persistNightMode(user); + onCustomTimeUpdated(user); + } catch (DateTimeException e) { + unregisterScreenOffEvent(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public long getCustomNightModeEnd() { + return mCustomAutoNightModeEndMilliseconds.toNanoOfDay() / 1000; + } + + @Override + public void setCustomNightModeEnd(long time) { + if (isNightModeLocked() && getContext().checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + != PackageManager.PERMISSION_GRANTED) { + Slog.e(TAG, "Set custom time end, requires MODIFY_DAY_NIGHT_MODE permission"); + return; + } + final int user = UserHandle.getCallingUserId(); + final long ident = Binder.clearCallingIdentity(); + try { + LocalTime newTime = LocalTime.ofNanoOfDay(time * 1000); + if (newTime == null) return; + mCustomAutoNightModeEndMilliseconds = newTime; + onCustomTimeUpdated(user); + } catch (DateTimeException e) { + unregisterScreenOffEvent(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } }; + private void onCustomTimeUpdated(int user) { + persistNightMode(user); + if (mNightMode != MODE_NIGHT_CUSTOM) return; + if (shouldApplyAutomaticChangesImmediately()) { + unregisterScreenOffEvent(); + updateLocked(0, 0); + } else { + registerScreenOffEvent(); + } + } + void dumpImpl(PrintWriter pw) { synchronized (mLock) { pw.println("Current UI Mode Service state:"); @@ -677,7 +822,6 @@ final class UiModeManagerService extends SystemService { mTwilightManager = getLocalService(TwilightManager.class); mSystemReady = true; mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR; - updateComputedNightModeLocked(); registerVrStateListener(); updateLocked(0, 0); } @@ -838,6 +982,12 @@ final class UiModeManagerService extends SystemService { Secure.UI_NIGHT_MODE, mNightMode, user); Secure.putIntForUser(getContext().getContentResolver(), OVERRIDE_NIGHT_MODE, mNightModeOverride, user); + Secure.putLongForUser(getContext().getContentResolver(), + Secure.DARK_THEME_CUSTOM_START_TIME, + mCustomAutoNightModeStartMilliseconds.toNanoOfDay() / 1000, user); + Secure.putLongForUser(getContext().getContentResolver(), + Secure.DARK_THEME_CUSTOM_END_TIME, + mCustomAutoNightModeEndMilliseconds.toNanoOfDay() / 1000, user); } private void updateConfigurationLocked() { @@ -856,13 +1006,16 @@ final class UiModeManagerService extends SystemService { uiMode = Configuration.UI_MODE_TYPE_VR_HEADSET; } - if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) { + if (mNightMode == MODE_NIGHT_AUTO) { + boolean activateNightMode = mComputedNightMode; if (mTwilightManager != null) { mTwilightManager.registerListener(mTwilightListener, mHandler); + final TwilightState lastState = mTwilightManager.getLastTwilightState(); + activateNightMode = lastState == null ? mComputedNightMode : lastState.isNight(); } - updateComputedNightModeLocked(); - uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES - : Configuration.UI_MODE_NIGHT_NO; + + updateComputedNightModeLocked(activateNightMode); + uiMode = getComputedUiModeConfiguration(uiMode); } else { if (mTwilightManager != null) { mTwilightManager.unregisterListener(mTwilightListener); @@ -870,6 +1023,16 @@ final class UiModeManagerService extends SystemService { uiMode |= mNightMode << 4; } + if (mNightMode == MODE_NIGHT_CUSTOM) { + registerTimeChangeEvent(); + final boolean activate = computeCustomNightMode(); + updateComputedNightModeLocked(activate); + scheduleNextCustomTimeListener(); + uiMode = getComputedUiModeConfiguration(uiMode); + } else { + unregisterTimeChangeEvent(); + } + // Override night mode in power save mode if not in car mode if (mPowerSave && !mCarModeEnabled) { uiMode &= ~Configuration.UI_MODE_NIGHT_NO; @@ -885,11 +1048,26 @@ final class UiModeManagerService extends SystemService { } mCurUiMode = uiMode; - if (!mHoldingConfiguration || !mWaitForScreenOff) { + if (!mHoldingConfiguration && !mWaitForScreenOff) { mConfiguration.uiMode = uiMode; } } + @UiModeManager.NightMode + private int getComputedUiModeConfiguration(@UiModeManager.NightMode int uiMode) { + uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES + : Configuration.UI_MODE_NIGHT_NO; + uiMode &= mComputedNightMode ? ~Configuration.UI_MODE_NIGHT_NO + : ~Configuration.UI_MODE_NIGHT_YES; + return uiMode; + } + + private boolean computeCustomNightMode() { + return isTimeBetween(LocalTime.now(), + mCustomAutoNightModeStartMilliseconds, + mCustomAutoNightModeEndMilliseconds); + } + private void applyConfigurationExternallyLocked() { if (mSetUiMode != mConfiguration.uiMode) { mSetUiMode = mConfiguration.uiMode; @@ -899,10 +1077,34 @@ final class UiModeManagerService extends SystemService { ActivityTaskManager.getService().updateConfiguration(mConfiguration); } catch (RemoteException e) { Slog.w(TAG, "Failure communicating with activity manager", e); + } catch (SecurityException e) { + Slog.e(TAG, "Activity does not have the ", e); } } } + private boolean shouldApplyAutomaticChangesImmediately() { + return mCar || !mPowerManager.isInteractive(); + } + + private void scheduleNextCustomTimeListener() { + cancelCustomAlarm(); + LocalDateTime now = LocalDateTime.now(); + final boolean active = computeCustomNightMode(); + final LocalDateTime next = active + ? getDateTimeAfter(mCustomAutoNightModeEndMilliseconds, now) + : getDateTimeAfter(mCustomAutoNightModeStartMilliseconds, now); + final long millis = next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + mAlarmManager.setExact(AlarmManager.RTC, millis, TAG, mCustomTimeListener, null); + } + + private LocalDateTime getDateTimeAfter(LocalTime localTime, LocalDateTime compareTime) { + final LocalDateTime ldt = LocalDateTime.of(compareTime.toLocalDate(), localTime); + + // Check if the local time has passed, if so return the same time tomorrow. + return ldt.isBefore(compareTime) ? ldt.plusDays(1) : ldt; + } + void updateLocked(int enableFlags, int disableFlags) { String action = null; String oldAction = null; @@ -1133,26 +1335,21 @@ final class UiModeManagerService extends SystemService { } } - private void updateComputedNightModeLocked() { - if (mTwilightManager != null) { - TwilightState state = mTwilightManager.getLastTwilightState(); - if (state != null) { - mComputedNightMode = state.isNight(); - } - if (mNightModeOverride == UiModeManager.MODE_NIGHT_YES && !mComputedNightMode) { - mComputedNightMode = true; - return; - } - if (mNightModeOverride == UiModeManager.MODE_NIGHT_NO && mComputedNightMode) { - mComputedNightMode = false; - return; - } - - mNightModeOverride = mNightMode; - final int user = UserHandle.getCallingUserId(); - Secure.putIntForUser(getContext().getContentResolver(), - OVERRIDE_NIGHT_MODE, mNightModeOverride, user); + private void updateComputedNightModeLocked(boolean activate) { + mComputedNightMode = activate; + if (mNightModeOverride == UiModeManager.MODE_NIGHT_YES && !mComputedNightMode) { + mComputedNightMode = true; + return; + } + if (mNightModeOverride == UiModeManager.MODE_NIGHT_NO && mComputedNightMode) { + mComputedNightMode = false; + return; } + + mNightModeOverride = mNightMode; + final int user = UserHandle.getCallingUserId(); + Secure.putIntForUser(getContext().getContentResolver(), + OVERRIDE_NIGHT_MODE, mNightModeOverride, user); } private void registerVrStateListener() { @@ -1174,6 +1371,7 @@ final class UiModeManagerService extends SystemService { public static final String NIGHT_MODE_STR_YES = "yes"; public static final String NIGHT_MODE_STR_NO = "no"; public static final String NIGHT_MODE_STR_AUTO = "auto"; + public static final String NIGHT_MODE_STR_CUSTOM = "custom"; public static final String NIGHT_MODE_STR_UNKNOWN = "unknown"; private final IUiModeManager mInterface; @@ -1246,6 +1444,8 @@ final class UiModeManagerService extends SystemService { return NIGHT_MODE_STR_NO; case UiModeManager.MODE_NIGHT_AUTO: return NIGHT_MODE_STR_AUTO; + case MODE_NIGHT_CUSTOM: + return NIGHT_MODE_STR_CUSTOM; default: return NIGHT_MODE_STR_UNKNOWN; } @@ -1259,6 +1459,8 @@ final class UiModeManagerService extends SystemService { return UiModeManager.MODE_NIGHT_NO; case NIGHT_MODE_STR_AUTO: return UiModeManager.MODE_NIGHT_AUTO; + case NIGHT_MODE_STR_CUSTOM: + return UiModeManager.MODE_NIGHT_CUSTOM; default: return -1; } diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 03c10f3a86f8..22046a51d059 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -16,38 +16,54 @@ package com.android.server; +import android.app.AlarmManager; import android.app.IUiModeManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import com.android.server.twilight.TwilightManager; +import com.android.server.twilight.TwilightState; import com.android.server.wm.WindowManagerInternal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import java.util.HashSet; -import java.util.Set; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import static android.app.UiModeManager.MODE_NIGHT_AUTO; +import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_NO; import static android.app.UiModeManager.MODE_NIGHT_YES; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -66,22 +82,51 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { TwilightManager mTwilightManager; @Mock PowerManager.WakeLock mWakeLock; - private Set<BroadcastReceiver> mScreenOffRecievers; + @Mock + AlarmManager mAlarmManager; + @Mock + PowerManager mPowerManager; + @Mock + TwilightState mTwilightState; + + private BroadcastReceiver mScreenOffCallback; + private BroadcastReceiver mTimeChangedCallback; + private AlarmManager.OnAlarmListener mCustomListener; @Before public void setUp() { - mUiManagerService = new UiModeManagerService(mContext, mWindowManager, mWakeLock, - mTwilightManager, true); - mScreenOffRecievers = new HashSet<>(); + initMocks(this); + mUiManagerService = new UiModeManagerService(mContext, + mWindowManager, mAlarmManager, mPowerManager, + mWakeLock, mTwilightManager, true); mService = mUiManagerService.getService(); when(mContext.checkCallingOrSelfPermission(anyString())) .thenReturn(PackageManager.PERMISSION_GRANTED); when(mContext.getResources()).thenReturn(mResources); when(mContext.getContentResolver()).thenReturn(mContentResolver); - when(mContext.registerReceiver(any(), any())).then(inv -> { - mScreenOffRecievers.add(inv.getArgument(0)); + when(mPowerManager.isInteractive()).thenReturn(true); + when(mTwilightManager.getLastTwilightState()).thenReturn(mTwilightState); + when(mTwilightState.isNight()).thenReturn(true); + when(mContext.registerReceiver(notNull(), notNull())).then(inv -> { + IntentFilter filter = inv.getArgument(1); + if (filter.hasAction(Intent.ACTION_TIMEZONE_CHANGED)) { + mTimeChangedCallback = inv.getArgument(0); + } + if (filter.hasAction(Intent.ACTION_SCREEN_OFF)) { + mScreenOffCallback = inv.getArgument(0); + } return null; }); + doAnswer(inv -> { + mCustomListener = inv.getArgument(3); + return null; + }).when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), + any(AlarmManager.OnAlarmListener.class), any(Handler.class)); + + doAnswer(inv -> { + mCustomListener = () -> {}; + return null; + }).when(mAlarmManager).cancel(eq(mCustomListener)); } @Test @@ -102,7 +147,7 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /*we should ignore this update config exception*/ } given(mContext.registerReceiver(any(), any())).willThrow(SecurityException.class); - verify(mContext).unregisterReceiver(any(BroadcastReceiver.class)); + verify(mContext, atLeastOnce()).unregisterReceiver(any(BroadcastReceiver.class)); } @Test @@ -165,6 +210,132 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { assertFalse(isNightModeActivated()); } + @Test + public void customTime_darkThemeOn() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_NO); + mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertTrue(isNightModeActivated()); + } + + @Test + public void customTime_darkThemeOff() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertFalse(isNightModeActivated()); + } + + @Test + public void customTime_darkThemeOff_afterStartEnd() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertFalse(isNightModeActivated()); + } + + @Test + public void customTime_darkThemeOn_afterStartEnd() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertFalse(isNightModeActivated()); + } + + + + @Test + public void customTime_darkThemeOn_beforeStartEnd() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.minusHours(2L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertTrue(isNightModeActivated()); + } + + @Test + public void customTime_darkThemeOff_beforeStartEnd() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.minusHours(2L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertFalse(isNightModeActivated()); + } + + @Test + public void customTIme_customAlarmSetWhenScreenTimeChanges() throws RemoteException { + when(mPowerManager.isInteractive()).thenReturn(false); + mService.setNightMode(MODE_NIGHT_CUSTOM); + verify(mAlarmManager, times(1)) + .setExact(anyInt(), anyLong(), anyString(), any(), any()); + mTimeChangedCallback.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED)); + verify(mAlarmManager, atLeast(2)) + .setExact(anyInt(), anyLong(), anyString(), any(), any()); + } + + @Test + public void customTime_alarmSetInTheFutureWhenOn() throws RemoteException { + LocalDateTime now = LocalDateTime.now(); + when(mPowerManager.isInteractive()).thenReturn(false); + mService.setNightMode(MODE_NIGHT_YES); + mService.setCustomNightModeStart(now.toLocalTime().minusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.toLocalTime().plusHours(1L).toNanoOfDay() / 1000); + LocalDateTime next = now.plusHours(1L); + final long millis = next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + mService.setNightMode(MODE_NIGHT_CUSTOM); + verify(mAlarmManager) + .setExact(anyInt(), eq(millis), anyString(), any(), any()); + } + + @Test + public void customTime_appliesImmediatelyWhenScreenOff() throws RemoteException { + when(mPowerManager.isInteractive()).thenReturn(false); + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_NO); + mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + assertTrue(isNightModeActivated()); + } + + @Test + public void customTime_appliesOnlyWhenScreenOff() throws RemoteException { + LocalTime now = LocalTime.now(); + mService.setNightMode(MODE_NIGHT_NO); + mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); + mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); + mService.setNightMode(MODE_NIGHT_CUSTOM); + assertFalse(isNightModeActivated()); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertTrue(isNightModeActivated()); + } + + @Test + public void nightAuto_appliesOnlyWhenScreenOff() throws RemoteException { + when(mTwilightState.isNight()).thenReturn(true); + mService.setNightMode(MODE_NIGHT_NO); + mService.setNightMode(MODE_NIGHT_AUTO); + assertFalse(isNightModeActivated()); + mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); + assertTrue(isNightModeActivated()); + } + private boolean isNightModeActivated() { return (mUiManagerService.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) != 0; |