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