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 
17 package com.android.deskclock.data;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.res.Resources;
22 import android.net.Uri;
23 import android.provider.Settings;
24 import androidx.annotation.NonNull;
25 import android.text.format.DateUtils;
26 
27 import com.android.deskclock.R;
28 import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
29 import com.android.deskclock.data.DataModel.CitySort;
30 import com.android.deskclock.data.DataModel.ClockStyle;
31 import com.android.deskclock.settings.ScreensaverSettingsActivity;
32 import com.android.deskclock.settings.SettingsActivity;
33 
34 import java.util.Arrays;
35 import java.util.Calendar;
36 import java.util.Locale;
37 import java.util.TimeZone;
38 
39 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
40 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
41 import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
42 import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
43 import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
44 import static com.android.deskclock.data.Weekdays.Order.MON_TO_SUN;
45 import static com.android.deskclock.data.Weekdays.Order.SAT_TO_FRI;
46 import static com.android.deskclock.data.Weekdays.Order.SUN_TO_SAT;
47 import static java.util.Calendar.MONDAY;
48 import static java.util.Calendar.SATURDAY;
49 import static java.util.Calendar.SUNDAY;
50 
51 /**
52  * This class encapsulates the storage of application preferences in {@link SharedPreferences}.
53  */
54 final 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 
SettingsDAO()68     private SettingsDAO() {}
69 
70     /**
71      * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
72      */
getGlobalIntentId(SharedPreferences prefs)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      */
updateGlobalIntentId(SharedPreferences prefs)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      */
getCitySort(SharedPreferences prefs)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      */
toggleCitySort(SharedPreferences prefs)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      */
getAutoShowHomeClock(SharedPreferences prefs)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      */
getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ)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      */
getClockStyle(Context context, SharedPreferences prefs)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      */
getDisplayClockSeconds(SharedPreferences prefs)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      */
setDisplayClockSeconds(SharedPreferences prefs, boolean displaySeconds)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      */
setDefaultDisplayClockSeconds(Context context, SharedPreferences prefs)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      */
getScreensaverClockStyle(Context context, SharedPreferences prefs)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      */
getScreensaverNightModeOn(SharedPreferences prefs)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      */
getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri)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      */
getTimerVibrate(SharedPreferences prefs)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      */
setTimerVibrate(SharedPreferences prefs, boolean enabled)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      */
setTimerRingtoneUri(SharedPreferences prefs, Uri uri)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      */
getDefaultAlarmRingtoneUri(SharedPreferences prefs)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      */
setDefaultAlarmRingtoneUri(SharedPreferences prefs, Uri uri)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      */
getAlarmCrescendoDuration(SharedPreferences prefs)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      */
getTimerCrescendoDuration(SharedPreferences prefs)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      */
getWeekdayOrder(SharedPreferences prefs)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      */
isRestoreBackupFinished(SharedPreferences prefs)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      */
setRestoreBackupFinished(SharedPreferences prefs, boolean finished)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      */
getAlarmVolumeButtonBehavior(SharedPreferences prefs)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      */
getAlarmTimeout(SharedPreferences prefs)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      */
getSnoozeLength(SharedPreferences prefs)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      */
getTimeZones(Context context, long currentTime)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 
getClockStyle(Context context, SharedPreferences prefs, String key)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 
TimeZoneDescriptor(Locale locale, String id, String name, long currentTime)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 }