[Night] Add a new night theme trigger: bedtime

This CL expands the existing UiModeManager APIs to allow users activate
dark mode at their configured bedtime schedule on supported devices,
i.e. devices with Digital Wellbeing preinstalled.

The CL added granular types for NIGHT_MODE_CUSTOM. There are two types
1. MODE_NIGHT_CUSTOM_TYPE_SCHEDULE
   This is the type for a schedule set up by users via the Settings
   app.
2. MODE_NIGHT_CUSTOM_TYPE_BEDTIME
   This is the type for a bedtime schedule set up by users via the
   Digital Wellbeing bedtime settings. Unlike
   MODE_NIGHT_CUSTOM_TYPE_SCHEDULE, Android framework doesn't have
   any information of the bedtime schedule. The activation of dark
   theme lives inside Digital Wellbeing

Test: unit tests: atest FrameworksUiServicesTests:UiModeManagerServiceTest
      CTS: atest CtsAppTestCases:UiModeManagerTest
      Manual testing via a system app integration
Bug: 210975231
Change-Id: I3b16e649a048f8a485b9e9d6464d4f251839320d
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 75c9be9..f67a71a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -896,15 +896,21 @@
     method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void addOnProjectionStateChangedListener(int, @NonNull java.util.concurrent.Executor, @NonNull android.app.UiModeManager.OnProjectionStateChangedListener);
     method @RequiresPermission(android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED) public void enableCarMode(@IntRange(from=0) int, int);
     method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public int getActiveProjectionTypes();
+    method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getNightModeCustomType();
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public java.util.Set<java.lang.String> getProjectingPackages(int);
     method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int);
     method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void removeOnProjectionStateChangedListener(@NonNull android.app.UiModeManager.OnProjectionStateChangedListener);
     method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public boolean setNightModeActivatedForCustomMode(int, boolean);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public void setNightModeCustomType(int);
     field public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED = "android.app.action.ENTER_CAR_MODE_PRIORITIZED";
     field public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED = "android.app.action.EXIT_CAR_MODE_PRIORITIZED";
     field public static final int DEFAULT_PRIORITY = 0; // 0x0
     field public static final String EXTRA_CALLING_PACKAGE = "android.app.extra.CALLING_PACKAGE";
     field public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY";
+    field public static final int MODE_NIGHT_CUSTOM_TYPE_BEDTIME = 1; // 0x1
+    field public static final int MODE_NIGHT_CUSTOM_TYPE_SCHEDULE = 0; // 0x0
+    field public static final int MODE_NIGHT_CUSTOM_TYPE_UNKNOWN = -1; // 0xffffffff
     field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff
     field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1
     field public static final int PROJECTION_TYPE_NONE = 0; // 0x0
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl
index 440dd62..55afed2 100644
--- a/core/java/android/app/IUiModeManager.aidl
+++ b/core/java/android/app/IUiModeManager.aidl
@@ -48,20 +48,43 @@
     
     /**
      * Sets the night mode.
+     * <p>
      * The mode can be one of:
-     *   1 - notnight mode
-     *   2 - night mode
-     *   3 - automatic mode switching
+     * <ol>notnight mode</ol>
+     * <ol>night mode</ol>
+     * <ol>custom schedule mode switching</ol>
      */
     void setNightMode(int mode);
 
     /**
-     * Gets the currently configured night mode.  Return 1 for notnight,
-     * 2 for night, and 3 for automatic mode switching.
+     * Gets the currently configured night mode.
+     * <p>
+     * Returns
+     * <ol>notnight mode</ol>
+     * <ol>night mode</ol>
+     * <ol>custom schedule mode switching</ol>
      */
     int getNightMode();
 
     /**
+     * Sets the current night mode to {@link #MODE_NIGHT_CUSTOM} with the custom night mode type
+     * {@code nightModeCustomType}.
+     *
+     * @param nightModeCustomType
+     * @hide
+     */
+    void setNightModeCustomType(int nightModeCustomType);
+
+    /**
+     * Returns the custom night mode type.
+     * <p>
+     * If the current night mode is not {@link #MODE_NIGHT_CUSTOM}, returns
+     * {@link #MODE_NIGHT_CUSTOM_TYPE_UNKNOWN}.
+     * @hide
+     */
+    int getNightModeCustomType();
+
+    /**
      * Sets the dark mode for the given application. This setting is persisted and will override the
      * system configuration for this application.
      *   1 - notnight mode
@@ -81,8 +104,21 @@
     boolean isNightModeLocked();
 
     /**
-    * [De]Activates night mode
-    */
+     * [De]activating night mode for the current user if the current night mode is custom and the
+     * custom type matches {@code nightModeCustomType}.
+     *
+     * @param nightModeCustomType the specify type of custom mode
+     * @param active {@code true} to activate night mode. Otherwise, deactivate night mode
+     * @return {@code true} if night mode has successfully activated for the requested
+     *         {@code nightModeCustomType}.
+     * @hide
+     */
+    boolean setNightModeActivatedForCustomMode(int nightModeCustom, boolean active);
+
+    /**
+     * [De]Activates night mode.
+     * @hide
+     */
     boolean setNightModeActivated(boolean active);
 
     /**
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 973a8fb..73a9e5a 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -243,6 +243,45 @@
      */
     public static final int MODE_NIGHT_YES = 2;
 
+    /**
+     * Granular types for {@link MODE_NIGHT_CUSTOM_TYPE_BEDTIME}
+     * @hide
+     */
+    @IntDef(prefix = { "MODE_NIGHT_CUSTOM_TYPE_" }, value = {
+            MODE_NIGHT_CUSTOM_TYPE_UNKNOWN,
+            MODE_NIGHT_CUSTOM_TYPE_SCHEDULE,
+            MODE_NIGHT_CUSTOM_TYPE_BEDTIME,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NightModeCustomType {}
+
+    /**
+     * A granular type for {@link #MODE_NIGHT_CUSTOM} which is unknown.
+     * <p>
+     * This is the default value when the night mode is set to value other than
+     * {@link #MODE_NIGHT_CUSTOM}.
+     * @hide
+     */
+    @SystemApi
+    public static final int MODE_NIGHT_CUSTOM_TYPE_UNKNOWN = -1;
+
+    /**
+     * A granular type for {@link #MODE_NIGHT_CUSTOM} which is based on a custom schedule.
+     * <p>
+     * This is the default value when night mode is set to {@link #MODE_NIGHT_CUSTOM} unless the
+     * the night mode custom type is specified by calling {@link #setNightModeCustomType(int)}.
+     * @hide
+     */
+    @SystemApi
+    public static final int MODE_NIGHT_CUSTOM_TYPE_SCHEDULE = 0;
+
+    /**
+     * A granular type for {@link #MODE_NIGHT_CUSTOM} which is based on the bedtime schedule.
+     * @hide
+     */
+    @SystemApi
+    public static final int MODE_NIGHT_CUSTOM_TYPE_BEDTIME = 1;
+
     private IUiModeManager mService;
 
     /**
@@ -496,6 +535,45 @@
     }
 
     /**
+     * Sets the current night mode to {@link #MODE_NIGHT_CUSTOM} with the custom night mode type
+     * {@code nightModeCustomType}.
+     *
+     * @param nightModeCustomType
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
+    public void setNightModeCustomType(@NightModeCustomType int nightModeCustomType) {
+        if (mService != null) {
+            try {
+                mService.setNightModeCustomType(nightModeCustomType);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Returns the custom night mode type.
+     * <p>
+     * If the current night mode is not {@link #MODE_NIGHT_CUSTOM}, returns
+     * {@link #MODE_NIGHT_CUSTOM_TYPE_UNKNOWN}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
+    public int getNightModeCustomType() {
+        if (mService != null) {
+            try {
+                return mService.getNightModeCustomType();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
+    }
+
+    /**
      * Sets and persist the night mode for this application.
      * <p>
      * The mode can be one of:
@@ -599,11 +677,36 @@
     }
 
     /**
+     * [De]activating night mode for the current user if the current night mode is custom and the
+     * custom type matches {@code nightModeCustomType}.
+     *
+     * @param nightModeCustomType the specify type of custom mode
+     * @param active {@code true} to activate night mode. Otherwise, deactivate night mode
+     * @return {@code true} if night mode has successfully activated for the requested
+     *         {@code nightModeCustomType}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
+    public boolean setNightModeActivatedForCustomMode(@NightModeCustomType int nightModeCustomType,
+            boolean active) {
+        if (mService != null) {
+            try {
+                return mService.setNightModeActivatedForCustomMode(nightModeCustomType, active);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        return false;
+    }
+
+    /**
      * Activating night mode for the current user
      *
      * @return {@code true} if the change is successful
      * @hide
      */
+    @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
     public boolean setNightModeActivated(boolean active) {
         if (mService != null) {
             try {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 190b8f6..e653d8f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8899,6 +8899,15 @@
         public static final String UI_NIGHT_MODE = "ui_night_mode";
 
         /**
+         * The current night mode custom type that has been selected by the user.  Owned
+         * and controlled by UiModeManagerService. Constants are as per UiModeManager.
+         * @hide
+         */
+        @Readable
+        @SuppressLint("NoSettingsProvider")
+        public static final String UI_NIGHT_MODE_CUSTOM_TYPE = "ui_night_mode_custom_type";
+
+        /**
          * The current night mode that has been overridden to turn on by the system.  Owned
          * and controlled by UiModeManagerService.  Constants are as per
          * UiModeManager.
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 81627a0..c236a7f 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -19,6 +19,9 @@
 import static android.app.UiModeManager.DEFAULT_PRIORITY;
 import static android.app.UiModeManager.MODE_NIGHT_AUTO;
 import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
 import static android.app.UiModeManager.MODE_NIGHT_NO;
 import static android.app.UiModeManager.MODE_NIGHT_YES;
 import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE;
@@ -40,6 +43,7 @@
 import android.app.PendingIntent;
 import android.app.StatusBarManager;
 import android.app.UiModeManager;
+import android.app.UiModeManager.NightModeCustomType;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -115,6 +119,7 @@
 
     private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
     private int mNightMode = UiModeManager.MODE_NIGHT_NO;
+    private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
     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;
@@ -136,6 +141,7 @@
     private boolean mWatch;
     private boolean mVrHeadset;
     private boolean mComputedNightMode;
+    private boolean mLastBedtimeRequestedNightMode = false;
     private int mCarModeEnableFlags;
     private boolean mSetupWizardComplete;
 
@@ -541,7 +547,9 @@
             mNightMode = Secure.getIntForUser(context.getContentResolver(),
                     Secure.UI_NIGHT_MODE, res.getInteger(
                             com.android.internal.R.integer.config_defaultNightMode), userId);
-            mOverrideNightModeOn = Secure.getIntForUser(context.getContentResolver(),
+            mNightModeCustomType = Secure.getIntForUser(context.getContentResolver(),
+                    Secure.UI_NIGHT_MODE_CUSTOM_TYPE, MODE_NIGHT_CUSTOM_TYPE_UNKNOWN, userId);
+                    mOverrideNightModeOn = Secure.getIntForUser(context.getContentResolver(),
                     Secure.UI_NIGHT_MODE_OVERRIDE_ON, 0, userId) != 0;
             mOverrideNightModeOff = Secure.getIntForUser(context.getContentResolver(),
                     Secure.UI_NIGHT_MODE_OVERRIDE_OFF, 0, userId) != 0;
@@ -702,6 +710,14 @@
 
         @Override
         public void setNightMode(int mode) {
+            // MODE_NIGHT_CUSTOM_TYPE_SCHEDULE is the default for MODE_NIGHT_CUSTOM.
+            int customModeType = mode == MODE_NIGHT_CUSTOM
+                    ? MODE_NIGHT_CUSTOM_TYPE_SCHEDULE
+                    : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
+            setNightModeInternal(mode, customModeType);
+        }
+
+        private void setNightModeInternal(int mode, int customModeType) {
             if (isNightModeLocked() && (getContext().checkCallingOrSelfPermission(
                     android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
                     != PackageManager.PERMISSION_GRANTED)) {
@@ -722,12 +738,14 @@
             final long ident = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    if (mNightMode != mode) {
+                    if (mNightMode != mode || mNightModeCustomType != customModeType) {
                         if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) {
                             unregisterScreenOffEventLocked();
                             cancelCustomAlarm();
                         }
-
+                        mNightModeCustomType = mode == MODE_NIGHT_CUSTOM
+                                ? customModeType
+                                : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
                         mNightMode = mode;
                         resetNightModeOverrideLocked();
                         persistNightMode(user);
@@ -754,6 +772,30 @@
         }
 
         @Override
+        public void setNightModeCustomType(@NightModeCustomType int nightModeCustomType) {
+            if (getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException(
+                        "setNightModeCustomType requires MODIFY_DAY_NIGHT_MODE permission");
+            }
+            setNightModeInternal(MODE_NIGHT_CUSTOM, nightModeCustomType);
+        }
+
+        @Override
+        public int getNightModeCustomType() {
+            if (getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException(
+                        "getNightModeCustomType requires MODIFY_DAY_NIGHT_MODE permission");
+            }
+            synchronized (mLock) {
+                return mNightModeCustomType;
+            }
+        }
+
+        @Override
         public void setApplicationNightMode(@UiModeManager.NightMode int mode) {
             switch (mode) {
                 case UiModeManager.MODE_NIGHT_NO:
@@ -808,10 +850,19 @@
         }
 
         @Override
+        public boolean setNightModeActivatedForCustomMode(int modeNightCustomType, boolean active) {
+            return setNightModeActivatedForModeInternal(modeNightCustomType, active);
+        }
+
+        @Override
         public boolean setNightModeActivated(boolean active) {
-            if (isNightModeLocked() && (getContext().checkCallingOrSelfPermission(
+            return setNightModeActivatedForModeInternal(mNightModeCustomType, active);
+        }
+
+        private boolean setNightModeActivatedForModeInternal(int modeCustomType, boolean active) {
+            if (getContext().checkCallingOrSelfPermission(
                     android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
-                    != PackageManager.PERMISSION_GRANTED)) {
+                    != PackageManager.PERMISSION_GRANTED) {
                 Slog.e(TAG, "Night mode locked, requires MODIFY_DAY_NIGHT_MODE permission");
                 return false;
             }
@@ -824,6 +875,14 @@
                 return false;
 
             }
+            // Store the last requested bedtime night mode state so that we don't need to notify
+            // anyone if the user decides to switch to the night mode to bedtime.
+            if (modeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) {
+                mLastBedtimeRequestedNightMode = active;
+            }
+            if (modeCustomType != mNightModeCustomType) {
+                return false;
+            }
             synchronized (mLock) {
                 final long ident = Binder.clearCallingIdentity();
                 try {
@@ -1422,6 +1481,8 @@
         Secure.putIntForUser(getContext().getContentResolver(),
                 Secure.UI_NIGHT_MODE, mNightMode, user);
         Secure.putLongForUser(getContext().getContentResolver(),
+                Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mNightModeCustomType, user);
+        Secure.putLongForUser(getContext().getContentResolver(),
                 Secure.DARK_THEME_CUSTOM_START_TIME,
                 mCustomAutoNightModeStartMilliseconds.toNanoOfDay() / 1000, user);
         Secure.putLongForUser(getContext().getContentResolver(),
@@ -1473,10 +1534,14 @@
         }
 
         if (mNightMode == MODE_NIGHT_CUSTOM) {
-            registerTimeChangeEvent();
-            final boolean activate = computeCustomNightMode();
-            updateComputedNightModeLocked(activate);
-            scheduleNextCustomTimeListener();
+            if (mNightModeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME) {
+                updateComputedNightModeLocked(mLastBedtimeRequestedNightMode);
+            } else {
+                registerTimeChangeEvent();
+                final boolean activate = computeCustomNightMode();
+                updateComputedNightModeLocked(activate);
+                scheduleNextCustomTimeListener();
+            }
         } else {
             unregisterTimeChangeEvent();
         }
@@ -1494,6 +1559,7 @@
                     "updateConfigurationLocked: mDockState=" + mDockState
                     + "; mCarMode=" + mCarModeEnabled
                     + "; mNightMode=" + mNightMode
+                    + "; mNightModeCustomType=" + mNightModeCustomType
                     + "; uiMode=" + uiMode);
         }
 
@@ -1534,7 +1600,8 @@
     }
 
     private boolean shouldApplyAutomaticChangesImmediately() {
-        return mCar || !mPowerManager.isInteractive();
+        return mCar || !mPowerManager.isInteractive()
+                || mNightModeCustomType == MODE_NIGHT_CUSTOM_TYPE_BEDTIME;
     }
 
     private void scheduleNextCustomTimeListener() {
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index f21991d..a12bc3b 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -16,14 +16,20 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.MODIFY_DAY_NIGHT_MODE;
 import static android.app.UiModeManager.MODE_NIGHT_AUTO;
 import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE;
+import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
 import static android.app.UiModeManager.MODE_NIGHT_NO;
 import static android.app.UiModeManager.MODE_NIGHT_YES;
 import static android.app.UiModeManager.PROJECTION_TYPE_ALL;
 import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE;
 import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
 
@@ -194,7 +200,7 @@
 
     @Ignore // b/152719290 - Fails on stage-aosp-master
     @Test
-    public void setNightMoveActivated_overridesFunctionCorrectly() throws RemoteException {
+    public void setNightModeActivated_overridesFunctionCorrectly() throws RemoteException {
         // set up
         when(mPowerManager.isInteractive()).thenReturn(false);
         mService.setNightMode(MODE_NIGHT_NO);
@@ -225,6 +231,29 @@
     }
 
     @Test
+    public void setNightModeActivated_true_withCustomModeBedtime_shouldOverrideNightModeCorrectly()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        assertFalse(mUiManagerService.getConfiguration().isNightModeActive());
+
+        mService.setNightModeActivated(true);
+
+        assertThat(mUiManagerService.getConfiguration().isNightModeActive()).isTrue();
+    }
+
+    @Test
+    public void setNightModeActivated_false_withCustomModeBedtime_shouldOverrideNightModeCorrectly()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        assertFalse(mUiManagerService.getConfiguration().isNightModeActive());
+
+        mService.setNightModeActivated(true);
+        mService.setNightModeActivated(false);
+
+        assertThat(mUiManagerService.getConfiguration().isNightModeActive()).isFalse();
+    }
+
+    @Test
     public void setAutoMode_screenOffRegistered() throws RemoteException {
         try {
             mService.setNightMode(MODE_NIGHT_NO);
@@ -247,7 +276,44 @@
     }
 
     @Test
-    public void setNightModeActivated_fromNoToYesAndBAck() throws RemoteException {
+    public void setNightModeCustomType_bedtime_shouldNotActivateNightMode() throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_NO);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void setNightModeCustomType_noPermission_shouldThrow() throws RemoteException {
+        when(mContext.checkCallingOrSelfPermission(eq(MODIFY_DAY_NIGHT_MODE)))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        assertThrows(SecurityException.class,
+                () -> mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME));
+    }
+
+    @Test
+    public void setNightModeCustomType_bedtime_shouldHaveNoScreenOffRegistered()
+            throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_NO);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        ArgumentCaptor<IntentFilter> intentFiltersCaptor = ArgumentCaptor.forClass(
+                IntentFilter.class);
+        verify(mContext, atLeastOnce()).registerReceiver(any(BroadcastReceiver.class),
+                intentFiltersCaptor.capture());
+
+        List<IntentFilter> intentFilters = intentFiltersCaptor.getAllValues();
+        for (IntentFilter intentFilter : intentFilters) {
+            assertThat(intentFilter.hasAction(Intent.ACTION_SCREEN_OFF)).isFalse();
+        }
+    }
+
+    @Test
+    public void setNightModeActivated_fromNoToYesAndBack() throws RemoteException {
         mService.setNightMode(MODE_NIGHT_NO);
         mService.setNightModeActivated(true);
         assertTrue(isNightModeActivated());
@@ -256,7 +322,7 @@
     }
 
     @Test
-    public void setNightModeActivated_permissiontoChangeOtherUsers() throws RemoteException {
+    public void setNightModeActivated_permissionToChangeOtherUsers() throws RemoteException {
         SystemService.TargetUser user = mock(SystemService.TargetUser.class);
         doReturn(9).when(user).getUserIdentifier();
         mUiManagerService.onUserSwitching(user, user);
@@ -267,6 +333,89 @@
     }
 
     @Test
+    public void setNightModeActivatedForCustomMode_customTypeBedtime_withParamOnAndBedtime_shouldActivate()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeBedtime_withParamOffAndBedtime_shouldDeactivate()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, false /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeBedtime_withParamOnAndSchedule_shouldNotActivate()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_SCHEDULE, true /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeSchedule_withParamOnAndBedtime_shouldNotActivate()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_CUSTOM);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeSchedule_withParamOnAndBedtime_thenCustomTypeBedtime_shouldActivate()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_CUSTOM);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeBedtime_withParamOnAndBedtime_thenCustomTypeSchedule_shouldKeepNightModeActivate()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightMode(MODE_NIGHT_CUSTOM);
+        LocalTime now = LocalTime.now();
+        mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000);
+        mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void setNightModeActivatedForCustomMode_customTypeBedtime_withParamOnAndBedtime_thenCustomTypeScheduleAndScreenOff_shouldDeactivateNightMode()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightMode(MODE_NIGHT_CUSTOM);
+        LocalTime now = LocalTime.now();
+        mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000);
+        mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000);
+        mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF));
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
     public void autoNightModeSwitch_batterySaverOn() throws RemoteException {
         mService.setNightMode(MODE_NIGHT_NO);
         when(mTwilightState.isNight()).thenReturn(false);
@@ -283,6 +432,191 @@
     }
 
     @Test
+    public void nightModeCustomBedtime_batterySaverOn_notInBedtime_shouldActivateNightMode()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(true).build());
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_batterySaverOn_afterBedtime_shouldKeepNightModeActivated()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(true).build());
+
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, false /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeBedtime_duringBedtime_batterySaverOnThenOff_shouldKeepNightModeActivated()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(true).build());
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(false).build());
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_duringBedtime_batterySaverOnThenOff_finallyAfterBedtime_shouldDeactivateNightMode()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(true).build());
+        mPowerSaveConsumer.accept(
+                new PowerSaveState.Builder().setBatterySaverEnabled(false).build());
+
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, false /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_duringBedtime_changeModeToNo_shouldDeactivateNightMode()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightMode(MODE_NIGHT_NO);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_duringBedtime_changeModeToNoAndThenExitBedtime_shouldKeepNightModeDeactivated()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+        mService.setNightMode(MODE_NIGHT_NO);
+
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, false /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_duringBedtime_changeModeToYes_shouldKeepNightModeActivated()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightMode(MODE_NIGHT_YES);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeCustomBedtime_duringBedtime_changeModeToYesAndThenExitBedtime_shouldKeepNightModeActivated()
+            throws RemoteException {
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        mService.setNightMode(MODE_NIGHT_YES);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, false /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeNo_duringBedtime_shouldKeepNightModeDeactivated()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_NO);
+
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeNo_thenChangeToCustomTypeBedtimeAndActivate_shouldActivateNightMode()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_NO);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeYes_thenChangeToCustomTypeBedtime_shouldDeactivateNightMode()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_YES);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeYes_thenChangeToCustomTypeBedtimeAndActivate_shouldActivateNightMode()
+            throws RemoteException {
+        mService.setNightMode(MODE_NIGHT_YES);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
+    public void nightModeAuto_thenChangeToCustomTypeBedtime_notInBedtime_shouldDeactivateNightMode()
+            throws RemoteException {
+        // set mode to auto
+        mService.setNightMode(MODE_NIGHT_AUTO);
+        mService.setNightModeActivated(true);
+        // now it is night time
+        doReturn(true).when(mTwilightState).isNight();
+        mTwilightListener.onTwilightStateChanged(mTwilightState);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+
+        assertThat(isNightModeActivated()).isFalse();
+    }
+
+    @Test
+    public void nightModeAuto_thenChangeToCustomTypeBedtime_duringBedtime_shouldActivateNightMode()
+            throws RemoteException {
+        // set mode to auto
+        mService.setNightMode(MODE_NIGHT_AUTO);
+        mService.setNightModeActivated(true);
+        // now it is night time
+        doReturn(true).when(mTwilightState).isNight();
+        mTwilightListener.onTwilightStateChanged(mTwilightState);
+
+        mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        mService.setNightModeActivatedForCustomMode(
+                MODE_NIGHT_CUSTOM_TYPE_BEDTIME, true /* active */);
+
+        assertThat(isNightModeActivated()).isTrue();
+    }
+
+    @Test
     public void setAutoMode_clearCache() throws RemoteException {
         try {
             mService.setNightMode(MODE_NIGHT_AUTO);
@@ -327,6 +661,62 @@
     }
 
     @Test
+    public void getNightModeCustomType_nightModeNo_shouldReturnUnknown() throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_NO);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+
+        assertThat(mService.getNightModeCustomType()).isEqualTo(MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
+    }
+
+    @Test
+    public void getNightModeCustomType_nightModeYes_shouldReturnUnknown() throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_YES);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+
+        assertThat(mService.getNightModeCustomType()).isEqualTo(MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
+    }
+
+    @Test
+    public void getNightModeCustomType_nightModeAuto_shouldReturnUnknown() throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_AUTO);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+
+        assertThat(mService.getNightModeCustomType()).isEqualTo(MODE_NIGHT_CUSTOM_TYPE_UNKNOWN);
+    }
+
+    @Test
+    public void getNightModeCustomType_nightModeCustom_shouldReturnSchedule()
+            throws RemoteException {
+        try {
+            mService.setNightMode(MODE_NIGHT_CUSTOM);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+
+        assertThat(mService.getNightModeCustomType()).isEqualTo(MODE_NIGHT_CUSTOM_TYPE_SCHEDULE);
+    }
+
+    @Test
+    public void getNightModeCustomType_nightModeCustomBedtime_shouldReturnBedtime()
+            throws RemoteException {
+        try {
+            mService.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+        } catch (SecurityException e) { /* we should ignore this update config exception*/ }
+
+        assertThat(mService.getNightModeCustomType()).isEqualTo(MODE_NIGHT_CUSTOM_TYPE_BEDTIME);
+    }
+
+    @Test
+    public void getNightModeCustomType_permissionNotGranted_shouldThrow()
+            throws RemoteException {
+        when(mContext.checkCallingOrSelfPermission(eq(MODIFY_DAY_NIGHT_MODE)))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        assertThrows(SecurityException.class, () -> mService.getNightModeCustomType());
+    }
+
+    @Test
     public void isNightModeActive_nightModeYes() throws RemoteException {
         try {
             mService.setNightMode(MODE_NIGHT_YES);