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 }