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