Custom dark theme scheduling
allows the use to set the start and end automatic dark theme
activation within a day.
Fixes: 147649309
Test: atest UiModeManagerServiceTest UiModeManagerTest
Change-Id: Iaa3593d4e8863412e3703ce9f089b88dd4df1225
diff --git a/api/current.txt b/api/current.txt
index 815bbf8..276cd73 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6470,7 +6470,11 @@
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 @@
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 @@
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 da0aae0..fde6bb3 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 @@
+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(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 @@
-
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.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#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#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#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.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 f5809ba..41e2ec9 100644
--- a/core/java/android/app/IUiModeManager.aidl
+++ b/core/java/android/app/IUiModeManager.aidl
@@ -70,7 +70,27 @@
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 3633064..24873b8 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -18,6 +18,7 @@
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 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 @@
/** @hide */
@IntDef(prefix = { "MODE_" }, value = {
MODE_NIGHT_AUTO,
+ MODE_NIGHT_CUSTOM,
MODE_NIGHT_NO,
MODE_NIGHT_YES
})
@@ -173,19 +176,25 @@
* 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 @@
* {@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 @@
* <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 @@
}
/**
- * @hide*
+ * @hide
*/
public boolean setNightModeActivated(boolean active) {
if (mService != null) {
@@ -487,4 +499,75 @@
}
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 eee8fb1..0742a20 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5883,6 +5883,22 @@
"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 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 8439f5a..0558204 100644
--- a/core/java/android/util/TimeUtils.java
+++ b/core/java/android/util/TimeUtils.java
@@ -30,6 +30,7 @@
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 @@
}
/**
+ * 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 a85c8f4..ce03727 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2562,4 +2562,10 @@
// 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 9129938..639005b 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 7bc2a0d..8f1769b 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.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 @@
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 @@
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 9ffe89c..b994e6c 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.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 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 @@
// 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 @@
private NotificationManager mNotificationManager;
private StatusBarManager mStatusBarManager;
private WindowManagerInternal mWindowManager;
+ private AlarmManager mAlarmManager;
+ private PowerManager mPowerManager;
private PowerManager.WakeLock mWakeLock;
@@ -141,14 +158,16 @@
}
@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 @@
}
};
+ 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 @@
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 @@
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 @@
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 @@
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 @@
getContext().registerReceiver(mOnScreenOffHandler, intentFilter);
}
+ private void cancelCustomAlarm() {
+ mAlarmManager.cancel(mCustomTimeListener);
+ }
+
private void unregisterScreenOffEvent() {
mWaitForScreenOff = false;
try {
@@ -428,6 +488,21 @@
}
}
+ 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 @@
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 @@
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 @@
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 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 @@
}
}
}
+
+ @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 @@
mTwilightManager = getLocalService(TwilightManager.class);
mSystemReady = true;
mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR;
- updateComputedNightModeLocked();
registerVrStateListener();
updateLocked(0, 0);
}
@@ -838,6 +982,12 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
}
}
- 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 @@
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 @@
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 @@
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 03c10f3..22046a51 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 @@
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 @@
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 @@
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;