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 
17 package com.android.internal.app;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.app.ActivityManager;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.net.Uri;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.provider.Settings.Secure;
29 import android.util.Slog;
30 
31 import com.android.internal.R;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.Calendar;
36 import 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  */
44 public 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 
NightDisplayController(@onNull Context context)82     public NightDisplayController(@NonNull Context context) {
83         this(context, ActivityManager.getCurrentUser());
84     }
85 
NightDisplayController(@onNull Context context, int userId)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      */
isActivated()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      */
setActivated(boolean activated)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      */
getLastActivatedTime()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      */
getAutoMode()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      */
setAutoMode(@utoMode int autoMode)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      */
getCustomStartTime()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      */
setCustomStartTime(@onNull LocalTime startTime)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      */
getCustomEndTime()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      */
setCustomEndTime(@onNull LocalTime endTime)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      */
getColorTemperature()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      */
setColorTemperature(int colorTemperature)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      */
getMinimumColorTemperature()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      */
getMaximumColorTemperature()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      */
getDefaultColorTemperature()311     public int getDefaultColorTemperature() {
312         return mContext.getResources().getInteger(
313                 R.integer.config_nightDisplayColorTemperatureDefault);
314     }
315 
onSettingChanged(@onNull String setting)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      */
setListener(Callback callback)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      */
isAvailable(Context context)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 
LocalTime(int hourOfDay, int minute)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          */
getDateTimeBefore(Calendar time)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          */
getDateTimeAfter(Calendar time)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          */
valueOf(int millis)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          */
toMillis()467         private int toMillis() {
468             return hourOfDay * 3600000 + minute * 60000;
469         }
470 
471         @Override
toString()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          */
onActivated(boolean activated)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          */
onAutoModeChanged(int autoMode)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          */
onCustomStartTimeChanged(LocalTime startTime)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          */
onCustomEndTimeChanged(LocalTime endTime)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          */
onColorTemperatureChanged(int colorTemperature)511         default void onColorTemperatureChanged(int colorTemperature) {}
512     }
513 }
514