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.content.AsyncQueryHandler
19 import android.content.ContentResolver
20 import android.content.ContentValues
21 import android.content.Context
22 import android.content.SharedPreferences
23 import android.database.Cursor
24 import android.os.Looper
25 import android.provider.CalendarContract.CalendarCache
26 import android.text.TextUtils
27 import android.text.format.DateUtils
28 import android.text.format.Time
29 import android.util.Log
30 
31 import java.util.Formatter
32 import java.util.HashSet
33 import java.util.Locale
34 
35 /**
36  * A class containing utility methods related to Calendar apps.
37  *
38  * This class is expected to move into the app framework eventually.
39  */
40 class CalendarUtils {
41 
42     companion object {
43         private const val DEBUG = false
44         private const val TAG = "CalendarUtils"
45 
46         /**
47          * A helper method for writing a boolean value to the preferences
48          * asynchronously.
49          *
50          * @param context A context with access to the correct preferences
51          * @param key The preference to write to
52          * @param value The value to write
53          */
54         @JvmStatic
setSharedPreferencenull55         fun setSharedPreference(prefs: SharedPreferences, key: String?, value: Boolean) {
56             val editor: SharedPreferences.Editor = prefs.edit()
57             editor.putBoolean(key, value)
58             editor.apply()
59         }
60 
61         /** Return a properly configured SharedPreferences instance  */
62         @JvmStatic
getSharedPreferencesnull63         fun getSharedPreferences(context: Context, prefsName: String?): SharedPreferences {
64             return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
65         }
66 
67         /**
68          * A helper method for writing a String value to the preferences
69          * asynchronously.
70          *
71          * @param context A context with access to the correct preferences
72          * @param key The preference to write to
73          * @param value The value to write
74          */
75         @JvmStatic
setSharedPreferencenull76         fun setSharedPreference(prefs: SharedPreferences, key: String?, value: String?) {
77             val editor: SharedPreferences.Editor = prefs.edit()
78             editor.putString(key, value)
79             editor.apply()
80         }
81     }
82 
83     /**
84      * This class contains methods specific to reading and writing time zone
85      * values.
86      */
87     class TimeZoneUtils
88     /**
89      * The name of the file where the shared prefs for Calendar are stored
90      * must be provided. All activities within an app should provide the
91      * same preferences name or behavior may become erratic.
92      *
93      * @param prefsName
94      */(  // The name of the shared preferences file. This name must be maintained for historical
95             // reasons, as it's what PreferenceManager assigned the first time the file was created.
96             private val mPrefsName: String) {
97         /**
98          * This is a helper class for handling the async queries and updates for the
99          * time zone settings in Calendar.
100          */
101         private inner class AsyncTZHandler(cr: ContentResolver?) : AsyncQueryHandler(cr) {
onQueryCompletenull102             protected override fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) {
103                 synchronized(mTZCallbacks) {
104                     if (cursor == null) {
105                         mTZQueryInProgress = false
106                         mFirstTZRequest = true
107                         return
108                     }
109                     var writePrefs = false
110                     // Check the values in the db
111                     val keyColumn: Int = cursor.getColumnIndexOrThrow(CalendarCache.KEY)
112                     val valueColumn: Int = cursor.getColumnIndexOrThrow(CalendarCache.VALUE)
113                     while (cursor.moveToNext()) {
114                         val key: String = cursor.getString(keyColumn)
115                         val value: String = cursor.getString(valueColumn)
116                         if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) {
117                             val useHomeTZ: Boolean = !TextUtils.equals(
118                                     value, CalendarCache.TIMEZONE_TYPE_AUTO)
119                             if (useHomeTZ != mUseHomeTZ) {
120                                 writePrefs = true
121                                 mUseHomeTZ = useHomeTZ
122                             }
123                         } else if (TextUtils.equals(
124                                         key, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) {
125                             if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) {
126                                 writePrefs = true
127                                 mHomeTZ = value
128                             }
129                         }
130                     }
131                     cursor.close()
132                     if (writePrefs) {
133                         val prefs: SharedPreferences =
134                         getSharedPreferences(cookie as Context, mPrefsName)
135                         // Write the prefs
136                         setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ)
137                         setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ)
138                     }
139                     mTZQueryInProgress = false
140                     for (callback in mTZCallbacks) {
141                         if (callback != null) {
142                             callback.run()
143                         }
144                     }
145                     mTZCallbacks.clear()
146                 }
147             }
148         }
149 
150         /**
151          * Formats a date or a time range according to the local conventions.
152          *
153          * This formats a date/time range using Calendar's time zone and the
154          * local conventions for the region of the device.
155          *
156          * If the [DateUtils.FORMAT_UTC] flag is used it will pass in
157          * the UTC time zone instead.
158          *
159          * @param context the context is required only if the time is shown
160          * @param startMillis the start time in UTC milliseconds
161          * @param endMillis the end time in UTC milliseconds
162          * @param flags a bit mask of options See
163          * [formatDateRange][DateUtils.formatDateRange]
164          * @return a string containing the formatted date/time range.
165          */
formatDateRangenull166         fun formatDateRange(context: Context, startMillis: Long,
167                             endMillis: Long, flags: Int): String {
168             var date: String
169             val tz: String
170             tz = if (flags and DateUtils.FORMAT_UTC !== 0) {
171                 Time.TIMEZONE_UTC
172             } else {
173                 getTimeZone(context, null)
174             }
175             synchronized(mSB) {
176                 mSB.setLength(0)
177                 date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags,
178                         tz).toString()
179             }
180             return date
181         }
182 
183         /**
184          * Writes a new home time zone to the db.
185          *
186          * Updates the home time zone in the db asynchronously and updates
187          * the local cache. Sending a time zone of
188          * [CalendarCache.TIMEZONE_TYPE_AUTO] will cause it to be set
189          * to the device's time zone. null or empty tz will be ignored.
190          *
191          * @param context The calling activity
192          * @param timeZone The time zone to set Calendar to, or
193          * [CalendarCache.TIMEZONE_TYPE_AUTO]
194          */
setTimeZonenull195         fun setTimeZone(context: Context, timeZone: String) {
196             if (TextUtils.isEmpty(timeZone)) {
197                 if (DEBUG) {
198                     Log.d(TAG, "Empty time zone, nothing to be done.")
199                 }
200                 return
201             }
202             var updatePrefs = false
203             synchronized(mTZCallbacks) {
204                 if (CalendarCache.TIMEZONE_TYPE_AUTO.equals(timeZone)) {
205                     if (mUseHomeTZ) {
206                         updatePrefs = true
207                     }
208                     mUseHomeTZ = false
209                 } else {
210                     if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) {
211                         updatePrefs = true
212                     }
213                     mUseHomeTZ = true
214                     mHomeTZ = timeZone
215                 }
216             }
217             if (updatePrefs) {
218                 // Write the prefs
219                 val prefs: SharedPreferences = getSharedPreferences(context, mPrefsName)
220                 setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ)
221                 setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ)
222 
223                 // Update the db
224                 val values = ContentValues()
225                 if (mHandler != null) {
226                     mHandler?.cancelOperation(mToken)
227                 }
228                 mHandler = AsyncTZHandler(context.getContentResolver())
229 
230                 // skip 0 so query can use it
231                 if (++mToken == 0) {
232                     mToken = 1
233                 }
234 
235                 // Write the use home tz setting
236                 values.put(CalendarCache.VALUE, if (mUseHomeTZ) CalendarCache.TIMEZONE_TYPE_HOME
237                            else CalendarCache.TIMEZONE_TYPE_AUTO)
238                 mHandler?.startUpdate(mToken, null, CalendarCache.URI, values, "key=?",
239                         TIMEZONE_TYPE_ARGS)
240 
241                 // If using a home tz write it to the db
242                 if (mUseHomeTZ) {
243                     val values2 = ContentValues()
244                     values2.put(CalendarCache.VALUE, mHomeTZ)
245                     mHandler?.startUpdate(mToken, null, CalendarCache.URI, values2,
246                             "key=?", TIMEZONE_INSTANCES_ARGS)
247                 }
248             }
249         }
250 
251         /**
252          * Gets the time zone that Calendar should be displayed in
253          *
254          * This is a helper method to get the appropriate time zone for Calendar. If this
255          * is the first time this method has been called it will initiate an asynchronous
256          * query to verify that the data in preferences is correct. The callback supplied
257          * will only be called if this query returns a value other than what is stored in
258          * preferences and should cause the calling activity to refresh anything that
259          * depends on calling this method.
260          *
261          * @param context The calling activity
262          * @param callback The runnable that should execute if a query returns new values
263          * @return The string value representing the time zone Calendar should display
264          */
getTimeZonenull265         fun getTimeZone(context: Context, callback: Runnable?): String {
266             synchronized(mTZCallbacks) {
267                 if (mFirstTZRequest) {
268                     val prefs: SharedPreferences = getSharedPreferences(context, mPrefsName)
269                     mUseHomeTZ = prefs.getBoolean(KEY_HOME_TZ_ENABLED, false)
270                     mHomeTZ = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()) ?: String()
271 
272                     // Only check content resolver if we have a looper to attach to use
273                     if (Looper.myLooper() != null) {
274                         mTZQueryInProgress = true
275                         mFirstTZRequest = false
276 
277                         // When the async query returns it should synchronize on
278                         // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the
279                         // preferences, set mTZQueryInProgress to false, and call all
280                         // the runnables in mTZCallbacks.
281                         if (mHandler == null) {
282                             mHandler = AsyncTZHandler(context.getContentResolver())
283                         }
284                         mHandler?.startQuery(0, context, CalendarCache.URI,
285                                              CALENDAR_CACHE_POJECTION, null, null, null)
286                     }
287                 }
288                 if (mTZQueryInProgress && callback != null) {
289                     mTZCallbacks.add(callback)
290                 }
291             }
292             return if (mUseHomeTZ) mHomeTZ else Time.getCurrentTimezone()
293         }
294 
295         /**
296          * Forces a query of the database to check for changes to the time zone.
297          * This should be called if another app may have modified the db. If a
298          * query is already in progress the callback will be added to the list
299          * of callbacks to be called when it returns.
300          *
301          * @param context The calling activity
302          * @param callback The runnable that should execute if a query returns
303          * new values
304          */
forceDBRequerynull305         fun forceDBRequery(context: Context, callback: Runnable) {
306             synchronized(mTZCallbacks) {
307                 if (mTZQueryInProgress) {
308                     mTZCallbacks.add(callback)
309                     return
310                 }
311                 mFirstTZRequest = true
312                 getTimeZone(context, callback)
313             }
314         }
315 
316         companion object {
317             private val TIMEZONE_TYPE_ARGS = arrayOf<String>(CalendarCache.KEY_TIMEZONE_TYPE)
318             private val TIMEZONE_INSTANCES_ARGS =
319             arrayOf<String>(CalendarCache.KEY_TIMEZONE_INSTANCES)
320             val CALENDAR_CACHE_POJECTION = arrayOf<String>(
321                     CalendarCache.KEY, CalendarCache.VALUE
322             )
323             private val mSB: StringBuilder = StringBuilder(50)
324             private val mF: Formatter = Formatter(mSB, Locale.getDefault())
325 
326             @Volatile
327             private var mFirstTZRequest = true
328 
329             @Volatile
330             private var mTZQueryInProgress = false
331 
332             @Volatile
333             private var mUseHomeTZ = false
334 
335             @Volatile
336             private var mHomeTZ: String = Time.getCurrentTimezone()
337             private val mTZCallbacks: HashSet<Runnable> = HashSet<Runnable>()
338             private var mToken = 1
339             private var mHandler: AsyncTZHandler? = null
340 
341             /**
342              * This is the key used for writing whether or not a home time zone should
343              * be used in the Calendar app to the Calendar Preferences.
344              */
345             const val KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled"
346 
347             /**
348              * This is the key used for writing the time zone that should be used if
349              * home time zones are enabled for the Calendar app.
350              */
351             const val KEY_HOME_TZ = "preferences_home_tz"
352         }
353     }
354 }