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 }