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 }