Add Night display feature

Bug: 28615069

Tints the display at night automatically according to your schedule or
using the sunrise/sunset corresponding to your current location.

Change-Id: Ie56b4eed88cc2fcbae88002492b1edad5820b6b1
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 874f3f3..9fe3efc 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6147,6 +6147,30 @@
                 "camera_double_tap_power_gesture_disabled";
 
         /**
+         * Control whether Night display is currently activated.
+         * @hide
+         */
+        public static final String NIGHT_DISPLAY_ACTIVATED = "night_display_activated";
+
+        /**
+         * Control whether Night display will automatically activate/deactivate.
+         * @hide
+         */
+        public static final String NIGHT_DISPLAY_AUTO_MODE = "night_display_auto_mode";
+
+        /**
+         * Custom time when Night display is scheduled to activate.
+         * Represented as milliseconds from midnight (e.g. 79200000 == 10pm).
+         * @hide
+         */
+        public static final String NIGHT_DISPLAY_CUSTOM_START_TIME = "night_display_custom_start_time";
+
+        /**
+         * Custom time when Night display is scheduled to deactivate.
+         * Represented as milliseconds from midnight (e.g. 21600000 == 6am).
+         * @hide
+         */
+        public static final String NIGHT_DISPLAY_CUSTOM_END_TIME = "night_display_custom_end_time";
 
         /**
          * Behavior of twilight on the device.
diff --git a/core/java/com/android/internal/app/NightDisplayController.java b/core/java/com/android/internal/app/NightDisplayController.java
new file mode 100644
index 0000000..03cd729
--- /dev/null
+++ b/core/java/com/android/internal/app/NightDisplayController.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings.Secure;
+import android.util.Slog;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Controller for managing Night display settings.
+ * <p/>
+ * Night display tints your screen red at night. This makes it easier to look at your screen in
+ * dim light and may help you fall asleep more easily.
+ */
+public final class NightDisplayController {
+
+    private static final String TAG = "NightDisplayController";
+    private static final boolean DEBUG = false;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ AUTO_MODE_DISABLED, AUTO_MODE_CUSTOM, AUTO_MODE_TWILIGHT })
+    public @interface AutoMode {}
+
+    /**
+     * Auto mode value to prevent Night display from being automatically activated. It can still
+     * be activated manually via {@link #setActivated(boolean)}.
+     *
+     * @see #setAutoMode(int)
+     */
+    public static final int AUTO_MODE_DISABLED = 0;
+    /**
+     * Auto mode value to automatically activate Night display at a specific start and end time.
+     *
+     * @see #setAutoMode(int)
+     * @see #setCustomStartTime(LocalTime)
+     * @see #setCustomEndTime(LocalTime)
+     */
+    public static final int AUTO_MODE_CUSTOM = 1;
+    /**
+     * Auto mode value to automatically activate Night display from sunset to sunrise.
+     *
+     * @see #setAutoMode(int)
+     */
+    public static final int AUTO_MODE_TWILIGHT = 2;
+
+    private final Context mContext;
+    private final int mUserId;
+
+    private final ContentObserver mContentObserver;
+
+    private Callback mCallback;
+
+    public NightDisplayController(@NonNull Context context) {
+        this(context, UserHandle.myUserId());
+    }
+
+    public NightDisplayController(@NonNull Context context, int userId) {
+        mContext = context.getApplicationContext();
+        mUserId = userId;
+
+        mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                super.onChange(selfChange, uri);
+
+                final String setting = uri == null ? null : uri.getLastPathSegment();
+                if (setting != null) {
+                    onSettingChanged(setting);
+                }
+            }
+        };
+    }
+
+    /**
+     * Returns {@code true} when Night display is activated (the display is tinted red).
+     */
+    public boolean isActivated() {
+        return Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_ACTIVATED, 0, mUserId) == 1;
+    }
+
+    /**
+     * Sets whether Night display should be activated.
+     *
+     * @param activated {@code true} if Night display should be activated
+     * @return {@code true} if the activated value was set successfully
+     */
+    public boolean setActivated(boolean activated) {
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_ACTIVATED, activated ? 1 : 0, mUserId);
+    }
+
+    /**
+     * Returns the current auto mode value controlling when Night display will be automatically
+     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
+     * {@link #AUTO_MODE_TWILIGHT}.
+     */
+    public @AutoMode int getAutoMode() {
+        int autoMode = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_AUTO_MODE, -1, mUserId);
+        if (autoMode == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: " + Secure.NIGHT_DISPLAY_AUTO_MODE);
+            }
+            autoMode = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayAutoMode);
+        }
+
+        if (autoMode != AUTO_MODE_DISABLED
+                && autoMode != AUTO_MODE_CUSTOM
+                && autoMode != AUTO_MODE_TWILIGHT) {
+            Slog.e(TAG, "Invalid autoMode: " + autoMode);
+            autoMode = AUTO_MODE_DISABLED;
+        }
+
+        return autoMode;
+    }
+
+    /**
+     * Sets the current auto mode value controlling when Night display will be automatically
+     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
+     * {@link #AUTO_MODE_TWILIGHT}.
+     *
+     * @param autoMode the new auto mode to use
+     * @return {@code true} if new auto mode was set successfully
+     */
+    public boolean setAutoMode(@AutoMode int autoMode) {
+        if (autoMode != AUTO_MODE_DISABLED
+                && autoMode != AUTO_MODE_CUSTOM
+                && autoMode != AUTO_MODE_TWILIGHT) {
+            throw new IllegalArgumentException("Invalid autoMode: " + autoMode);
+        }
+
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_AUTO_MODE, autoMode, mUserId);
+    }
+
+    /**
+     * Returns the local time when Night display will be automatically activated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     */
+    public @NonNull LocalTime getCustomStartTime() {
+        int startTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, -1, mUserId);
+        if (startTimeValue == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: "
+                        + Secure.NIGHT_DISPLAY_CUSTOM_START_TIME);
+            }
+            startTimeValue = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayCustomStartTime);
+        }
+
+        return LocalTime.valueOf(startTimeValue);
+    }
+
+    /**
+     * Sets the local time when Night display will be automatically activated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     *
+     * @param startTime the local time to automatically activate Night display
+     * @return {@code true} if the new custom start time was set successfully
+     */
+    public boolean setCustomStartTime(@NonNull LocalTime startTime) {
+        if (startTime == null) {
+            throw new IllegalArgumentException("startTime cannot be null");
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, startTime.toMillis(), mUserId);
+    }
+
+    /**
+     * Returns the local time when Night display will be automatically deactivated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     */
+    public @NonNull LocalTime getCustomEndTime() {
+        int endTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, -1, mUserId);
+        if (endTimeValue == -1) {
+            if (DEBUG) {
+                Slog.d(TAG, "Using default value for setting: "
+                        + Secure.NIGHT_DISPLAY_CUSTOM_END_TIME);
+            }
+            endTimeValue = mContext.getResources().getInteger(
+                    R.integer.config_defaultNightDisplayCustomEndTime);
+        }
+
+        return LocalTime.valueOf(endTimeValue);
+    }
+
+    /**
+     * Sets the local time when Night display will be automatically deactivated when using
+     * {@link #AUTO_MODE_CUSTOM}.
+     *
+     * @param endTime the local time to automatically deactivate Night display
+     * @return {@code true} if the new custom end time was set successfully
+     */
+    public boolean setCustomEndTime(@NonNull LocalTime endTime) {
+        if (endTime == null) {
+            throw new IllegalArgumentException("endTime cannot be null");
+        }
+        return Secure.putIntForUser(mContext.getContentResolver(),
+                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, endTime.toMillis(), mUserId);
+    }
+
+    private void onSettingChanged(@NonNull String setting) {
+        if (DEBUG) {
+            Slog.d(TAG, "onSettingChanged: " + setting);
+        }
+
+        if (mCallback != null) {
+            switch (setting) {
+                case Secure.NIGHT_DISPLAY_ACTIVATED:
+                    mCallback.onActivated(isActivated());
+                    break;
+                case Secure.NIGHT_DISPLAY_AUTO_MODE:
+                    mCallback.onAutoModeChanged(getAutoMode());
+                    break;
+                case Secure.NIGHT_DISPLAY_CUSTOM_START_TIME:
+                    mCallback.onCustomStartTimeChanged(getCustomStartTime());
+                    break;
+                case Secure.NIGHT_DISPLAY_CUSTOM_END_TIME:
+                    mCallback.onCustomEndTimeChanged(getCustomEndTime());
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Register a callback to be invoked whenever the Night display settings are changed.
+     */
+    public void setListener(Callback callback) {
+        final Callback oldCallback = mCallback;
+        if (oldCallback != callback) {
+            mCallback = callback;
+
+            if (callback == null) {
+                // Stop listening for changes now that there IS NOT a listener.
+                mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+            } else if (oldCallback == null) {
+                // Start listening for changes now that there IS a listener.
+                final ContentResolver cr = mContext.getContentResolver();
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_ACTIVATED),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_AUTO_MODE),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_START_TIME),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_END_TIME),
+                        false /* notifyForDescendants */, mContentObserver, mUserId);
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if Night display is supported by the device.
+     */
+    public static boolean isAvailable(Context context) {
+        return context.getResources().getBoolean(R.bool.config_nightDisplayAvailable);
+    }
+
+    /**
+     * A time without a time-zone or date.
+     */
+    public static class LocalTime {
+
+        /**
+         * The hour of the day from 0 - 23.
+         */
+        public final int hourOfDay;
+        /**
+         * The minute within the hour from 0 - 59.
+         */
+        public final int minute;
+
+        public LocalTime(int hourOfDay, int minute) {
+            if (hourOfDay < 0 || hourOfDay > 23) {
+                throw new IllegalArgumentException("Invalid hourOfDay: " + hourOfDay);
+            } else if (minute < 0 || minute > 59) {
+                throw new IllegalArgumentException("Invalid minute: " + minute);
+            }
+
+            this.hourOfDay = hourOfDay;
+            this.minute = minute;
+        }
+
+        /**
+         * Returns the first date time corresponding to this local time that occurs before the
+         * provided date time.
+         *
+         * @param time the date time to compare against
+         * @return the prior date time corresponding to this local time
+         */
+        public Calendar getDateTimeBefore(Calendar time) {
+            final Calendar c = Calendar.getInstance();
+            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
+            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
+
+            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
+            c.set(Calendar.MINUTE, minute);
+            c.set(Calendar.SECOND, 0);
+            c.set(Calendar.MILLISECOND, 0);
+
+            // Check if the local time has past, if so return the same time tomorrow.
+            if (c.after(time)) {
+                c.add(Calendar.DATE, -1);
+            }
+
+            return c;
+        }
+
+        /**
+         * Returns the first date time corresponding to this local time that occurs after the
+         * provided date time.
+         *
+         * @param time the date time to compare against
+         * @return the next date time corresponding to this local time
+         */
+        public Calendar getDateTimeAfter(Calendar time) {
+            final Calendar c = Calendar.getInstance();
+            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
+            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
+
+            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
+            c.set(Calendar.MINUTE, minute);
+            c.set(Calendar.SECOND, 0);
+            c.set(Calendar.MILLISECOND, 0);
+
+            // Check if the local time has past, if so return the same time tomorrow.
+            if (c.before(time)) {
+                c.add(Calendar.DATE, 1);
+            }
+
+            return c;
+        }
+
+        /**
+         * Returns a local time corresponding the given number of milliseconds from midnight.
+         *
+         * @param millis the number of milliseconds from midnight
+         * @return the corresponding local time
+         */
+        private static LocalTime valueOf(int millis) {
+            final int hourOfDay = (millis / 3600000) % 24;
+            final int minutes = (millis / 60000) % 60;
+            return new LocalTime(hourOfDay, minutes);
+        }
+
+        /**
+         * Returns the local time represented as milliseconds from midnight.
+         */
+        private int toMillis() {
+            return hourOfDay * 3600000 + minute * 60000;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.US, "%02d:%02d", hourOfDay, minute);
+        }
+    }
+
+    /**
+     * Callback invoked whenever the Night display settings are changed.
+     */
+    public interface Callback {
+        /**
+         * Callback invoked when the activated state changes.
+         *
+         * @param activated {@code true} if Night display is activated
+         */
+        default void onActivated(boolean activated) {}
+        /**
+         * Callback invoked when the auto mode changes.
+         *
+         * @param autoMode the auto mode to use
+         */
+        default void onAutoModeChanged(int autoMode) {}
+        /**
+         * Callback invoked when the time to automatically activate Night display changes.
+         *
+         * @param startTime the local time to automatically activate Night display
+         */
+        default void onCustomStartTimeChanged(LocalTime startTime) {}
+        /**
+         * Callback invoked when the time to automatically deactivate Night display changes.
+         *
+         * @param endTime the local time to automatically deactivate Night display
+         */
+        default void onCustomEndTimeChanged(LocalTime endTime) {}
+    }
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7d8d811..7d19537 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -754,6 +754,26 @@
     -->
     <integer name="config_defaultNightMode">1</integer>
 
+    <!-- Control whether Night display is available. This should only be enabled on devices
+         with HWC 2.0 or higher. -->
+    <bool name="config_nightDisplayAvailable">false</bool>
+
+    <!-- Default mode to control how Night display is automatically activated.
+         One of the following values (see NightDisplayController.java):
+             0 - AUTO_MODE_DISABLED
+             1 - AUTO_MODE_CUSTOM
+             2 - AUTO_MODE_TWILIGHT
+    -->
+    <integer name="config_defaultNightDisplayAutoMode">1</integer>
+
+    <!-- Default time when Night display is automatically activated.
+         Represented as milliseconds from midnight (e.g. 79200000 == 10pm). -->
+    <integer name="config_defaultNightDisplayCustomStartTime">79200000</integer>
+
+    <!-- Default time when Night display is automatically deactivated.
+         Represented as milliseconds from midnight (e.g. 21600000 == 6am). -->
+    <integer name="config_defaultNightDisplayCustomEndTime">21600000</integer>
+
     <!-- Indicate whether to allow the device to suspend when the screen is off
          due to the proximity sensor.  This resource should only be set to true
          if the sensor HAL correctly handles the proximity sensor as a wake-up source.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a781f9a..ff5f7d9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2664,4 +2664,8 @@
   <java-symbol type="drawable" name="ic_doc_video" />
   <java-symbol type="drawable" name="ic_doc_generic" />
 
+  <java-symbol type="bool" name="config_nightDisplayAvailable" />
+  <java-symbol type="integer" name="config_defaultNightDisplayAutoMode" />
+  <java-symbol type="integer" name="config_defaultNightDisplayCustomStartTime" />
+  <java-symbol type="integer" name="config_defaultNightDisplayCustomEndTime" />
 </resources>
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 1fb9642..45e3e7e 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2367,6 +2367,10 @@
     // ACTION: Settings -> Support -> "Travel Abroad" Button -> Tolled Phone
     ACTION_SUPPORT_DIAL_TOLLED = 487;
 
+    // OPEN: Settings > Display > Night display
+    // CATEGORY: SETTINGS
+    NIGHT_DISPLAY_SETTINGS = 488;
+
     // ---- End N-MR1 Constants, all N-MR1 constants go above this line ----
     // Add new aosp constants above this line.
     // END OF AOSP CONSTANTS
diff --git a/services/core/java/com/android/server/display/NightDisplayService.java b/services/core/java/com/android/server/display/NightDisplayService.java
new file mode 100644
index 0000000..27abd1e
--- /dev/null
+++ b/services/core/java/com/android/server/display/NightDisplayService.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings.Secure;
+import android.util.Slog;
+
+import com.android.internal.app.NightDisplayController;
+import com.android.server.SystemService;
+import com.android.server.twilight.TwilightListener;
+import com.android.server.twilight.TwilightManager;
+import com.android.server.twilight.TwilightState;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+/**
+ * Tints the display at night.
+ */
+public final class NightDisplayService extends SystemService
+        implements NightDisplayController.Callback {
+
+    private static final String TAG = "NightDisplayService";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Night mode ~= 3400 K.
+     */
+    private static final String MATRIX_NIGHT = "1,0,0,0,0,.754,0,0,0,0,.516,0,0,0,0,1";
+
+    private int mCurrentUser = UserHandle.USER_NULL;
+    private boolean mBootCompleted;
+
+    private NightDisplayController mController;
+    private Boolean mIsActivated;
+    private AutoMode mAutoMode;
+
+    public NightDisplayService(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onStart() {
+        // Nothing to publish.
+    }
+
+    @Override
+    public void onStartUser(int userHandle) {
+        super.onStartUser(userHandle);
+
+        // Register listeners for the new user.
+        if (mCurrentUser == UserHandle.USER_NULL) {
+            mCurrentUser = userHandle;
+            if (mBootCompleted) {
+                setUpNightMode();
+            }
+        }
+    }
+
+    @Override
+    public void onSwitchUser(int userHandle) {
+        super.onSwitchUser(userHandle);
+
+        // Unregister listeners for the old user.
+        if (mBootCompleted && mCurrentUser != UserHandle.USER_NULL) {
+            tearDownNightMode();
+        }
+
+        // Register listeners for the new user.
+        mCurrentUser = userHandle;
+        if (mBootCompleted) {
+            setUpNightMode();
+        }
+    }
+
+    @Override
+    public void onStopUser(int userHandle) {
+        super.onStopUser(userHandle);
+
+        // Unregister listeners for the old user.
+        if (mCurrentUser == userHandle) {
+            if (mBootCompleted) {
+                tearDownNightMode();
+            }
+            mCurrentUser = UserHandle.USER_NULL;
+        }
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == PHASE_BOOT_COMPLETED) {
+            mBootCompleted = true;
+
+            // Register listeners now that boot is complete.
+            if (mCurrentUser != UserHandle.USER_NULL) {
+                setUpNightMode();
+            }
+        }
+    }
+
+    private void setUpNightMode() {
+        // Create a new controller for the current user and start listening for changes.
+        mController = new NightDisplayController(getContext(), mCurrentUser);
+        mController.setListener(this);
+
+        // Initialize the current auto mode.
+        onAutoModeChanged(mController.getAutoMode());
+
+        // Force the initialization current activated state.
+        if (mIsActivated == null) {
+            onActivated(mController.isActivated());
+        }
+    }
+
+    private void tearDownNightMode() {
+        mController.setListener(null);
+
+        if (mAutoMode != null) {
+            mAutoMode.onStop();
+            mAutoMode = null;
+        }
+
+        mIsActivated = null;
+        mController = null;
+    }
+
+    @Override
+    public void onActivated(boolean activated) {
+        if (mIsActivated == null || mIsActivated != activated) {
+            Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
+
+            mIsActivated = activated;
+
+            if (mAutoMode != null) {
+                mAutoMode.onActivated(activated);
+            }
+
+            // Update the current color matrix.
+            final ContentResolver cr = getContext().getContentResolver();
+            Secure.putStringForUser(cr, Secure.ACCESSIBILITY_DISPLAY_COLOR_MATRIX,
+                    activated ? MATRIX_NIGHT : null, mCurrentUser);
+        }
+    }
+
+    @Override
+    public void onAutoModeChanged(int autoMode) {
+        if (mAutoMode != null) {
+            mAutoMode.onStop();
+            mAutoMode = null;
+        }
+
+        if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM) {
+            mAutoMode = new CustomAutoMode();
+        } else if (autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
+            mAutoMode = new TwilightAutoMode();
+        }
+
+        if (mAutoMode != null) {
+            mAutoMode.onStart();
+        }
+    }
+
+    @Override
+    public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
+        if (mAutoMode != null) {
+            mAutoMode.onCustomStartTimeChanged(startTime);
+        }
+    }
+
+    @Override
+    public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
+        if (mAutoMode != null) {
+            mAutoMode.onCustomEndTimeChanged(endTime);
+        }
+    }
+
+    private abstract class AutoMode implements NightDisplayController.Callback {
+        public abstract void onStart();
+        public abstract void onStop();
+    }
+
+    private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener {
+
+        private final AlarmManager mAlarmManager;
+        private final BroadcastReceiver mTimeChangedReceiver;
+
+        private NightDisplayController.LocalTime mStartTime;
+        private NightDisplayController.LocalTime mEndTime;
+
+        private Calendar mLastActivatedTime;
+
+        public CustomAutoMode() {
+            mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+            mTimeChangedReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    updateActivated();
+                }
+            };
+        }
+
+        private void updateActivated() {
+            final Calendar now = Calendar.getInstance();
+            final Calendar startTime = mStartTime.getDateTimeBefore(now);
+            final Calendar endTime = mEndTime.getDateTimeAfter(startTime);
+            final boolean activated = now.before(endTime);
+
+            boolean setActivated = mIsActivated == null || mLastActivatedTime == null;
+            if (!setActivated && mIsActivated != activated) {
+                final TimeZone currentTimeZone = now.getTimeZone();
+                if (!currentTimeZone.equals(mLastActivatedTime.getTimeZone())) {
+                    final int year = mLastActivatedTime.get(Calendar.YEAR);
+                    final int dayOfYear = mLastActivatedTime.get(Calendar.DAY_OF_YEAR);
+                    final int hourOfDay = mLastActivatedTime.get(Calendar.HOUR_OF_DAY);
+                    final int minute = mLastActivatedTime.get(Calendar.MINUTE);
+
+                    mLastActivatedTime.setTimeZone(currentTimeZone);
+                    mLastActivatedTime.set(Calendar.YEAR, year);
+                    mLastActivatedTime.set(Calendar.DAY_OF_YEAR, dayOfYear);
+                    mLastActivatedTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
+                    mLastActivatedTime.set(Calendar.MINUTE, minute);
+                }
+
+                if (mIsActivated) {
+                    setActivated = now.before(mStartTime.getDateTimeBefore(mLastActivatedTime))
+                            || now.after(mEndTime.getDateTimeAfter(mLastActivatedTime));
+                } else {
+                    setActivated = now.before(mEndTime.getDateTimeBefore(mLastActivatedTime))
+                            || now.after(mStartTime.getDateTimeAfter(mLastActivatedTime));
+                }
+            }
+
+            if (setActivated) {
+                mController.setActivated(activated);
+            }
+            updateNextAlarm();
+        }
+
+        private void updateNextAlarm() {
+            if (mIsActivated != null) {
+                final Calendar now = Calendar.getInstance();
+                final Calendar next = mIsActivated ? mEndTime.getDateTimeAfter(now)
+                        : mStartTime.getDateTimeAfter(now);
+                mAlarmManager.setExact(AlarmManager.RTC, next.getTimeInMillis(), TAG, this, null);
+            }
+        }
+
+        @Override
+        public void onStart() {
+            final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
+            intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+            getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
+
+            mStartTime = mController.getCustomStartTime();
+            mEndTime = mController.getCustomEndTime();
+
+            // Force an update to initialize state.
+            updateActivated();
+        }
+
+        @Override
+        public void onStop() {
+            getContext().unregisterReceiver(mTimeChangedReceiver);
+
+            mAlarmManager.cancel(this);
+            mLastActivatedTime = null;
+        }
+
+        @Override
+        public void onActivated(boolean activated) {
+            mLastActivatedTime = Calendar.getInstance();
+            updateNextAlarm();
+        }
+
+        @Override
+        public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
+            mStartTime = startTime;
+            mLastActivatedTime = null;
+            updateActivated();
+        }
+
+        @Override
+        public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
+            mEndTime = endTime;
+            mLastActivatedTime = null;
+            updateActivated();
+        }
+
+        @Override
+        public void onAlarm() {
+            if (DEBUG) Slog.d(TAG, "onAlarm");
+            updateActivated();
+        }
+    }
+
+    private class TwilightAutoMode extends AutoMode implements TwilightListener {
+
+        private final TwilightManager mTwilightManager;
+        private final Handler mHandler;
+
+        private boolean mIsNight;
+
+        public TwilightAutoMode() {
+            mTwilightManager = getLocalService(TwilightManager.class);
+            mHandler = new Handler(Looper.getMainLooper());
+        }
+
+        private void updateActivated() {
+            final TwilightState state = mTwilightManager.getCurrentState();
+            final boolean isNight = state != null && state.isNight();
+            if (mIsNight != isNight) {
+                mIsNight = isNight;
+
+                if (mIsActivated == null || mIsActivated != isNight) {
+                    mController.setActivated(isNight);
+                }
+            }
+        }
+
+        @Override
+        public void onStart() {
+            mTwilightManager.registerListener(this, mHandler);
+
+            // Force an update to initialize state.
+            updateActivated();
+        }
+
+        @Override
+        public void onStop() {
+            mTwilightManager.unregisterListener(this);
+        }
+
+        @Override
+        public void onTwilightStateChanged() {
+            if (DEBUG) Slog.d(TAG, "onTwilightStateChanged");
+            updateActivated();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/twilight/TwilightService.java b/services/core/java/com/android/server/twilight/TwilightService.java
index 6158c92..89e5e58 100644
--- a/services/core/java/com/android/server/twilight/TwilightService.java
+++ b/services/core/java/com/android/server/twilight/TwilightService.java
@@ -556,7 +556,7 @@
         public void onChange(boolean selfChange) {
             super.onChange(selfChange);
             int value = Secure.getIntForUser(getContext().getContentResolver(),
-                    Secure.TWILIGHT_MODE, Secure.TWILIGHT_MODE_LOCKED_OFF, mCurrentUser);
+                    Secure.TWILIGHT_MODE, Secure.TWILIGHT_MODE_AUTO, mCurrentUser);
             if (value == Secure.TWILIGHT_MODE_LOCKED_OFF) {
                 setLockedState(new TwilightState(false, 0));
             } else if (value == Secure.TWILIGHT_MODE_LOCKED_ON) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 37a9d5d..1002b0d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -51,6 +51,7 @@
 import android.view.WindowManager;
 
 import com.android.internal.R;
+import com.android.internal.app.NightDisplayController;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.SamplingProfilerIntegration;
 import com.android.internal.os.ZygoteInit;
@@ -63,6 +64,7 @@
 import com.android.server.connectivity.MetricsLoggerService;
 import com.android.server.devicepolicy.DevicePolicyManagerService;
 import com.android.server.display.DisplayManagerService;
+import com.android.server.display.NightDisplayService;
 import com.android.server.dreams.DreamManagerService;
 import com.android.server.fingerprint.FingerprintService;
 import com.android.server.hdmi.HdmiControlService;
@@ -995,6 +997,10 @@
 
             mSystemServiceManager.startService(TwilightService.class);
 
+            if (NightDisplayController.isAvailable(context)) {
+                mSystemServiceManager.startService(NightDisplayService.class);
+            }
+
             mSystemServiceManager.startService(JobSchedulerService.class);
 
             mSystemServiceManager.startService(SoundTriggerService.class);