Utils.java revision 63974da72657fd9b9631a75dcc46eb9298c81013
1/* 2 * Copyright (C) 2006 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 17package com.android.calendar; 18 19import static android.provider.Calendar.EVENT_BEGIN_TIME; 20 21import android.content.AsyncQueryHandler; 22import android.content.ContentResolver; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.Intent; 26import android.content.SharedPreferences; 27import android.database.Cursor; 28import android.database.MatrixCursor; 29import android.graphics.drawable.Drawable; 30import android.graphics.drawable.GradientDrawable; 31import android.net.Uri; 32import android.text.TextUtils; 33import android.text.format.DateUtils; 34import android.text.format.Time; 35import android.util.Log; 36import android.view.animation.AlphaAnimation; 37import android.widget.ViewFlipper; 38 39import java.util.Calendar; 40import java.util.Formatter; 41import java.util.HashSet; 42import java.util.List; 43import java.util.Locale; 44import java.util.Map; 45 46public class Utils { 47 private static final boolean DEBUG = true; 48 private static final String TAG = "CalUtils"; 49 private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF; 50 private static final int HIGH_ALPHA = 255 << 24; 51 private static final int MED_ALPHA = 180 << 24; 52 private static final int LOW_ALPHA = 150 << 24; 53 54 protected static final String OPEN_EMAIL_MARKER = " <"; 55 protected static final String CLOSE_EMAIL_MARKER = ">"; 56 /* The corner should be rounded on the top right and bottom right */ 57 private static final float[] CORNERS = new float[] {0, 0, 5, 5, 5, 5, 0, 0}; 58 59 // TODO switch these to use Calendar.java when it gets added 60 private static final String TIMEZONE_COLUMN_KEY = "key"; 61 private static final String TIMEZONE_COLUMN_VALUE = "value"; 62 private static final String TIMEZONE_KEY_TYPE = "timezoneType"; 63 private static final String TIMEZONE_KEY_INSTANCES = "timezoneInstances"; 64 private static final String TIMEZONE_KEY_INSTANCES_PREVIOUS = "timezoneInstancesPrevious"; 65 private static final String TIMEZONE_TYPE_AUTO = "auto"; 66 private static final String TIMEZONE_TYPE_HOME = "home"; 67 private static final String[] TIMEZONE_POJECTION = new String[] { 68 TIMEZONE_COLUMN_KEY, 69 TIMEZONE_COLUMN_VALUE, 70 }; 71 // Uri.parse("content://" + AUTHORITY + "/reminders"); 72 private static final Uri TIMEZONE_URI = 73 Uri.parse("content://" + android.provider.Calendar.AUTHORITY + "/properties"); 74 private static final String TIMEZONE_UPDATE_WHERE = "key=?"; 75 76 77 private static StringBuilder mSB = new StringBuilder(50); 78 private static Formatter mF = new Formatter(mSB, Locale.getDefault()); 79 private volatile static boolean mFirstTZRequest = true; 80 private volatile static boolean mTZQueryInProgress = false; 81 82 private volatile static boolean mUseHomeTZ = false; 83 private volatile static String mHomeTZ = Time.getCurrentTimezone(); 84 85 private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>(); 86 private static int mToken = 1; 87 private static AsyncTZHandler mHandler; 88 89 private static class AsyncTZHandler extends AsyncQueryHandler { 90 public AsyncTZHandler(ContentResolver cr) { 91 super(cr); 92 } 93 94 @Override 95 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 96 synchronized (mTZCallbacks) { 97 boolean writePrefs = false; 98 // Check the values in the db 99 while(cursor.moveToNext()) { 100 int keyColumn = cursor.getColumnIndexOrThrow(TIMEZONE_COLUMN_KEY); 101 int valueColumn = cursor.getColumnIndexOrThrow(TIMEZONE_COLUMN_VALUE); 102 String key = cursor.getString(keyColumn); 103 String value = cursor.getString(valueColumn); 104 if (TextUtils.equals(key, TIMEZONE_KEY_TYPE)) { 105 boolean useHomeTZ = !TextUtils.equals(value, TIMEZONE_TYPE_AUTO); 106 if (useHomeTZ != mUseHomeTZ) { 107 writePrefs = true; 108 mUseHomeTZ = useHomeTZ; 109 } 110 } else if (TextUtils.equals(key, TIMEZONE_KEY_INSTANCES_PREVIOUS)) { 111 if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) { 112 writePrefs = true; 113 mHomeTZ = value; 114 } 115 } 116 } 117 if (writePrefs) { 118 // Write the prefs 119 setSharedPreference((Context)cookie, 120 CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, mUseHomeTZ); 121 setSharedPreference((Context)cookie, 122 CalendarPreferenceActivity.KEY_HOME_TZ, mHomeTZ); 123 } 124 125 mTZQueryInProgress = false; 126 for (Runnable callback : mTZCallbacks) { 127 if (callback != null) { 128 callback.run(); 129 } 130 } 131 mTZCallbacks.clear(); 132 } 133 } 134 } 135 136 public static void startActivity(Context context, String className, long time) { 137 Intent intent = new Intent(Intent.ACTION_VIEW); 138 139 intent.setClassName(context, className); 140 intent.putExtra(EVENT_BEGIN_TIME, time); 141 intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); 142 143 context.startActivity(intent); 144 } 145 146 static String getSharedPreference(Context context, String key, String defaultValue) { 147 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context); 148 return prefs.getString(key, defaultValue); 149 } 150 151 /** 152 * Writes a new home time zone to the db. 153 * 154 * Updates the home time zone in the db asynchronously and updates 155 * the local cache. Sending a time zone of **tbd** will cause it to 156 * be set to the device's time zone. null or empty tz will be ignored. 157 * 158 * @param context The calling activity 159 * @param timeZone The time zone to set Calendar to, or **tbd** 160 */ 161 public static void setTimeZone(Context context, String timeZone) { 162 if (TextUtils.isEmpty(timeZone)) { 163 if (DEBUG) { 164 Log.d(TAG, "Empty time zone, nothing to be done."); 165 } 166 return; 167 } 168 boolean updatePrefs = false; 169 synchronized (mTZCallbacks) { 170 if (CalendarPreferenceActivity.LOCAL_TZ.equals(timeZone)) { 171 if (mUseHomeTZ) { 172 updatePrefs = true; 173 } 174 mUseHomeTZ = false; 175 } else { 176 if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) { 177 updatePrefs = true; 178 } 179 mUseHomeTZ = true; 180 mHomeTZ = timeZone; 181 } 182 } 183 if (updatePrefs) { 184 // Write the prefs 185 setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, 186 mUseHomeTZ); 187 setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ, mHomeTZ); 188 189 // Update the db 190 ContentValues values = new ContentValues(); 191 if (mHandler == null) { 192 mHandler = new AsyncTZHandler(context.getContentResolver()); 193 } 194 195 mHandler.cancelOperation(mToken); 196 197 String[] selArgs = new String[] {TIMEZONE_KEY_TYPE}; 198 values.put(TIMEZONE_COLUMN_VALUE, mUseHomeTZ ? TIMEZONE_TYPE_HOME : TIMEZONE_TYPE_AUTO); 199 mHandler.startUpdate(mToken, null, TIMEZONE_URI, values, TIMEZONE_UPDATE_WHERE, 200 selArgs); 201 202 if (mUseHomeTZ) { 203 selArgs[0] = TIMEZONE_KEY_INSTANCES; 204 values.clear(); 205 values.put(TIMEZONE_COLUMN_VALUE, mHomeTZ); 206 mHandler.startUpdate(mToken, null, TIMEZONE_URI, values, TIMEZONE_UPDATE_WHERE, 207 selArgs); 208 } 209 210 // skip 0 so query can use it 211 if (++mToken == 0) { 212 mToken = 1; 213 } 214 } 215 } 216 217 /** 218 * Gets the time zone that Calendar should be displayed in 219 * 220 * This is a helper method to get the appropriate time zone for Calendar. If this 221 * is the first time this method has been called it will initiate an asynchronous 222 * query to verify that the data in preferences is correct. The callback supplied 223 * will only be called if this query returns a value other than what is stored in 224 * preferences and should cause the calling activity to refresh anything that 225 * depends on calling this method. 226 * 227 * @param context The calling activity 228 * @param callback The runnable that should execute if a query returns new values 229 * @return The string value representing the time zone Calendar should display 230 */ 231 public static String getTimeZone(Context context, Runnable callback) { 232 synchronized (mTZCallbacks){ 233 if (mFirstTZRequest) { 234 mTZQueryInProgress = true; 235 mFirstTZRequest = false; 236 237 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context); 238 mUseHomeTZ = prefs.getBoolean( 239 CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, false); 240 mHomeTZ = prefs.getString( 241 CalendarPreferenceActivity.KEY_HOME_TZ, Time.getCurrentTimezone()); 242 243 // When the async query returns it should synchronize on 244 // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the 245 // preferences, set mTZQueryInProgress to false, and call all 246 // the runnables in mTZCallbacks. 247 if (mHandler == null) { 248 mHandler = new AsyncTZHandler(context.getContentResolver()); 249 } 250 mHandler.startQuery(0, context, TIMEZONE_URI, TIMEZONE_POJECTION, null, null, null); 251 } 252 if (mTZQueryInProgress) { 253 mTZCallbacks.add(callback); 254 } 255 } 256 return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone(); 257 } 258 259 /** 260 * Formats a date or a time range according to the local conventions. 261 * 262 * @param context the context is required only if the time is shown 263 * @param startMillis the start time in UTC milliseconds 264 * @param endMillis the end time in UTC milliseconds 265 * @param flags a bit mask of options See 266 * {@link #formatDateRange(Context, Formatter, long, long, int, String) formatDateRange} 267 * @return a string containing the formatted date/time range. 268 */ 269 public static String formatDateRange(Context context, long startMillis, 270 long endMillis, int flags) { 271 String date; 272 synchronized (mSB) { 273 mSB.setLength(0); 274 date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags, 275 getTimeZone(context, null)).toString(); 276 } 277 return date; 278 } 279 280 static void setSharedPreference(Context context, String key, String value) { 281 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context); 282 SharedPreferences.Editor editor = prefs.edit(); 283 editor.putString(key, value); 284 editor.commit(); 285 } 286 287 static void setSharedPreference(Context context, String key, boolean value) { 288 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context); 289 SharedPreferences.Editor editor = prefs.edit(); 290 editor.putBoolean(key, value); 291 editor.commit(); 292 } 293 294 static void setDefaultView(Context context, int viewId) { 295 String activityString = CalendarApplication.ACTIVITY_NAMES[viewId]; 296 297 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context); 298 SharedPreferences.Editor editor = prefs.edit(); 299 if (viewId == CalendarApplication.AGENDA_VIEW_ID || 300 viewId == CalendarApplication.DAY_VIEW_ID) { 301 // Record the (new) detail start view only for Agenda and Day 302 editor.putString(CalendarPreferenceActivity.KEY_DETAILED_VIEW, activityString); 303 } 304 305 // Record the (new) start view 306 editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString); 307 editor.commit(); 308 } 309 310 public static final Time timeFromIntent(Intent intent) { 311 Time time = new Time(); 312 time.set(timeFromIntentInMillis(intent)); 313 return time; 314 } 315 316 public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { 317 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames()); 318 int numColumns = cursor.getColumnCount(); 319 String data[] = new String[numColumns]; 320 cursor.moveToPosition(-1); 321 while (cursor.moveToNext()) { 322 for (int i = 0; i < numColumns; i++) { 323 data[i] = cursor.getString(i); 324 } 325 newCursor.addRow(data); 326 } 327 return newCursor; 328 } 329 330 /** 331 * Compares two cursors to see if they contain the same data. 332 * 333 * @return Returns true of the cursors contain the same data and are not null, false 334 * otherwise 335 */ 336 public static boolean compareCursors(Cursor c1, Cursor c2) { 337 if(c1 == null || c2 == null) { 338 return false; 339 } 340 341 int numColumns = c1.getColumnCount(); 342 if (numColumns != c2.getColumnCount()) { 343 return false; 344 } 345 346 if (c1.getCount() != c2.getCount()) { 347 return false; 348 } 349 350 c1.moveToPosition(-1); 351 c2.moveToPosition(-1); 352 while(c1.moveToNext() && c2.moveToNext()) { 353 for(int i = 0; i < numColumns; i++) { 354 if(!TextUtils.equals(c1.getString(i), c2.getString(i))) { 355 return false; 356 } 357 } 358 } 359 360 return true; 361 } 362 363 /** 364 * If the given intent specifies a time (in milliseconds since the epoch), 365 * then that time is returned. Otherwise, the current time is returned. 366 */ 367 public static final long timeFromIntentInMillis(Intent intent) { 368 // If the time was specified, then use that. Otherwise, use the current time. 369 Uri data = intent.getData(); 370 long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1); 371 if (millis == -1 && data != null && data.isHierarchical()) { 372 List<String> path = data.getPathSegments(); 373 if(path.size() == 2 && path.get(0).equals("time")) { 374 try { 375 millis = Long.valueOf(data.getLastPathSegment()); 376 } catch (NumberFormatException e) { 377 Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " + 378 "found. Using current time."); 379 } 380 } 381 } 382 if (millis <= 0) { 383 millis = System.currentTimeMillis(); 384 } 385 return millis; 386 } 387 388 public static final void applyAlphaAnimation(ViewFlipper v) { 389 AlphaAnimation in = new AlphaAnimation(0.0f, 1.0f); 390 391 in.setStartOffset(0); 392 in.setDuration(500); 393 394 AlphaAnimation out = new AlphaAnimation(1.0f, 0.0f); 395 396 out.setStartOffset(0); 397 out.setDuration(500); 398 399 v.setInAnimation(in); 400 v.setOutAnimation(out); 401 } 402 403 public static Drawable getColorChip(int color) { 404 /* 405 * We want the color chip to have a nice gradient using 406 * the color of the calendar. To do this we use a GradientDrawable. 407 * The color supplied has an alpha of FF so we first do: 408 * color & 0x00FFFFFF 409 * to clear the alpha. Then we add our alpha to it. 410 * We use 3 colors to get a step effect where it starts off very 411 * light and quickly becomes dark and then a slow transition to 412 * be even darker. 413 */ 414 color &= CLEAR_ALPHA_MASK; 415 int startColor = color | HIGH_ALPHA; 416 int middleColor = color | MED_ALPHA; 417 int endColor = color | LOW_ALPHA; 418 int[] colors = new int[] {startColor, middleColor, endColor}; 419 GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors); 420 d.setCornerRadii(CORNERS); 421 return d; 422 } 423 424 /** 425 * Formats the given Time object so that it gives the month and year 426 * (for example, "September 2007"). 427 * 428 * @param time the time to format 429 * @return the string containing the weekday and the date 430 */ 431 public static String formatMonthYear(Context context, Time time) { 432 return time.format(context.getResources().getString(R.string.month_year)); 433 } 434 435 // TODO: replace this with the correct i18n way to do this 436 public static final String englishNthDay[] = { 437 "", "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", 438 "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", 439 "20th", "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th", 440 "30th", "31st" 441 }; 442 443 public static String formatNth(int nth) { 444 return "the " + englishNthDay[nth]; 445 } 446 447 /** 448 * Returns a list joined together by the provided delimiter, for example, 449 * ["a", "b", "c"] could be joined into "a,b,c" 450 * 451 * @param things the things to join together 452 * @param delim the delimiter to use 453 * @return a string contained the things joined together 454 */ 455 public static String join(List<?> things, String delim) { 456 StringBuilder builder = new StringBuilder(); 457 boolean first = true; 458 for (Object thing : things) { 459 if (first) { 460 first = false; 461 } else { 462 builder.append(delim); 463 } 464 builder.append(thing.toString()); 465 } 466 return builder.toString(); 467 } 468 469 /** 470 * Sets the time to the beginning of the day (midnight) by clearing the 471 * hour, minute, and second fields. 472 */ 473 static void setTimeToStartOfDay(Time time) { 474 time.second = 0; 475 time.minute = 0; 476 time.hour = 0; 477 } 478 479 /** 480 * Get first day of week as android.text.format.Time constant. 481 * @return the first day of week in android.text.format.Time 482 */ 483 public static int getFirstDayOfWeek() { 484 int startDay = Calendar.getInstance().getFirstDayOfWeek(); 485 if (startDay == Calendar.SATURDAY) { 486 return Time.SATURDAY; 487 } else if (startDay == Calendar.MONDAY) { 488 return Time.MONDAY; 489 } else { 490 return Time.SUNDAY; 491 } 492 } 493 494 /** 495 * Determine whether the column position is Saturday or not. 496 * @param column the column position 497 * @param firstDayOfWeek the first day of week in android.text.format.Time 498 * @return true if the column is Saturday position 499 */ 500 public static boolean isSaturday(int column, int firstDayOfWeek) { 501 return (firstDayOfWeek == Time.SUNDAY && column == 6) 502 || (firstDayOfWeek == Time.MONDAY && column == 5) 503 || (firstDayOfWeek == Time.SATURDAY && column == 0); 504 } 505 506 /** 507 * Determine whether the column position is Sunday or not. 508 * @param column the column position 509 * @param firstDayOfWeek the first day of week in android.text.format.Time 510 * @return true if the column is Sunday position 511 */ 512 public static boolean isSunday(int column, int firstDayOfWeek) { 513 return (firstDayOfWeek == Time.SUNDAY && column == 0) 514 || (firstDayOfWeek == Time.MONDAY && column == 6) 515 || (firstDayOfWeek == Time.SATURDAY && column == 1); 516 } 517 518 /** 519 * Scan through a cursor of calendars and check if names are duplicated. 520 * 521 * This travels a cursor containing calendar display names and fills in the provided map with 522 * whether or not each name is repeated. 523 * @param isDuplicateName The map to put the duplicate check results in. 524 * @param cursor The query of calendars to check 525 * @param nameIndex The column of the query that contains the display name 526 */ 527 public static void checkForDuplicateNames(Map<String, Boolean> isDuplicateName, Cursor cursor, 528 int nameIndex) { 529 isDuplicateName.clear(); 530 cursor.moveToPosition(-1); 531 while (cursor.moveToNext()) { 532 String displayName = cursor.getString(nameIndex); 533 // Set it to true if we've seen this name before, false otherwise 534 if (displayName != null) { 535 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName)); 536 } 537 } 538 } 539} 540