NightDisplayController.java revision 1454eae75da9f775425b51c2cb573a2b4c33b7e7
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.app;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.app.ActivityManager;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.database.ContentObserver;
25import android.net.Uri;
26import android.os.Handler;
27import android.os.Looper;
28import android.provider.Settings.Secure;
29import android.util.Slog;
30
31import com.android.internal.R;
32
33import java.lang.annotation.Retention;
34import java.lang.annotation.RetentionPolicy;
35import java.util.Calendar;
36import java.util.Locale;
37
38/**
39 * Controller for managing Night display settings.
40 * <p/>
41 * Night display tints your screen red at night. This makes it easier to look at your screen in
42 * dim light and may help you fall asleep more easily.
43 */
44public final class NightDisplayController {
45
46    private static final String TAG = "NightDisplayController";
47    private static final boolean DEBUG = false;
48
49    @Retention(RetentionPolicy.SOURCE)
50    @IntDef({ AUTO_MODE_DISABLED, AUTO_MODE_CUSTOM, AUTO_MODE_TWILIGHT })
51    public @interface AutoMode {}
52
53    /**
54     * Auto mode value to prevent Night display from being automatically activated. It can still
55     * be activated manually via {@link #setActivated(boolean)}.
56     *
57     * @see #setAutoMode(int)
58     */
59    public static final int AUTO_MODE_DISABLED = 0;
60    /**
61     * Auto mode value to automatically activate Night display at a specific start and end time.
62     *
63     * @see #setAutoMode(int)
64     * @see #setCustomStartTime(LocalTime)
65     * @see #setCustomEndTime(LocalTime)
66     */
67    public static final int AUTO_MODE_CUSTOM = 1;
68    /**
69     * Auto mode value to automatically activate Night display from sunset to sunrise.
70     *
71     * @see #setAutoMode(int)
72     */
73    public static final int AUTO_MODE_TWILIGHT = 2;
74
75    private final Context mContext;
76    private final int mUserId;
77
78    private final ContentObserver mContentObserver;
79
80    private Callback mCallback;
81
82    public NightDisplayController(@NonNull Context context) {
83        this(context, ActivityManager.getCurrentUser());
84    }
85
86    public NightDisplayController(@NonNull Context context, int userId) {
87        mContext = context.getApplicationContext();
88        mUserId = userId;
89
90        mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
91            @Override
92            public void onChange(boolean selfChange, Uri uri) {
93                super.onChange(selfChange, uri);
94
95                final String setting = uri == null ? null : uri.getLastPathSegment();
96                if (setting != null) {
97                    onSettingChanged(setting);
98                }
99            }
100        };
101    }
102
103    /**
104     * Returns {@code true} when Night display is activated (the display is tinted red).
105     */
106    public boolean isActivated() {
107        return Secure.getIntForUser(mContext.getContentResolver(),
108                Secure.NIGHT_DISPLAY_ACTIVATED, 0, mUserId) == 1;
109    }
110
111    /**
112     * Sets whether Night display should be activated. This also sets the last activated time.
113     *
114     * @param activated {@code true} if Night display should be activated
115     * @return {@code true} if the activated value was set successfully
116     */
117    public boolean setActivated(boolean activated) {
118        if (isActivated() != activated) {
119            Secure.putLongForUser(mContext.getContentResolver(),
120                    Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME, System.currentTimeMillis(),
121                    mUserId);
122        }
123        return Secure.putIntForUser(mContext.getContentResolver(),
124                Secure.NIGHT_DISPLAY_ACTIVATED, activated ? 1 : 0, mUserId);
125    }
126
127    /**
128     * Returns the time when Night display's activation state last changed, or {@code null} if it
129     * has never been changed.
130     */
131    public Calendar getLastActivatedTime() {
132        final ContentResolver cr = mContext.getContentResolver();
133        final long lastActivatedTimeMillis = Secure.getLongForUser(
134                cr, Secure.NIGHT_DISPLAY_LAST_ACTIVATED_TIME, -1, mUserId);
135        if (lastActivatedTimeMillis < 0) {
136            return null;
137        }
138
139        final Calendar lastActivatedTime = Calendar.getInstance();
140        lastActivatedTime.setTimeInMillis(lastActivatedTimeMillis);
141        return lastActivatedTime;
142    }
143
144    /**
145     * Returns the current auto mode value controlling when Night display will be automatically
146     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
147     * {@link #AUTO_MODE_TWILIGHT}.
148     */
149    public @AutoMode int getAutoMode() {
150        int autoMode = Secure.getIntForUser(mContext.getContentResolver(),
151                Secure.NIGHT_DISPLAY_AUTO_MODE, -1, mUserId);
152        if (autoMode == -1) {
153            if (DEBUG) {
154                Slog.d(TAG, "Using default value for setting: " + Secure.NIGHT_DISPLAY_AUTO_MODE);
155            }
156            autoMode = mContext.getResources().getInteger(
157                    R.integer.config_defaultNightDisplayAutoMode);
158        }
159
160        if (autoMode != AUTO_MODE_DISABLED
161                && autoMode != AUTO_MODE_CUSTOM
162                && autoMode != AUTO_MODE_TWILIGHT) {
163            Slog.e(TAG, "Invalid autoMode: " + autoMode);
164            autoMode = AUTO_MODE_DISABLED;
165        }
166
167        return autoMode;
168    }
169
170    /**
171     * Sets the current auto mode value controlling when Night display will be automatically
172     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
173     * {@link #AUTO_MODE_TWILIGHT}.
174     *
175     * @param autoMode the new auto mode to use
176     * @return {@code true} if new auto mode was set successfully
177     */
178    public boolean setAutoMode(@AutoMode int autoMode) {
179        if (autoMode != AUTO_MODE_DISABLED
180                && autoMode != AUTO_MODE_CUSTOM
181                && autoMode != AUTO_MODE_TWILIGHT) {
182            throw new IllegalArgumentException("Invalid autoMode: " + autoMode);
183        }
184
185        return Secure.putIntForUser(mContext.getContentResolver(),
186                Secure.NIGHT_DISPLAY_AUTO_MODE, autoMode, mUserId);
187    }
188
189    /**
190     * Returns the local time when Night display will be automatically activated when using
191     * {@link #AUTO_MODE_CUSTOM}.
192     */
193    public @NonNull LocalTime getCustomStartTime() {
194        int startTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
195                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, -1, mUserId);
196        if (startTimeValue == -1) {
197            if (DEBUG) {
198                Slog.d(TAG, "Using default value for setting: "
199                        + Secure.NIGHT_DISPLAY_CUSTOM_START_TIME);
200            }
201            startTimeValue = mContext.getResources().getInteger(
202                    R.integer.config_defaultNightDisplayCustomStartTime);
203        }
204
205        return LocalTime.valueOf(startTimeValue);
206    }
207
208    /**
209     * Sets the local time when Night display will be automatically activated when using
210     * {@link #AUTO_MODE_CUSTOM}.
211     *
212     * @param startTime the local time to automatically activate Night display
213     * @return {@code true} if the new custom start time was set successfully
214     */
215    public boolean setCustomStartTime(@NonNull LocalTime startTime) {
216        if (startTime == null) {
217            throw new IllegalArgumentException("startTime cannot be null");
218        }
219        return Secure.putIntForUser(mContext.getContentResolver(),
220                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, startTime.toMillis(), mUserId);
221    }
222
223    /**
224     * Returns the local time when Night display will be automatically deactivated when using
225     * {@link #AUTO_MODE_CUSTOM}.
226     */
227    public @NonNull LocalTime getCustomEndTime() {
228        int endTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
229                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, -1, mUserId);
230        if (endTimeValue == -1) {
231            if (DEBUG) {
232                Slog.d(TAG, "Using default value for setting: "
233                        + Secure.NIGHT_DISPLAY_CUSTOM_END_TIME);
234            }
235            endTimeValue = mContext.getResources().getInteger(
236                    R.integer.config_defaultNightDisplayCustomEndTime);
237        }
238
239        return LocalTime.valueOf(endTimeValue);
240    }
241
242    /**
243     * Sets the local time when Night display will be automatically deactivated when using
244     * {@link #AUTO_MODE_CUSTOM}.
245     *
246     * @param endTime the local time to automatically deactivate Night display
247     * @return {@code true} if the new custom end time was set successfully
248     */
249    public boolean setCustomEndTime(@NonNull LocalTime endTime) {
250        if (endTime == null) {
251            throw new IllegalArgumentException("endTime cannot be null");
252        }
253        return Secure.putIntForUser(mContext.getContentResolver(),
254                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, endTime.toMillis(), mUserId);
255    }
256
257    /**
258     * Returns the color temperature (in Kelvin) to tint the display when activated.
259     */
260    public int getColorTemperature() {
261        int colorTemperature = Secure.getIntForUser(mContext.getContentResolver(),
262                Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, -1, mUserId);
263        if (colorTemperature == -1) {
264            if (DEBUG) {
265                Slog.d(TAG, "Using default value for setting: "
266                    + Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE);
267            }
268            colorTemperature = getDefaultColorTemperature();
269        }
270        final int minimumTemperature = getMinimumColorTemperature();
271        final int maximumTemperature = getMaximumColorTemperature();
272        if (colorTemperature < minimumTemperature) {
273            colorTemperature = minimumTemperature;
274        } else if (colorTemperature > maximumTemperature) {
275            colorTemperature = maximumTemperature;
276        }
277
278        return colorTemperature;
279    }
280
281    /**
282     * Sets the current temperature.
283     *
284     * @param colorTemperature the temperature, in Kelvin.
285     * @return {@code true} if new temperature was set successfully.
286     */
287    public boolean setColorTemperature(int colorTemperature) {
288        return Secure.putIntForUser(mContext.getContentResolver(),
289            Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, colorTemperature, mUserId);
290    }
291
292    /**
293     * Returns the minimum allowed color temperature (in Kelvin) to tint the display when activated.
294     */
295    public int getMinimumColorTemperature() {
296        return mContext.getResources().getInteger(
297                R.integer.config_nightDisplayColorTemperatureMin);
298    }
299
300    /**
301     * Returns the maximum allowed color temperature (in Kelvin) to tint the display when activated.
302     */
303    public int getMaximumColorTemperature() {
304        return mContext.getResources().getInteger(
305                R.integer.config_nightDisplayColorTemperatureMax);
306    }
307
308    /**
309     * Returns the default color temperature (in Kelvin) to tint the display when activated.
310     */
311    public int getDefaultColorTemperature() {
312        return mContext.getResources().getInteger(
313                R.integer.config_nightDisplayColorTemperatureDefault);
314    }
315
316    private void onSettingChanged(@NonNull String setting) {
317        if (DEBUG) {
318            Slog.d(TAG, "onSettingChanged: " + setting);
319        }
320
321        if (mCallback != null) {
322            switch (setting) {
323                case Secure.NIGHT_DISPLAY_ACTIVATED:
324                    mCallback.onActivated(isActivated());
325                    break;
326                case Secure.NIGHT_DISPLAY_AUTO_MODE:
327                    mCallback.onAutoModeChanged(getAutoMode());
328                    break;
329                case Secure.NIGHT_DISPLAY_CUSTOM_START_TIME:
330                    mCallback.onCustomStartTimeChanged(getCustomStartTime());
331                    break;
332                case Secure.NIGHT_DISPLAY_CUSTOM_END_TIME:
333                    mCallback.onCustomEndTimeChanged(getCustomEndTime());
334                    break;
335                case Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE:
336                    mCallback.onColorTemperatureChanged(getColorTemperature());
337                    break;
338            }
339        }
340    }
341
342    /**
343     * Register a callback to be invoked whenever the Night display settings are changed.
344     */
345    public void setListener(Callback callback) {
346        final Callback oldCallback = mCallback;
347        if (oldCallback != callback) {
348            mCallback = callback;
349
350            if (callback == null) {
351                // Stop listening for changes now that there IS NOT a listener.
352                mContext.getContentResolver().unregisterContentObserver(mContentObserver);
353            } else if (oldCallback == null) {
354                // Start listening for changes now that there IS a listener.
355                final ContentResolver cr = mContext.getContentResolver();
356                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_ACTIVATED),
357                        false /* notifyForDescendants */, mContentObserver, mUserId);
358                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_AUTO_MODE),
359                        false /* notifyForDescendants */, mContentObserver, mUserId);
360                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_START_TIME),
361                        false /* notifyForDescendants */, mContentObserver, mUserId);
362                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_END_TIME),
363                        false /* notifyForDescendants */, mContentObserver, mUserId);
364                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE),
365                        false /* notifyForDescendants */, mContentObserver, mUserId);
366            }
367        }
368    }
369
370    /**
371     * Returns {@code true} if Night display is supported by the device.
372     */
373    public static boolean isAvailable(Context context) {
374        return context.getResources().getBoolean(R.bool.config_nightDisplayAvailable);
375    }
376
377    /**
378     * A time without a time-zone or date.
379     */
380    public static class LocalTime {
381
382        /**
383         * The hour of the day from 0 - 23.
384         */
385        public final int hourOfDay;
386        /**
387         * The minute within the hour from 0 - 59.
388         */
389        public final int minute;
390
391        public LocalTime(int hourOfDay, int minute) {
392            if (hourOfDay < 0 || hourOfDay > 23) {
393                throw new IllegalArgumentException("Invalid hourOfDay: " + hourOfDay);
394            } else if (minute < 0 || minute > 59) {
395                throw new IllegalArgumentException("Invalid minute: " + minute);
396            }
397
398            this.hourOfDay = hourOfDay;
399            this.minute = minute;
400        }
401
402        /**
403         * Returns the first date time corresponding to this local time that occurs before the
404         * provided date time.
405         *
406         * @param time the date time to compare against
407         * @return the prior date time corresponding to this local time
408         */
409        public Calendar getDateTimeBefore(Calendar time) {
410            final Calendar c = Calendar.getInstance();
411            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
412            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
413
414            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
415            c.set(Calendar.MINUTE, minute);
416            c.set(Calendar.SECOND, 0);
417            c.set(Calendar.MILLISECOND, 0);
418
419            // Check if the local time has past, if so return the same time tomorrow.
420            if (c.after(time)) {
421                c.add(Calendar.DATE, -1);
422            }
423
424            return c;
425        }
426
427        /**
428         * Returns the first date time corresponding to this local time that occurs after the
429         * provided date time.
430         *
431         * @param time the date time to compare against
432         * @return the next date time corresponding to this local time
433         */
434        public Calendar getDateTimeAfter(Calendar time) {
435            final Calendar c = Calendar.getInstance();
436            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
437            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
438
439            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
440            c.set(Calendar.MINUTE, minute);
441            c.set(Calendar.SECOND, 0);
442            c.set(Calendar.MILLISECOND, 0);
443
444            // Check if the local time has past, if so return the same time tomorrow.
445            if (c.before(time)) {
446                c.add(Calendar.DATE, 1);
447            }
448
449            return c;
450        }
451
452        /**
453         * Returns a local time corresponding the given number of milliseconds from midnight.
454         *
455         * @param millis the number of milliseconds from midnight
456         * @return the corresponding local time
457         */
458        private static LocalTime valueOf(int millis) {
459            final int hourOfDay = (millis / 3600000) % 24;
460            final int minutes = (millis / 60000) % 60;
461            return new LocalTime(hourOfDay, minutes);
462        }
463
464        /**
465         * Returns the local time represented as milliseconds from midnight.
466         */
467        private int toMillis() {
468            return hourOfDay * 3600000 + minute * 60000;
469        }
470
471        @Override
472        public String toString() {
473            return String.format(Locale.US, "%02d:%02d", hourOfDay, minute);
474        }
475    }
476
477    /**
478     * Callback invoked whenever the Night display settings are changed.
479     */
480    public interface Callback {
481        /**
482         * Callback invoked when the activated state changes.
483         *
484         * @param activated {@code true} if Night display is activated
485         */
486        default void onActivated(boolean activated) {}
487        /**
488         * Callback invoked when the auto mode changes.
489         *
490         * @param autoMode the auto mode to use
491         */
492        default void onAutoModeChanged(int autoMode) {}
493        /**
494         * Callback invoked when the time to automatically activate Night display changes.
495         *
496         * @param startTime the local time to automatically activate Night display
497         */
498        default void onCustomStartTimeChanged(LocalTime startTime) {}
499        /**
500         * Callback invoked when the time to automatically deactivate Night display changes.
501         *
502         * @param endTime the local time to automatically deactivate Night display
503         */
504        default void onCustomEndTimeChanged(LocalTime endTime) {}
505
506        /**
507         * Callback invoked when the color temperature changes.
508         *
509         * @param colorTemperature the color temperature to tint the screen
510         */
511        default void onColorTemperatureChanged(int colorTemperature) {}
512    }
513}
514