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