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