1 /*
2  * Copyright (C) 2021 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 package com.android.calendar
17 
18 import android.app.Activity
19 import android.app.FragmentManager
20 import android.app.backup.BackupManager
21 import android.content.Context
22 import android.content.Intent
23 import android.content.SharedPreferences
24 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
25 import android.media.Ringtone
26 import android.media.RingtoneManager
27 import android.net.Uri
28 import android.os.Bundle
29 import android.os.Vibrator
30 import android.preference.CheckBoxPreference
31 import android.preference.ListPreference
32 import android.preference.Preference
33 import android.preference.Preference.OnPreferenceChangeListener
34 import android.preference.Preference.OnPreferenceClickListener
35 import android.preference.PreferenceCategory
36 import android.preference.PreferenceFragment
37 import android.preference.PreferenceManager
38 import android.preference.PreferenceScreen
39 import android.provider.CalendarContract
40 import android.provider.CalendarContract.CalendarCache
41 import android.text.TextUtils
42 import android.text.format.Time
43 import com.android.calendar.alerts.AlertReceiver
44 import com.android.timezonepicker.TimeZoneInfo
45 import com.android.timezonepicker.TimeZonePickerDialog
46 import com.android.timezonepicker.TimeZonePickerDialog.OnTimeZoneSetListener
47 import com.android.timezonepicker.TimeZonePickerUtils
48 
49 class GeneralPreferences : PreferenceFragment(), OnSharedPreferenceChangeListener,
50         OnPreferenceChangeListener, OnTimeZoneSetListener {
51     var mAlert: CheckBoxPreference? = null
52     var mVibrate: CheckBoxPreference? = null
53     var mPopup: CheckBoxPreference? = null
54     var mUseHomeTZ: CheckBoxPreference? = null
55     var mHideDeclined: CheckBoxPreference? = null
56     var mHomeTZ: Preference? = null
57     var mTzPickerUtils: TimeZonePickerUtils? = null
58     var mWeekStart: ListPreference? = null
59     var mDefaultReminder: ListPreference? = null
60     private var mTimeZoneId: String? = null
61 
62     @Override
onCreatenull63     override fun onCreate(icicle: Bundle?) {
64         super.onCreate(icicle)
65         val activity: Activity = getActivity()
66 
67         // Make sure to always use the same preferences file regardless of the package name
68         // we're running under
69         val preferenceManager: PreferenceManager = getPreferenceManager()
70         val sharedPreferences: SharedPreferences? = getSharedPreferences(activity)
71         preferenceManager.setSharedPreferencesName(SHARED_PREFS_NAME)
72 
73         // Load the preferences from an XML resource
74         addPreferencesFromResource(R.xml.general_preferences)
75         val preferenceScreen: PreferenceScreen = getPreferenceScreen()
76         mAlert = preferenceScreen.findPreference(KEY_ALERTS) as CheckBoxPreference
77         mVibrate = preferenceScreen.findPreference(KEY_ALERTS_VIBRATE) as CheckBoxPreference
78         val vibrator: Vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
79         if (vibrator == null || !vibrator.hasVibrator()) {
80             val mAlertGroup: PreferenceCategory = preferenceScreen
81                     .findPreference(KEY_ALERTS_CATEGORY) as PreferenceCategory
82             mAlertGroup.removePreference(mVibrate)
83         }
84         mPopup = preferenceScreen.findPreference(KEY_ALERTS_POPUP) as CheckBoxPreference
85         mUseHomeTZ = preferenceScreen.findPreference(KEY_HOME_TZ_ENABLED) as CheckBoxPreference
86         mHideDeclined = preferenceScreen.findPreference(KEY_HIDE_DECLINED) as CheckBoxPreference
87         mWeekStart = preferenceScreen.findPreference(KEY_WEEK_START_DAY) as ListPreference
88         mDefaultReminder = preferenceScreen.findPreference(KEY_DEFAULT_REMINDER) as ListPreference
89         mHomeTZ = preferenceScreen.findPreference(KEY_HOME_TZ)
90         mWeekStart?.setSummary(mWeekStart?.getEntry())
91         mDefaultReminder?.setSummary(mDefaultReminder?.getEntry())
92 
93         // This triggers an asynchronous call to the provider to refresh the data in shared pref
94         mTimeZoneId = Utils.getTimeZone(activity, null)
95         val prefs: SharedPreferences = CalendarUtils.getSharedPreferences(activity,
96                 Utils.SHARED_PREFS_NAME)
97 
98         // Utils.getTimeZone will return the currentTimeZone instead of the one
99         // in the shared_pref if home time zone is disabled. So if home tz is
100         // off, we will explicitly read it.
101         if (!prefs.getBoolean(KEY_HOME_TZ_ENABLED, false)) {
102             mTimeZoneId = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone())
103         }
104 
105         mHomeTZ?.setOnPreferenceClickListener(object : Preference.OnPreferenceClickListener {
106             @Override
107             override fun onPreferenceClick(preference: Preference?): Boolean {
108                 showTimezoneDialog()
109                 return true
110             }
111         })
112 
113         if (mTzPickerUtils == null) {
114             mTzPickerUtils = TimeZonePickerUtils(getActivity())
115         }
116         val timezoneName: CharSequence? = mTzPickerUtils?.getGmtDisplayName(getActivity(),
117                 mTimeZoneId, System.currentTimeMillis(), false)
118         mHomeTZ?.setSummary(timezoneName ?: mTimeZoneId)
119         val tzpd: TimeZonePickerDialog = activity.getFragmentManager()
120                 .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER) as TimeZonePickerDialog
121         if (tzpd != null) {
122             tzpd.setOnTimeZoneSetListener(this)
123         }
124         migrateOldPreferences(sharedPreferences)
125         updateChildPreferences()
126     }
127 
showTimezoneDialognull128     private fun showTimezoneDialog() {
129         val activity: Activity = getActivity() ?: return
130         val b = Bundle()
131         b.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, System.currentTimeMillis())
132         b.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, Utils.getTimeZone(activity, null))
133         val fm: FragmentManager = getActivity().getFragmentManager()
134         var tzpd: TimeZonePickerDialog? = fm
135                 .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER) as TimeZonePickerDialog
136         if (tzpd != null) {
137             tzpd.dismiss()
138         }
139         tzpd = TimeZonePickerDialog()
140         tzpd.setArguments(b)
141         tzpd.setOnTimeZoneSetListener(this)
142         tzpd.show(fm, FRAG_TAG_TIME_ZONE_PICKER)
143     }
144 
145     @Override
onStartnull146     override fun onStart() {
147         super.onStart()
148         getPreferenceScreen().getSharedPreferences()
149                 .registerOnSharedPreferenceChangeListener(this)
150         setPreferenceListeners(this)
151     }
152 
153     /**
154      * Sets up all the preference change listeners to use the specified
155      * listener.
156      */
setPreferenceListenersnull157     private fun setPreferenceListeners(listener: OnPreferenceChangeListener?) {
158         mUseHomeTZ?.setOnPreferenceChangeListener(listener)
159         mHomeTZ?.setOnPreferenceChangeListener(listener)
160         mWeekStart?.setOnPreferenceChangeListener(listener)
161         mDefaultReminder?.setOnPreferenceChangeListener(listener)
162         mHideDeclined?.setOnPreferenceChangeListener(listener)
163         mVibrate?.setOnPreferenceChangeListener(listener)
164     }
165 
166     @Override
onStopnull167     override fun onStop() {
168         getPreferenceScreen().getSharedPreferences()
169                 .unregisterOnSharedPreferenceChangeListener(this)
170         setPreferenceListeners(null)
171         super.onStop()
172     }
173 
174     @Override
onSharedPreferenceChangednull175     override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
176         val a: Activity = getActivity()
177         if (key.equals(KEY_ALERTS)) {
178             updateChildPreferences()
179             if (a != null) {
180                 val intent = Intent()
181                 intent.setClass(a, AlertReceiver::class.java)
182                 if (mAlert?.isChecked() ?: false) {
183                     intent.setAction(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)
184                 } else {
185                     intent.setAction(AlertReceiver.EVENT_REMINDER_APP_ACTION)
186                 }
187                 a.sendBroadcast(intent)
188             }
189         }
190         if (a != null) {
191             BackupManager.dataChanged(a.getPackageName())
192         }
193     }
194 
195     /**
196      * Handles time zone preference changes
197      */
198     @Override
onPreferenceChangenull199     override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
200         val tz: String?
201         val activity: Activity = getActivity()
202         if (preference === mUseHomeTZ) {
203             tz = if (newValue != null) {
204                 mTimeZoneId
205             } else {
206                 CalendarCache.TIMEZONE_TYPE_AUTO
207             }
208             Utils.setTimeZone(activity, tz)
209             return true
210         } else if (preference === mHideDeclined) {
211             mHideDeclined?.setChecked(newValue as Boolean)
212             val intent = Intent(Utils.getWidgetScheduledUpdateAction(activity))
213             intent.setDataAndType(CalendarContract.CONTENT_URI, Utils.APPWIDGET_DATA_TYPE)
214             activity.sendBroadcast(intent)
215             return true
216         } else if (preference === mWeekStart) {
217             mWeekStart?.setValue(newValue as String)
218             mWeekStart?.setSummary(mWeekStart?.getEntry())
219         } else if (preference === mDefaultReminder) {
220             mDefaultReminder?.setValue(newValue as String)
221             mDefaultReminder?.setSummary(mDefaultReminder?.getEntry())
222         } else if (preference === mVibrate) {
223             mVibrate?.setChecked(newValue as Boolean)
224             return true
225         } else {
226             return true
227         }
228         return false
229     }
230 
getRingtoneTitleFromUrinull231     fun getRingtoneTitleFromUri(context: Context?, uri: String?): String? {
232         if (TextUtils.isEmpty(uri)) {
233             return null
234         }
235         val ring: Ringtone = RingtoneManager.getRingtone(getActivity(), Uri.parse(uri))
236         return if (ring != null) {
237             ring.getTitle(context)
238         } else null
239     }
240 
241     /**
242      * If necessary, upgrades previous versions of preferences to the current
243      * set of keys and values.
244      * @param prefs the preferences to upgrade
245      */
migrateOldPreferencesnull246     private fun migrateOldPreferences(prefs: SharedPreferences?) {
247         // If needed, migrate vibration setting from a previous version
248         mVibrate?.setChecked(Utils.getDefaultVibrate(getActivity(), prefs))
249 
250         // If needed, migrate the old alerts type settin
251         if (prefs?.contains(KEY_ALERTS) == false && prefs.contains(KEY_ALERTS_TYPE) == true) {
252             val type: String? = prefs.getString(KEY_ALERTS_TYPE, ALERT_TYPE_STATUS_BAR)
253             if (type.equals(ALERT_TYPE_OFF)) {
254                 mAlert?.setChecked(false)
255                 mPopup?.setChecked(false)
256                 mPopup?.setEnabled(false)
257             } else if (type.equals(ALERT_TYPE_STATUS_BAR)) {
258                 mAlert?.setChecked(true)
259                 mPopup?.setChecked(false)
260                 mPopup?.setEnabled(true)
261             } else if (type.equals(ALERT_TYPE_ALERTS)) {
262                 mAlert?.setChecked(true)
263                 mPopup?.setChecked(true)
264                 mPopup?.setEnabled(true)
265             }
266             // clear out the old setting
267             prefs.edit().remove(KEY_ALERTS_TYPE).commit()
268         }
269     }
270 
271     /**
272      * Keeps the dependent settings in sync with the parent preference, so for
273      * example, when notifications are turned off, we disable the preferences
274      * for configuring the exact notification behavior.
275      */
updateChildPreferencesnull276     private fun updateChildPreferences() {
277         if (mAlert?.isChecked() ?: false) {
278             mVibrate?.setEnabled(true)
279             mPopup?.setEnabled(true)
280         } else {
281             mVibrate?.setEnabled(false)
282             mPopup?.setEnabled(false)
283         }
284     }
285 
286     @Override
onPreferenceTreeClicknull287     override fun onPreferenceTreeClick(
288         preferenceScreen: PreferenceScreen?,
289         preference: Preference
290     ): Boolean {
291         val key: String = preference.getKey()
292         return super.onPreferenceTreeClick(preferenceScreen, preference)
293     }
294 
295     @Override
onTimeZoneSetnull296     override fun onTimeZoneSet(tzi: TimeZoneInfo) {
297         if (mTzPickerUtils == null) {
298             mTzPickerUtils = TimeZonePickerUtils(getActivity())
299         }
300         val timezoneName: CharSequence? = mTzPickerUtils?.getGmtDisplayName(
301                 getActivity(), tzi.mTzId, System.currentTimeMillis(), false)
302         mHomeTZ?.setSummary(timezoneName)
303         Utils.setTimeZone(getActivity(), tzi.mTzId)
304     }
305 
306     companion object {
307         // The name of the shared preferences file. This name must be maintained for historical
308         // reasons, as it's what PreferenceManager assigned the first time the file was created.
309         const val SHARED_PREFS_NAME = "com.android.calendar_preferences"
310         const val SHARED_PREFS_NAME_NO_BACKUP = "com.android.calendar_preferences_no_backup"
311         private const val FRAG_TAG_TIME_ZONE_PICKER = "TimeZonePicker"
312 
313         // Preference keys
314         const val KEY_HIDE_DECLINED = "preferences_hide_declined"
315         const val KEY_WEEK_START_DAY = "preferences_week_start_day"
316         const val KEY_SHOW_WEEK_NUM = "preferences_show_week_num"
317         const val KEY_DAYS_PER_WEEK = "preferences_days_per_week"
318         const val KEY_SKIP_SETUP = "preferences_skip_setup"
319         const val KEY_CLEAR_SEARCH_HISTORY = "preferences_clear_search_history"
320         const val KEY_ALERTS_CATEGORY = "preferences_alerts_category"
321         const val KEY_ALERTS = "preferences_alerts"
322         const val KEY_ALERTS_VIBRATE = "preferences_alerts_vibrate"
323         const val KEY_ALERTS_RINGTONE = "preferences_alerts_ringtone"
324         const val KEY_ALERTS_POPUP = "preferences_alerts_popup"
325         const val KEY_SHOW_CONTROLS = "preferences_show_controls"
326         const val KEY_DEFAULT_REMINDER = "preferences_default_reminder"
327         const val NO_REMINDER = -1
328         const val NO_REMINDER_STRING = "-1"
329         const val REMINDER_DEFAULT_TIME = 10 // in minutes
330         const val KEY_DEFAULT_CELL_HEIGHT = "preferences_default_cell_height"
331         const val KEY_VERSION = "preferences_version"
332 
333         /** Key to SharePreference for default view (CalendarController.ViewType)  */
334         const val KEY_START_VIEW = "preferred_startView"
335 
336         /**
337          * Key to SharePreference for default detail view (CalendarController.ViewType)
338          * Typically used by widget
339          */
340         const val KEY_DETAILED_VIEW = "preferred_detailedView"
341         const val KEY_DEFAULT_CALENDAR = "preference_defaultCalendar"
342 
343         // These must be in sync with the array preferences_week_start_day_values
344         const val WEEK_START_DEFAULT = "-1"
345         const val WEEK_START_SATURDAY = "7"
346         const val WEEK_START_SUNDAY = "1"
347         const val WEEK_START_MONDAY = "2"
348 
349         // These keys are kept to enable migrating users from previous versions
350         private const val KEY_ALERTS_TYPE = "preferences_alerts_type"
351         private const val ALERT_TYPE_ALERTS = "0"
352         private const val ALERT_TYPE_STATUS_BAR = "1"
353         private const val ALERT_TYPE_OFF = "2"
354         const val KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled"
355         const val KEY_HOME_TZ = "preferences_home_tz"
356 
357         // Default preference values
358         const val DEFAULT_START_VIEW: Int = CalendarController.ViewType.WEEK
359         const val DEFAULT_DETAILED_VIEW: Int = CalendarController.ViewType.DAY
360         const val DEFAULT_SHOW_WEEK_NUM = false
361 
362         // This should match the XML file.
363         const val DEFAULT_RINGTONE = "content://settings/system/notification_sound"
364 
365         /** Return a properly configured SharedPreferences instance  */
366         @JvmStatic
getSharedPreferencesnull367         fun getSharedPreferences(context: Context?): SharedPreferences? {
368             return context?.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
369         }
370 
371         /** Set the default shared preferences in the proper context  */
372         @JvmStatic
setDefaultValuesnull373         fun setDefaultValues(context: Context?) {
374             PreferenceManager.setDefaultValues(context, SHARED_PREFS_NAME, Context.MODE_PRIVATE,
375                     R.xml.general_preferences, false)
376         }
377     }
378 }