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.content.ContentResolver;
22import android.content.Context;
23import android.database.ContentObserver;
24import android.net.Uri;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.UserHandle;
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    /** @hide */
50    @Retention(RetentionPolicy.SOURCE)
51    @IntDef({ AUTO_MODE_DISABLED, AUTO_MODE_CUSTOM, AUTO_MODE_TWILIGHT })
52    public @interface AutoMode {}
53
54    /**
55     * Auto mode value to prevent Night display from being automatically activated. It can still
56     * be activated manually via {@link #setActivated(boolean)}.
57     *
58     * @see #setAutoMode(int)
59     */
60    public static final int AUTO_MODE_DISABLED = 0;
61    /**
62     * Auto mode value to automatically activate Night display at a specific start and end time.
63     *
64     * @see #setAutoMode(int)
65     * @see #setCustomStartTime(LocalTime)
66     * @see #setCustomEndTime(LocalTime)
67     */
68    public static final int AUTO_MODE_CUSTOM = 1;
69    /**
70     * Auto mode value to automatically activate Night display from sunset to sunrise.
71     *
72     * @see #setAutoMode(int)
73     */
74    public static final int AUTO_MODE_TWILIGHT = 2;
75
76    private final Context mContext;
77    private final int mUserId;
78
79    private final ContentObserver mContentObserver;
80
81    private Callback mCallback;
82
83    public NightDisplayController(@NonNull Context context) {
84        this(context, UserHandle.myUserId());
85    }
86
87    public NightDisplayController(@NonNull Context context, int userId) {
88        mContext = context.getApplicationContext();
89        mUserId = userId;
90
91        mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
92            @Override
93            public void onChange(boolean selfChange, Uri uri) {
94                super.onChange(selfChange, uri);
95
96                final String setting = uri == null ? null : uri.getLastPathSegment();
97                if (setting != null) {
98                    onSettingChanged(setting);
99                }
100            }
101        };
102    }
103
104    /**
105     * Returns {@code true} when Night display is activated (the display is tinted red).
106     */
107    public boolean isActivated() {
108        return Secure.getIntForUser(mContext.getContentResolver(),
109                Secure.NIGHT_DISPLAY_ACTIVATED, 0, mUserId) == 1;
110    }
111
112    /**
113     * Sets whether Night display should be activated.
114     *
115     * @param activated {@code true} if Night display should be activated
116     * @return {@code true} if the activated value was set successfully
117     */
118    public boolean setActivated(boolean activated) {
119        return Secure.putIntForUser(mContext.getContentResolver(),
120                Secure.NIGHT_DISPLAY_ACTIVATED, activated ? 1 : 0, mUserId);
121    }
122
123    /**
124     * Returns the current auto mode value controlling when Night display will be automatically
125     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
126     * {@link #AUTO_MODE_TWILIGHT}.
127     */
128    public @AutoMode int getAutoMode() {
129        int autoMode = Secure.getIntForUser(mContext.getContentResolver(),
130                Secure.NIGHT_DISPLAY_AUTO_MODE, -1, mUserId);
131        if (autoMode == -1) {
132            if (DEBUG) {
133                Slog.d(TAG, "Using default value for setting: " + Secure.NIGHT_DISPLAY_AUTO_MODE);
134            }
135            autoMode = mContext.getResources().getInteger(
136                    R.integer.config_defaultNightDisplayAutoMode);
137        }
138
139        if (autoMode != AUTO_MODE_DISABLED
140                && autoMode != AUTO_MODE_CUSTOM
141                && autoMode != AUTO_MODE_TWILIGHT) {
142            Slog.e(TAG, "Invalid autoMode: " + autoMode);
143            autoMode = AUTO_MODE_DISABLED;
144        }
145
146        return autoMode;
147    }
148
149    /**
150     * Sets the current auto mode value controlling when Night display will be automatically
151     * activated. One of {@link #AUTO_MODE_DISABLED}, {@link #AUTO_MODE_CUSTOM}, or
152     * {@link #AUTO_MODE_TWILIGHT}.
153     *
154     * @param autoMode the new auto mode to use
155     * @return {@code true} if new auto mode was set successfully
156     */
157    public boolean setAutoMode(@AutoMode int autoMode) {
158        if (autoMode != AUTO_MODE_DISABLED
159                && autoMode != AUTO_MODE_CUSTOM
160                && autoMode != AUTO_MODE_TWILIGHT) {
161            throw new IllegalArgumentException("Invalid autoMode: " + autoMode);
162        }
163
164        return Secure.putIntForUser(mContext.getContentResolver(),
165                Secure.NIGHT_DISPLAY_AUTO_MODE, autoMode, mUserId);
166    }
167
168    /**
169     * Returns the local time when Night display will be automatically activated when using
170     * {@link #AUTO_MODE_CUSTOM}.
171     */
172    public @NonNull LocalTime getCustomStartTime() {
173        int startTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
174                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, -1, mUserId);
175        if (startTimeValue == -1) {
176            if (DEBUG) {
177                Slog.d(TAG, "Using default value for setting: "
178                        + Secure.NIGHT_DISPLAY_CUSTOM_START_TIME);
179            }
180            startTimeValue = mContext.getResources().getInteger(
181                    R.integer.config_defaultNightDisplayCustomStartTime);
182        }
183
184        return LocalTime.valueOf(startTimeValue);
185    }
186
187    /**
188     * Sets the local time when Night display will be automatically activated when using
189     * {@link #AUTO_MODE_CUSTOM}.
190     *
191     * @param startTime the local time to automatically activate Night display
192     * @return {@code true} if the new custom start time was set successfully
193     */
194    public boolean setCustomStartTime(@NonNull LocalTime startTime) {
195        if (startTime == null) {
196            throw new IllegalArgumentException("startTime cannot be null");
197        }
198        return Secure.putIntForUser(mContext.getContentResolver(),
199                Secure.NIGHT_DISPLAY_CUSTOM_START_TIME, startTime.toMillis(), mUserId);
200    }
201
202    /**
203     * Returns the local time when Night display will be automatically deactivated when using
204     * {@link #AUTO_MODE_CUSTOM}.
205     */
206    public @NonNull LocalTime getCustomEndTime() {
207        int endTimeValue = Secure.getIntForUser(mContext.getContentResolver(),
208                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, -1, mUserId);
209        if (endTimeValue == -1) {
210            if (DEBUG) {
211                Slog.d(TAG, "Using default value for setting: "
212                        + Secure.NIGHT_DISPLAY_CUSTOM_END_TIME);
213            }
214            endTimeValue = mContext.getResources().getInteger(
215                    R.integer.config_defaultNightDisplayCustomEndTime);
216        }
217
218        return LocalTime.valueOf(endTimeValue);
219    }
220
221    /**
222     * Sets the local time when Night display will be automatically deactivated when using
223     * {@link #AUTO_MODE_CUSTOM}.
224     *
225     * @param endTime the local time to automatically deactivate Night display
226     * @return {@code true} if the new custom end time was set successfully
227     */
228    public boolean setCustomEndTime(@NonNull LocalTime endTime) {
229        if (endTime == null) {
230            throw new IllegalArgumentException("endTime cannot be null");
231        }
232        return Secure.putIntForUser(mContext.getContentResolver(),
233                Secure.NIGHT_DISPLAY_CUSTOM_END_TIME, endTime.toMillis(), mUserId);
234    }
235
236    private void onSettingChanged(@NonNull String setting) {
237        if (DEBUG) {
238            Slog.d(TAG, "onSettingChanged: " + setting);
239        }
240
241        if (mCallback != null) {
242            switch (setting) {
243                case Secure.NIGHT_DISPLAY_ACTIVATED:
244                    mCallback.onActivated(isActivated());
245                    break;
246                case Secure.NIGHT_DISPLAY_AUTO_MODE:
247                    mCallback.onAutoModeChanged(getAutoMode());
248                    break;
249                case Secure.NIGHT_DISPLAY_CUSTOM_START_TIME:
250                    mCallback.onCustomStartTimeChanged(getCustomStartTime());
251                    break;
252                case Secure.NIGHT_DISPLAY_CUSTOM_END_TIME:
253                    mCallback.onCustomEndTimeChanged(getCustomEndTime());
254                    break;
255            }
256        }
257    }
258
259    /**
260     * Register a callback to be invoked whenever the Night display settings are changed.
261     */
262    public void setListener(Callback callback) {
263        final Callback oldCallback = mCallback;
264        if (oldCallback != callback) {
265            mCallback = callback;
266
267            if (callback == null) {
268                // Stop listening for changes now that there IS NOT a listener.
269                mContext.getContentResolver().unregisterContentObserver(mContentObserver);
270            } else if (oldCallback == null) {
271                // Start listening for changes now that there IS a listener.
272                final ContentResolver cr = mContext.getContentResolver();
273                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_ACTIVATED),
274                        false /* notifyForDescendants */, mContentObserver, mUserId);
275                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_AUTO_MODE),
276                        false /* notifyForDescendants */, mContentObserver, mUserId);
277                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_START_TIME),
278                        false /* notifyForDescendants */, mContentObserver, mUserId);
279                cr.registerContentObserver(Secure.getUriFor(Secure.NIGHT_DISPLAY_CUSTOM_END_TIME),
280                        false /* notifyForDescendants */, mContentObserver, mUserId);
281            }
282        }
283    }
284
285    /**
286     * Returns {@code true} if Night display is supported by the device.
287     */
288    public static boolean isAvailable(Context context) {
289        return context.getResources().getBoolean(R.bool.config_nightDisplayAvailable);
290    }
291
292    /**
293     * A time without a time-zone or date.
294     */
295    public static class LocalTime {
296
297        /**
298         * The hour of the day from 0 - 23.
299         */
300        public final int hourOfDay;
301        /**
302         * The minute within the hour from 0 - 59.
303         */
304        public final int minute;
305
306        public LocalTime(int hourOfDay, int minute) {
307            if (hourOfDay < 0 || hourOfDay > 23) {
308                throw new IllegalArgumentException("Invalid hourOfDay: " + hourOfDay);
309            } else if (minute < 0 || minute > 59) {
310                throw new IllegalArgumentException("Invalid minute: " + minute);
311            }
312
313            this.hourOfDay = hourOfDay;
314            this.minute = minute;
315        }
316
317        /**
318         * Returns the first date time corresponding to this local time that occurs before the
319         * provided date time.
320         *
321         * @param time the date time to compare against
322         * @return the prior date time corresponding to this local time
323         */
324        public Calendar getDateTimeBefore(Calendar time) {
325            final Calendar c = Calendar.getInstance();
326            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
327            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
328
329            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
330            c.set(Calendar.MINUTE, minute);
331            c.set(Calendar.SECOND, 0);
332            c.set(Calendar.MILLISECOND, 0);
333
334            // Check if the local time has past, if so return the same time tomorrow.
335            if (c.after(time)) {
336                c.add(Calendar.DATE, -1);
337            }
338
339            return c;
340        }
341
342        /**
343         * Returns the first date time corresponding to this local time that occurs after the
344         * provided date time.
345         *
346         * @param time the date time to compare against
347         * @return the next date time corresponding to this local time
348         */
349        public Calendar getDateTimeAfter(Calendar time) {
350            final Calendar c = Calendar.getInstance();
351            c.set(Calendar.YEAR, time.get(Calendar.YEAR));
352            c.set(Calendar.DAY_OF_YEAR, time.get(Calendar.DAY_OF_YEAR));
353
354            c.set(Calendar.HOUR_OF_DAY, hourOfDay);
355            c.set(Calendar.MINUTE, minute);
356            c.set(Calendar.SECOND, 0);
357            c.set(Calendar.MILLISECOND, 0);
358
359            // Check if the local time has past, if so return the same time tomorrow.
360            if (c.before(time)) {
361                c.add(Calendar.DATE, 1);
362            }
363
364            return c;
365        }
366
367        /**
368         * Returns a local time corresponding the given number of milliseconds from midnight.
369         *
370         * @param millis the number of milliseconds from midnight
371         * @return the corresponding local time
372         */
373        private static LocalTime valueOf(int millis) {
374            final int hourOfDay = (millis / 3600000) % 24;
375            final int minutes = (millis / 60000) % 60;
376            return new LocalTime(hourOfDay, minutes);
377        }
378
379        /**
380         * Returns the local time represented as milliseconds from midnight.
381         */
382        private int toMillis() {
383            return hourOfDay * 3600000 + minute * 60000;
384        }
385
386        @Override
387        public String toString() {
388            return String.format(Locale.US, "%02d:%02d", hourOfDay, minute);
389        }
390    }
391
392    /**
393     * Callback invoked whenever the Night display settings are changed.
394     */
395    public interface Callback {
396        /**
397         * Callback invoked when the activated state changes.
398         *
399         * @param activated {@code true} if Night display is activated
400         */
401        default void onActivated(boolean activated) {}
402        /**
403         * Callback invoked when the auto mode changes.
404         *
405         * @param autoMode the auto mode to use
406         */
407        default void onAutoModeChanged(int autoMode) {}
408        /**
409         * Callback invoked when the time to automatically activate Night display changes.
410         *
411         * @param startTime the local time to automatically activate Night display
412         */
413        default void onCustomStartTimeChanged(LocalTime startTime) {}
414        /**
415         * Callback invoked when the time to automatically deactivate Night display changes.
416         *
417         * @param endTime the local time to automatically deactivate Night display
418         */
419        default void onCustomEndTimeChanged(LocalTime endTime) {}
420    }
421}
422