1/*
2 * Copyright (C) 2015 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.deskclock.data;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.Resources;
22import android.net.Uri;
23import android.provider.Settings;
24import android.support.annotation.NonNull;
25import android.text.format.DateUtils;
26
27import com.android.deskclock.R;
28import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
29import com.android.deskclock.data.DataModel.CitySort;
30import com.android.deskclock.data.DataModel.ClockStyle;
31import com.android.deskclock.settings.ScreensaverSettingsActivity;
32import com.android.deskclock.settings.SettingsActivity;
33
34import java.util.Arrays;
35import java.util.Calendar;
36import java.util.Locale;
37import java.util.TimeZone;
38
39import static android.text.format.DateUtils.HOUR_IN_MILLIS;
40import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
41import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
42import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
43import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
44import static com.android.deskclock.data.Weekdays.Order.MON_TO_SUN;
45import static com.android.deskclock.data.Weekdays.Order.SAT_TO_FRI;
46import static com.android.deskclock.data.Weekdays.Order.SUN_TO_SAT;
47import static java.util.Calendar.MONDAY;
48import static java.util.Calendar.SATURDAY;
49import static java.util.Calendar.SUNDAY;
50
51/**
52 * This class encapsulates the storage of application preferences in {@link SharedPreferences}.
53 */
54final class SettingsDAO {
55
56    /** Key to a preference that stores the preferred sort order of world cities. */
57    private static final String KEY_SORT_PREFERENCE = "sort_preference";
58
59    /** Key to a preference that stores the default ringtone for new alarms. */
60    private static final String KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri";
61
62    /** Key to a preference that stores the global broadcast id. */
63    private static final String KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id";
64
65    /** Key to a preference that indicates whether restore (of backup and restore) has completed. */
66    private static final String KEY_RESTORE_BACKUP_FINISHED = "restore_finished";
67
68    private SettingsDAO() {}
69
70    /**
71     * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
72     */
73    static int getGlobalIntentId(SharedPreferences prefs) {
74        return prefs.getInt(KEY_ALARM_GLOBAL_ID, -1);
75    }
76
77    /**
78     * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
79     */
80    static void updateGlobalIntentId(SharedPreferences prefs) {
81        final int globalId = prefs.getInt(KEY_ALARM_GLOBAL_ID, -1) + 1;
82        prefs.edit().putInt(KEY_ALARM_GLOBAL_ID, globalId).apply();
83    }
84
85    /**
86     * @return an enumerated value indicating the order in which cities are ordered
87     */
88    static CitySort getCitySort(SharedPreferences prefs) {
89        final int defaultSortOrdinal = CitySort.NAME.ordinal();
90        final int citySortOrdinal = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal);
91        return CitySort.values()[citySortOrdinal];
92    }
93
94    /**
95     * Adjust the sort order of cities.
96     */
97    static void toggleCitySort(SharedPreferences prefs) {
98        final CitySort oldSort = getCitySort(prefs);
99        final CitySort newSort = oldSort == CitySort.NAME ? CitySort.UTC_OFFSET : CitySort.NAME;
100        prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal()).apply();
101    }
102
103    /**
104     * @return {@code true} if a clock for the user's home timezone should be automatically
105     *      displayed when it doesn't match the current timezone
106     */
107    static boolean getAutoShowHomeClock(SharedPreferences prefs) {
108        return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true);
109    }
110
111    /**
112     * @return the user's home timezone
113     */
114    static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) {
115        String timeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, null);
116
117        // If the recorded home timezone is legal, use it.
118        final TimeZones timeZones = getTimeZones(context, System.currentTimeMillis());
119        if (timeZones.contains(timeZoneId)) {
120            return TimeZone.getTimeZone(timeZoneId);
121        }
122
123        // No legal home timezone has yet been recorded, attempt to record the default.
124        timeZoneId = defaultTZ.getID();
125        if (timeZones.contains(timeZoneId)) {
126            prefs.edit().putString(SettingsActivity.KEY_HOME_TZ, timeZoneId).apply();
127        }
128
129        // The timezone returned here may be valid or invalid. When it matches TimeZone.getDefault()
130        // the Home city will not show, regardless of its validity.
131        return defaultTZ;
132    }
133
134    /**
135     * @return a value indicating whether analog or digital clocks are displayed in the app
136     */
137    static ClockStyle getClockStyle(Context context, SharedPreferences prefs) {
138        return getClockStyle(context, prefs, SettingsActivity.KEY_CLOCK_STYLE);
139    }
140
141    /**
142     * @return a value indicating whether analog or digital clocks are displayed in the app
143     */
144    static boolean getDisplayClockSeconds(SharedPreferences prefs) {
145       return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false);
146    }
147
148    /**
149     * @param displaySeconds whether or not to display seconds on main clock
150     */
151    static void setDisplayClockSeconds(SharedPreferences prefs, boolean displaySeconds) {
152        prefs.edit().putBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, displaySeconds).apply();
153    }
154
155    /**
156     * Sets the user's display seconds preference based on the currently selected clock if one has
157     * not yet been manually chosen.
158     */
159    static void setDefaultDisplayClockSeconds(Context context, SharedPreferences prefs) {
160        if (!prefs.contains(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS)) {
161            // If on analog clock style on upgrade, default to true. Otherwise, default to false.
162            final boolean isAnalog = getClockStyle(context, prefs) == ClockStyle.ANALOG;
163            setDisplayClockSeconds(prefs, isAnalog);
164        }
165    }
166
167    /**
168     * @return a value indicating whether analog or digital clocks are displayed on the screensaver
169     */
170    static ClockStyle getScreensaverClockStyle(Context context, SharedPreferences prefs) {
171        return getClockStyle(context, prefs, ScreensaverSettingsActivity.KEY_CLOCK_STYLE);
172    }
173
174    /**
175     * @return {@code true} if the screen saver should be dimmed for lower contrast at night
176     */
177    static boolean getScreensaverNightModeOn(SharedPreferences prefs) {
178        return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false);
179    }
180
181    /**
182     * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
183     *      has yet been made
184     */
185    static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) {
186        final String uriString = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null);
187        return uriString == null ? defaultUri : Uri.parse(uriString);
188    }
189
190    /**
191     * @return whether timer vibration is enabled. false by default.
192     */
193    static boolean getTimerVibrate(SharedPreferences prefs) {
194        return prefs.getBoolean(SettingsActivity.KEY_TIMER_VIBRATE, false);
195    }
196
197    /**
198     * @param enabled whether vibration will be turned on for all timers.
199     */
200    static void setTimerVibrate(SharedPreferences prefs, boolean enabled) {
201        prefs.edit().putBoolean(SettingsActivity.KEY_TIMER_VIBRATE, enabled).apply();
202    }
203
204    /**
205     * @param uri the uri of the ringtone to play for all timers
206     */
207    static void setTimerRingtoneUri(SharedPreferences prefs, Uri uri) {
208        prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply();
209    }
210
211    /**
212     * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
213     *      has yet been made
214     */
215    static Uri getDefaultAlarmRingtoneUri(SharedPreferences prefs) {
216        final String uriString = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null);
217        return uriString == null ? Settings.System.DEFAULT_ALARM_ALERT_URI : Uri.parse(uriString);
218    }
219
220    /**
221     * @param uri identifies the default ringtone to play for new alarms
222     */
223    static void setDefaultAlarmRingtoneUri(SharedPreferences prefs, Uri uri) {
224        prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply();
225    }
226
227    /**
228     * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
229     *      {@code 0} implies no crescendo should be applied
230     */
231    static long getAlarmCrescendoDuration(SharedPreferences prefs) {
232        final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0");
233        return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
234    }
235
236    /**
237     * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
238     *      {@code 0} implies no crescendo should be applied
239     */
240    static long getTimerCrescendoDuration(SharedPreferences prefs) {
241        final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0");
242        return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
243    }
244
245    /**
246     * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
247     *      {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
248     */
249    static Weekdays.Order getWeekdayOrder(SharedPreferences prefs) {
250        final String defaultValue = String.valueOf(Calendar.getInstance().getFirstDayOfWeek());
251        final String value = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue);
252        final int firstCalendarDay = Integer.parseInt(value);
253        switch (firstCalendarDay) {
254            case SATURDAY: return SAT_TO_FRI;
255            case SUNDAY: return SUN_TO_SAT;
256            case MONDAY: return MON_TO_SUN;
257            default:
258                throw new IllegalArgumentException("Unknown weekday: " + firstCalendarDay);
259        }
260    }
261
262    /**
263     * @return {@code true} if the restore process (of backup and restore) has completed
264     */
265    static boolean isRestoreBackupFinished(SharedPreferences prefs) {
266        return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false);
267    }
268
269    /**
270     * @param finished {@code true} means the restore process (of backup and restore) has completed
271     */
272    static void setRestoreBackupFinished(SharedPreferences prefs, boolean finished) {
273        if (finished) {
274            prefs.edit().putBoolean(KEY_RESTORE_BACKUP_FINISHED, true).apply();
275        } else {
276            prefs.edit().remove(KEY_RESTORE_BACKUP_FINISHED).apply();
277        }
278    }
279
280    /**
281     * @return the behavior to execute when volume buttons are pressed while firing an alarm
282     */
283    static AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior(SharedPreferences prefs) {
284        final String defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR;
285        final String value = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue);
286        switch (value) {
287            case SettingsActivity.DEFAULT_VOLUME_BEHAVIOR: return NOTHING;
288            case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE: return SNOOZE;
289            case SettingsActivity.VOLUME_BEHAVIOR_DISMISS: return DISMISS;
290            default:
291                throw new IllegalArgumentException("Unknown volume button behavior: " + value);
292        }
293    }
294
295    /**
296     * @return the number of minutes an alarm may ring before it has timed out and becomes missed
297     */
298    static int getAlarmTimeout(SharedPreferences prefs) {
299        // Default value must match the one in res/xml/settings.xml
300        final String string = prefs.getString(SettingsActivity.KEY_AUTO_SILENCE, "10");
301        return Integer.parseInt(string);
302    }
303
304    /**
305     * @return the number of minutes an alarm will remain snoozed before it rings again
306     */
307    static int getSnoozeLength(SharedPreferences prefs) {
308        // Default value must match the one in res/xml/settings.xml
309        final String string = prefs.getString(SettingsActivity.KEY_ALARM_SNOOZE, "10");
310        return Integer.parseInt(string);
311    }
312
313    /**
314     * @param currentTime timezone offsets created relative to this time
315     * @return a description of the time zones available for selection
316     */
317    static TimeZones getTimeZones(Context context, long currentTime) {
318        final Locale locale = Locale.getDefault();
319        final Resources resources = context.getResources();
320        final String[] timeZoneIds = resources.getStringArray(R.array.timezone_values);
321        final String[] timeZoneNames = resources.getStringArray(R.array.timezone_labels);
322
323        // Verify the data is consistent.
324        if (timeZoneIds.length != timeZoneNames.length) {
325            final String message = String.format(Locale.US,
326                    "id count (%d) does not match name count (%d) for locale %s",
327                    timeZoneIds.length, timeZoneNames.length, locale);
328            throw new IllegalStateException(message);
329        }
330
331        // Create TimeZoneDescriptors for each TimeZone so they can be sorted.
332        final TimeZoneDescriptor[] descriptors = new TimeZoneDescriptor[timeZoneIds.length];
333        for (int i = 0; i < timeZoneIds.length; i++) {
334            final String id = timeZoneIds[i];
335            final String name = timeZoneNames[i].replaceAll("\"", "");
336            descriptors[i] = new TimeZoneDescriptor(locale, id, name, currentTime);
337        }
338        Arrays.sort(descriptors);
339
340        // Transfer the TimeZoneDescriptors into parallel arrays for easy consumption by the caller.
341        final CharSequence[] tzIds = new CharSequence[descriptors.length];
342        final CharSequence[] tzNames = new CharSequence[descriptors.length];
343        for (int i = 0; i < descriptors.length; i++) {
344            final TimeZoneDescriptor descriptor = descriptors[i];
345            tzIds[i] = descriptor.mTimeZoneId;
346            tzNames[i] = descriptor.mTimeZoneName;
347        }
348
349        return new TimeZones(tzIds, tzNames);
350    }
351
352    private static ClockStyle getClockStyle(Context context, SharedPreferences prefs, String key) {
353        final String defaultStyle = context.getString(R.string.default_clock_style);
354        final String clockStyle = prefs.getString(key, defaultStyle);
355        // Use hardcoded locale to perform toUpperCase, because in some languages toUpperCase adds
356        // accent to character, which breaks the enum conversion.
357        return ClockStyle.valueOf(clockStyle.toUpperCase(Locale.US));
358    }
359
360    /**
361     * These descriptors have a natural order from furthest ahead of GMT to furthest behind GMT.
362     */
363    private static class TimeZoneDescriptor implements Comparable<TimeZoneDescriptor> {
364
365        private final int mOffset;
366        private final String mTimeZoneId;
367        private final String mTimeZoneName;
368
369        private TimeZoneDescriptor(Locale locale, String id, String name, long currentTime) {
370            mTimeZoneId = id;
371
372            final TimeZone tz = TimeZone.getTimeZone(id);
373            mOffset = tz.getOffset(currentTime);
374
375            final char sign = mOffset < 0 ? '-' : '+';
376            final int absoluteGMTOffset = Math.abs(mOffset);
377            final long hour = absoluteGMTOffset / HOUR_IN_MILLIS;
378            final long minute = (absoluteGMTOffset / MINUTE_IN_MILLIS) % 60;
379            mTimeZoneName = String.format(locale, "(GMT%s%d:%02d) %s", sign, hour, minute, name);
380        }
381
382        @Override
383        public int compareTo(@NonNull TimeZoneDescriptor other) {
384            return mOffset - other.mOffset;
385        }
386    }
387}