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 17 package com.android.calendar; 18 19 import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; 20 21 import android.app.Activity; 22 import android.app.SearchManager; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.SharedPreferences; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.graphics.Color; 33 import android.graphics.drawable.Drawable; 34 import android.graphics.drawable.LayerDrawable; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.text.TextUtils; 40 import android.text.format.DateFormat; 41 import android.text.format.DateUtils; 42 import android.text.format.Time; 43 import android.util.Log; 44 import android.widget.SearchView; 45 46 import com.android.calendar.CalendarController.ViewType; 47 import com.android.calendar.CalendarUtils.TimeZoneUtils; 48 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.Calendar; 52 import java.util.Formatter; 53 import java.util.HashMap; 54 import java.util.Iterator; 55 import java.util.LinkedHashSet; 56 import java.util.LinkedList; 57 import java.util.List; 58 import java.util.Locale; 59 import java.util.Map; 60 import java.util.Set; 61 import java.util.TimeZone; 62 63 public class Utils { 64 private static final boolean DEBUG = false; 65 private static final String TAG = "CalUtils"; 66 67 // Set to 0 until we have UI to perform undo 68 public static final long UNDO_DELAY = 0; 69 70 // For recurring events which instances of the series are being modified 71 public static final int MODIFY_UNINITIALIZED = 0; 72 public static final int MODIFY_SELECTED = 1; 73 public static final int MODIFY_ALL_FOLLOWING = 2; 74 public static final int MODIFY_ALL = 3; 75 76 // When the edit event view finishes it passes back the appropriate exit 77 // code. 78 public static final int DONE_REVERT = 1 << 0; 79 public static final int DONE_SAVE = 1 << 1; 80 public static final int DONE_DELETE = 1 << 2; 81 // And should re run with DONE_EXIT if it should also leave the view, just 82 // exiting is identical to reverting 83 public static final int DONE_EXIT = 1 << 0; 84 85 public static final String OPEN_EMAIL_MARKER = " <"; 86 public static final String CLOSE_EMAIL_MARKER = ">"; 87 88 public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW"; 89 public static final String INTENT_KEY_VIEW_TYPE = "VIEW"; 90 public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY"; 91 public static final String INTENT_KEY_HOME = "KEY_HOME"; 92 93 public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; 94 public static final int DECLINED_EVENT_ALPHA = 0x66; 95 public static final int DECLINED_EVENT_TEXT_ALPHA = 0xC0; 96 97 private static final float SATURATION_ADJUST = 1.3f; 98 private static final float INTENSITY_ADJUST = 0.8f; 99 100 // Defines used by the DNA generation code 101 static final int DAY_IN_MINUTES = 60 * 24; 102 static final int WEEK_IN_MINUTES = DAY_IN_MINUTES * 7; 103 // The work day is being counted as 6am to 8pm 104 static int WORK_DAY_MINUTES = 14 * 60; 105 static int WORK_DAY_START_MINUTES = 6 * 60; 106 static int WORK_DAY_END_MINUTES = 20 * 60; 107 static int WORK_DAY_END_LENGTH = (24 * 60) - WORK_DAY_END_MINUTES; 108 static int CONFLICT_COLOR = 0xFF000000; 109 static boolean mMinutesLoaded = false; 110 111 // The name of the shared preferences file. This name must be maintained for 112 // historical 113 // reasons, as it's what PreferenceManager assigned the first time the file 114 // was created. 115 static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; 116 117 public static final String KEY_QUICK_RESPONSES = "preferences_quick_responses"; 118 119 public static final String KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen"; 120 121 public static final String APPWIDGET_DATA_TYPE = "vnd.android.data/update"; 122 123 static final String MACHINE_GENERATED_ADDRESS = "calendar.google.com"; 124 125 private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME); 126 private static boolean mAllowWeekForDetailView = false; 127 private static long mTardis = 0; 128 private static String sVersion = null; 129 130 /** 131 * Returns whether the SDK is the Jellybean release or later. 132 */ isJellybeanOrLater()133 public static boolean isJellybeanOrLater() { 134 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 135 } 136 getViewTypeFromIntentAndSharedPref(Activity activity)137 public static int getViewTypeFromIntentAndSharedPref(Activity activity) { 138 Intent intent = activity.getIntent(); 139 Bundle extras = intent.getExtras(); 140 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity); 141 142 if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) { 143 return ViewType.EDIT; 144 } 145 if (extras != null) { 146 if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) { 147 // This is the "detail" view which is either agenda or day view 148 return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW, 149 GeneralPreferences.DEFAULT_DETAILED_VIEW); 150 } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) { 151 // Not sure who uses this. This logic came from LaunchActivity 152 return ViewType.DAY; 153 } 154 } 155 156 // Default to the last view 157 return prefs.getInt( 158 GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW); 159 } 160 161 /** 162 * Gets the intent action for telling the widget to update. 163 */ getWidgetUpdateAction(Context context)164 public static String getWidgetUpdateAction(Context context) { 165 return context.getPackageName() + ".APPWIDGET_UPDATE"; 166 } 167 168 /** 169 * Gets the intent action for telling the widget to update. 170 */ getWidgetScheduledUpdateAction(Context context)171 public static String getWidgetScheduledUpdateAction(Context context) { 172 return context.getPackageName() + ".APPWIDGET_SCHEDULED_UPDATE"; 173 } 174 175 /** 176 * Gets the intent action for telling the widget to update. 177 */ getSearchAuthority(Context context)178 public static String getSearchAuthority(Context context) { 179 return context.getPackageName() + ".CalendarRecentSuggestionsProvider"; 180 } 181 182 /** 183 * Writes a new home time zone to the db. Updates the home time zone in the 184 * db asynchronously and updates the local cache. Sending a time zone of 185 * **tbd** will cause it to be set to the device's time zone. null or empty 186 * tz will be ignored. 187 * 188 * @param context The calling activity 189 * @param timeZone The time zone to set Calendar to, or **tbd** 190 */ setTimeZone(Context context, String timeZone)191 public static void setTimeZone(Context context, String timeZone) { 192 mTZUtils.setTimeZone(context, timeZone); 193 } 194 195 /** 196 * Gets the time zone that Calendar should be displayed in This is a helper 197 * method to get the appropriate time zone for Calendar. If this is the 198 * first time this method has been called it will initiate an asynchronous 199 * query to verify that the data in preferences is correct. The callback 200 * supplied will only be called if this query returns a value other than 201 * what is stored in preferences and should cause the calling activity to 202 * refresh anything that depends on calling this method. 203 * 204 * @param context The calling activity 205 * @param callback The runnable that should execute if a query returns new 206 * values 207 * @return The string value representing the time zone Calendar should 208 * display 209 */ getTimeZone(Context context, Runnable callback)210 public static String getTimeZone(Context context, Runnable callback) { 211 return mTZUtils.getTimeZone(context, callback); 212 } 213 214 /** 215 * Formats a date or a time range according to the local conventions. 216 * 217 * @param context the context is required only if the time is shown 218 * @param startMillis the start time in UTC milliseconds 219 * @param endMillis the end time in UTC milliseconds 220 * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter, 221 * long, long, int, String) formatDateRange} 222 * @return a string containing the formatted date/time range. 223 */ formatDateRange( Context context, long startMillis, long endMillis, int flags)224 public static String formatDateRange( 225 Context context, long startMillis, long endMillis, int flags) { 226 return mTZUtils.formatDateRange(context, startMillis, endMillis, flags); 227 } 228 getDefaultVibrate(Context context, SharedPreferences prefs)229 public static boolean getDefaultVibrate(Context context, SharedPreferences prefs) { 230 boolean vibrate; 231 if (prefs.contains(KEY_ALERTS_VIBRATE_WHEN)) { 232 // Migrate setting to new 4.2 behavior 233 // 234 // silent and never -> off 235 // always -> on 236 String vibrateWhen = prefs.getString(KEY_ALERTS_VIBRATE_WHEN, null); 237 vibrate = vibrateWhen != null && vibrateWhen.equals(context 238 .getString(R.string.prefDefault_alerts_vibrate_true)); 239 prefs.edit().remove(KEY_ALERTS_VIBRATE_WHEN).commit(); 240 Log.d(TAG, "Migrating KEY_ALERTS_VIBRATE_WHEN(" + vibrateWhen 241 + ") to KEY_ALERTS_VIBRATE = " + vibrate); 242 } else { 243 vibrate = prefs.getBoolean(GeneralPreferences.KEY_ALERTS_VIBRATE, 244 false); 245 } 246 return vibrate; 247 } 248 getSharedPreference(Context context, String key, String[] defaultValue)249 public static String[] getSharedPreference(Context context, String key, String[] defaultValue) { 250 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 251 Set<String> ss = prefs.getStringSet(key, null); 252 if (ss != null) { 253 String strings[] = new String[ss.size()]; 254 return ss.toArray(strings); 255 } 256 return defaultValue; 257 } 258 getSharedPreference(Context context, String key, String defaultValue)259 public static String getSharedPreference(Context context, String key, String defaultValue) { 260 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 261 return prefs.getString(key, defaultValue); 262 } 263 getSharedPreference(Context context, String key, int defaultValue)264 public static int getSharedPreference(Context context, String key, int defaultValue) { 265 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 266 return prefs.getInt(key, defaultValue); 267 } 268 getSharedPreference(Context context, String key, boolean defaultValue)269 public static boolean getSharedPreference(Context context, String key, boolean defaultValue) { 270 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 271 return prefs.getBoolean(key, defaultValue); 272 } 273 274 /** 275 * Asynchronously sets the preference with the given key to the given value 276 * 277 * @param context the context to use to get preferences from 278 * @param key the key of the preference to set 279 * @param value the value to set 280 */ setSharedPreference(Context context, String key, String value)281 public static void setSharedPreference(Context context, String key, String value) { 282 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 283 prefs.edit().putString(key, value).apply(); 284 } 285 setSharedPreference(Context context, String key, String[] values)286 public static void setSharedPreference(Context context, String key, String[] values) { 287 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 288 LinkedHashSet<String> set = new LinkedHashSet<String>(); 289 for (String value : values) { 290 set.add(value); 291 } 292 prefs.edit().putStringSet(key, set).apply(); 293 } 294 tardis()295 protected static void tardis() { 296 mTardis = System.currentTimeMillis(); 297 } 298 getTardis()299 protected static long getTardis() { 300 return mTardis; 301 } 302 setSharedPreference(Context context, String key, boolean value)303 static void setSharedPreference(Context context, String key, boolean value) { 304 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 305 SharedPreferences.Editor editor = prefs.edit(); 306 editor.putBoolean(key, value); 307 editor.apply(); 308 } 309 setSharedPreference(Context context, String key, int value)310 static void setSharedPreference(Context context, String key, int value) { 311 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 312 SharedPreferences.Editor editor = prefs.edit(); 313 editor.putInt(key, value); 314 editor.apply(); 315 } 316 317 /** 318 * Save default agenda/day/week/month view for next time 319 * 320 * @param context 321 * @param viewId {@link CalendarController.ViewType} 322 */ setDefaultView(Context context, int viewId)323 static void setDefaultView(Context context, int viewId) { 324 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 325 SharedPreferences.Editor editor = prefs.edit(); 326 327 boolean validDetailView = false; 328 if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) { 329 validDetailView = true; 330 } else { 331 validDetailView = viewId == CalendarController.ViewType.AGENDA 332 || viewId == CalendarController.ViewType.DAY; 333 } 334 335 if (validDetailView) { 336 // Record the detail start view 337 editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId); 338 } 339 340 // Record the (new) start view 341 editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId); 342 editor.apply(); 343 } 344 matrixCursorFromCursor(Cursor cursor)345 public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { 346 if (cursor == null) { 347 return null; 348 } 349 350 String[] columnNames = cursor.getColumnNames(); 351 if (columnNames == null) { 352 columnNames = new String[] {}; 353 } 354 MatrixCursor newCursor = new MatrixCursor(columnNames); 355 int numColumns = cursor.getColumnCount(); 356 String data[] = new String[numColumns]; 357 cursor.moveToPosition(-1); 358 while (cursor.moveToNext()) { 359 for (int i = 0; i < numColumns; i++) { 360 data[i] = cursor.getString(i); 361 } 362 newCursor.addRow(data); 363 } 364 return newCursor; 365 } 366 367 /** 368 * Compares two cursors to see if they contain the same data. 369 * 370 * @return Returns true of the cursors contain the same data and are not 371 * null, false otherwise 372 */ compareCursors(Cursor c1, Cursor c2)373 public static boolean compareCursors(Cursor c1, Cursor c2) { 374 if (c1 == null || c2 == null) { 375 return false; 376 } 377 378 int numColumns = c1.getColumnCount(); 379 if (numColumns != c2.getColumnCount()) { 380 return false; 381 } 382 383 if (c1.getCount() != c2.getCount()) { 384 return false; 385 } 386 387 c1.moveToPosition(-1); 388 c2.moveToPosition(-1); 389 while (c1.moveToNext() && c2.moveToNext()) { 390 for (int i = 0; i < numColumns; i++) { 391 if (!TextUtils.equals(c1.getString(i), c2.getString(i))) { 392 return false; 393 } 394 } 395 } 396 397 return true; 398 } 399 400 /** 401 * If the given intent specifies a time (in milliseconds since the epoch), 402 * then that time is returned. Otherwise, the current time is returned. 403 */ timeFromIntentInMillis(Intent intent)404 public static final long timeFromIntentInMillis(Intent intent) { 405 // If the time was specified, then use that. Otherwise, use the current 406 // time. 407 Uri data = intent.getData(); 408 long millis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1); 409 if (millis == -1 && data != null && data.isHierarchical()) { 410 List<String> path = data.getPathSegments(); 411 if (path.size() == 2 && path.get(0).equals("time")) { 412 try { 413 millis = Long.valueOf(data.getLastPathSegment()); 414 } catch (NumberFormatException e) { 415 Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " 416 + "found. Using current time."); 417 } 418 } 419 } 420 if (millis <= 0) { 421 millis = System.currentTimeMillis(); 422 } 423 return millis; 424 } 425 426 /** 427 * Formats the given Time object so that it gives the month and year (for 428 * example, "September 2007"). 429 * 430 * @param time the time to format 431 * @return the string containing the weekday and the date 432 */ formatMonthYear(Context context, Time time)433 public static String formatMonthYear(Context context, Time time) { 434 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY 435 | DateUtils.FORMAT_SHOW_YEAR; 436 long millis = time.toMillis(true); 437 return formatDateRange(context, millis, millis, flags); 438 } 439 440 /** 441 * Returns a list joined together by the provided delimiter, for example, 442 * ["a", "b", "c"] could be joined into "a,b,c" 443 * 444 * @param things the things to join together 445 * @param delim the delimiter to use 446 * @return a string contained the things joined together 447 */ join(List<?> things, String delim)448 public static String join(List<?> things, String delim) { 449 StringBuilder builder = new StringBuilder(); 450 boolean first = true; 451 for (Object thing : things) { 452 if (first) { 453 first = false; 454 } else { 455 builder.append(delim); 456 } 457 builder.append(thing.toString()); 458 } 459 return builder.toString(); 460 } 461 462 /** 463 * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970) 464 * adjusted for first day of week. 465 * 466 * This takes a julian day and the week start day and calculates which 467 * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting 468 * at 0. *Do not* use this to compute the ISO week number for the year. 469 * 470 * @param julianDay The julian day to calculate the week number for 471 * @param firstDayOfWeek Which week day is the first day of the week, 472 * see {@link Time#SUNDAY} 473 * @return Weeks since the epoch 474 */ getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)475 public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) { 476 int diff = Time.THURSDAY - firstDayOfWeek; 477 if (diff < 0) { 478 diff += 7; 479 } 480 int refDay = Time.EPOCH_JULIAN_DAY - diff; 481 return (julianDay - refDay) / 7; 482 } 483 484 /** 485 * Takes a number of weeks since the epoch and calculates the Julian day of 486 * the Monday for that week. 487 * 488 * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY} 489 * is considered week 0. It returns the Julian day for the Monday 490 * {@code week} weeks after the Monday of the week containing the epoch. 491 * 492 * @param week Number of weeks since the epoch 493 * @return The julian day for the Monday of the given week since the epoch 494 */ getJulianMondayFromWeeksSinceEpoch(int week)495 public static int getJulianMondayFromWeeksSinceEpoch(int week) { 496 return MONDAY_BEFORE_JULIAN_EPOCH + week * 7; 497 } 498 499 /** 500 * Get first day of week as android.text.format.Time constant. 501 * 502 * @return the first day of week in android.text.format.Time 503 */ getFirstDayOfWeek(Context context)504 public static int getFirstDayOfWeek(Context context) { 505 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 506 String pref = prefs.getString( 507 GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT); 508 509 int startDay; 510 if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) { 511 startDay = Calendar.getInstance().getFirstDayOfWeek(); 512 } else { 513 startDay = Integer.parseInt(pref); 514 } 515 516 if (startDay == Calendar.SATURDAY) { 517 return Time.SATURDAY; 518 } else if (startDay == Calendar.MONDAY) { 519 return Time.MONDAY; 520 } else { 521 return Time.SUNDAY; 522 } 523 } 524 525 /** 526 * @return true when week number should be shown. 527 */ getShowWeekNumber(Context context)528 public static boolean getShowWeekNumber(Context context) { 529 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 530 return prefs.getBoolean( 531 GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM); 532 } 533 534 /** 535 * @return true when declined events should be hidden. 536 */ getHideDeclinedEvents(Context context)537 public static boolean getHideDeclinedEvents(Context context) { 538 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 539 return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false); 540 } 541 getDaysPerWeek(Context context)542 public static int getDaysPerWeek(Context context) { 543 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 544 return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7); 545 } 546 547 /** 548 * Determine whether the column position is Saturday or not. 549 * 550 * @param column the column position 551 * @param firstDayOfWeek the first day of week in android.text.format.Time 552 * @return true if the column is Saturday position 553 */ isSaturday(int column, int firstDayOfWeek)554 public static boolean isSaturday(int column, int firstDayOfWeek) { 555 return (firstDayOfWeek == Time.SUNDAY && column == 6) 556 || (firstDayOfWeek == Time.MONDAY && column == 5) 557 || (firstDayOfWeek == Time.SATURDAY && column == 0); 558 } 559 560 /** 561 * Determine whether the column position is Sunday or not. 562 * 563 * @param column the column position 564 * @param firstDayOfWeek the first day of week in android.text.format.Time 565 * @return true if the column is Sunday position 566 */ isSunday(int column, int firstDayOfWeek)567 public static boolean isSunday(int column, int firstDayOfWeek) { 568 return (firstDayOfWeek == Time.SUNDAY && column == 0) 569 || (firstDayOfWeek == Time.MONDAY && column == 6) 570 || (firstDayOfWeek == Time.SATURDAY && column == 1); 571 } 572 573 /** 574 * Convert given UTC time into current local time. This assumes it is for an 575 * allday event and will adjust the time to be on a midnight boundary. 576 * 577 * @param recycle Time object to recycle, otherwise null. 578 * @param utcTime Time to convert, in UTC. 579 * @param tz The time zone to convert this time to. 580 */ convertAlldayUtcToLocal(Time recycle, long utcTime, String tz)581 public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) { 582 if (recycle == null) { 583 recycle = new Time(); 584 } 585 recycle.timezone = Time.TIMEZONE_UTC; 586 recycle.set(utcTime); 587 recycle.timezone = tz; 588 return recycle.normalize(true); 589 } 590 convertAlldayLocalToUTC(Time recycle, long localTime, String tz)591 public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) { 592 if (recycle == null) { 593 recycle = new Time(); 594 } 595 recycle.timezone = tz; 596 recycle.set(localTime); 597 recycle.timezone = Time.TIMEZONE_UTC; 598 return recycle.normalize(true); 599 } 600 601 /** 602 * Finds and returns the next midnight after "theTime" in milliseconds UTC 603 * 604 * @param recycle - Time object to recycle, otherwise null. 605 * @param theTime - Time used for calculations (in UTC) 606 * @param tz The time zone to convert this time to. 607 */ getNextMidnight(Time recycle, long theTime, String tz)608 public static long getNextMidnight(Time recycle, long theTime, String tz) { 609 if (recycle == null) { 610 recycle = new Time(); 611 } 612 recycle.timezone = tz; 613 recycle.set(theTime); 614 recycle.monthDay ++; 615 recycle.hour = 0; 616 recycle.minute = 0; 617 recycle.second = 0; 618 return recycle.normalize(true); 619 } 620 621 /** 622 * Scan through a cursor of calendars and check if names are duplicated. 623 * This travels a cursor containing calendar display names and fills in the 624 * provided map with whether or not each name is repeated. 625 * 626 * @param isDuplicateName The map to put the duplicate check results in. 627 * @param cursor The query of calendars to check 628 * @param nameIndex The column of the query that contains the display name 629 */ checkForDuplicateNames( Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex)630 public static void checkForDuplicateNames( 631 Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) { 632 isDuplicateName.clear(); 633 cursor.moveToPosition(-1); 634 while (cursor.moveToNext()) { 635 String displayName = cursor.getString(nameIndex); 636 // Set it to true if we've seen this name before, false otherwise 637 if (displayName != null) { 638 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName)); 639 } 640 } 641 } 642 643 /** 644 * Null-safe object comparison 645 * 646 * @param s1 647 * @param s2 648 * @return 649 */ equals(Object o1, Object o2)650 public static boolean equals(Object o1, Object o2) { 651 return o1 == null ? o2 == null : o1.equals(o2); 652 } 653 setAllowWeekForDetailView(boolean allowWeekView)654 public static void setAllowWeekForDetailView(boolean allowWeekView) { 655 mAllowWeekForDetailView = allowWeekView; 656 } 657 getAllowWeekForDetailView()658 public static boolean getAllowWeekForDetailView() { 659 return mAllowWeekForDetailView; 660 } 661 getConfigBool(Context c, int key)662 public static boolean getConfigBool(Context c, int key) { 663 return c.getResources().getBoolean(key); 664 } 665 getDisplayColorFromColor(int color)666 public static int getDisplayColorFromColor(int color) { 667 if (!isJellybeanOrLater()) { 668 return color; 669 } 670 671 float[] hsv = new float[3]; 672 Color.colorToHSV(color, hsv); 673 hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f); 674 hsv[2] = hsv[2] * INTENSITY_ADJUST; 675 return Color.HSVToColor(hsv); 676 } 677 678 // This takes a color and computes what it would look like blended with 679 // white. The result is the color that should be used for declined events. getDeclinedColorFromColor(int color)680 public static int getDeclinedColorFromColor(int color) { 681 int bg = 0xffffffff; 682 int a = DECLINED_EVENT_ALPHA; 683 int r = (((color & 0x00ff0000) * a) + ((bg & 0x00ff0000) * (0xff - a))) & 0xff000000; 684 int g = (((color & 0x0000ff00) * a) + ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000; 685 int b = (((color & 0x000000ff) * a) + ((bg & 0x000000ff) * (0xff - a))) & 0x0000ff00; 686 return (0xff000000) | ((r | g | b) >> 8); 687 } 688 689 // A single strand represents one color of events. Events are divided up by 690 // color to make them convenient to draw. The black strand is special in 691 // that it holds conflicting events as well as color settings for allday on 692 // each day. 693 public static class DNAStrand { 694 public float[] points; 695 public int[] allDays; // color for the allday, 0 means no event 696 int position; 697 public int color; 698 int count; 699 } 700 701 // A segment is a single continuous length of time occupied by a single 702 // color. Segments should never span multiple days. 703 private static class DNASegment { 704 int startMinute; // in minutes since the start of the week 705 int endMinute; 706 int color; // Calendar color or black for conflicts 707 int day; // quick reference to the day this segment is on 708 } 709 710 /** 711 * Converts a list of events to a list of segments to draw. Assumes list is 712 * ordered by start time of the events. The function processes events for a 713 * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1. 714 * The algorithm goes over all the events and creates a set of segments 715 * ordered by start time. This list of segments is then converted into a 716 * HashMap of strands which contain the draw points and are organized by 717 * color. The strands can then be drawn by setting the paint color to each 718 * strand's color and calling drawLines on its set of points. The points are 719 * set up using the following parameters. 720 * <ul> 721 * <li>Events between midnight and WORK_DAY_START_MINUTES are compressed 722 * into the first 1/8th of the space between top and bottom.</li> 723 * <li>Events between WORK_DAY_END_MINUTES and the following midnight are 724 * compressed into the last 1/8th of the space between top and bottom</li> 725 * <li>Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use 726 * the remaining 3/4ths of the space</li> 727 * <li>All segments drawn will maintain at least minPixels height, except 728 * for conflicts in the first or last 1/8th, which may be smaller</li> 729 * </ul> 730 * 731 * @param firstJulianDay The julian day of the first day of events 732 * @param events A list of events sorted by start time 733 * @param top The lowest y value the dna should be drawn at 734 * @param bottom The highest y value the dna should be drawn at 735 * @param dayXs An array of x values to draw the dna at, one for each day 736 * @param conflictColor the color to use for conflicts 737 * @return 738 */ createDNAStrands(int firstJulianDay, ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, Context context)739 public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay, 740 ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, 741 Context context) { 742 743 if (!mMinutesLoaded) { 744 if (context == null) { 745 Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA."); 746 } 747 Resources res = context.getResources(); 748 CONFLICT_COLOR = res.getColor(R.color.month_dna_conflict_time_color); 749 WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes); 750 WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes); 751 WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES; 752 WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES; 753 mMinutesLoaded = true; 754 } 755 756 if (events == null || events.isEmpty() || dayXs == null || dayXs.length < 1 757 || bottom - top < 8 || minPixels < 0) { 758 Log.e(TAG, 759 "Bad values for createDNAStrands! events:" + events + " dayXs:" 760 + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:" 761 + minPixels); 762 return null; 763 } 764 765 LinkedList<DNASegment> segments = new LinkedList<DNASegment>(); 766 HashMap<Integer, DNAStrand> strands = new HashMap<Integer, DNAStrand>(); 767 // add a black strand by default, other colors will get added in 768 // the loop 769 DNAStrand blackStrand = new DNAStrand(); 770 blackStrand.color = CONFLICT_COLOR; 771 strands.put(CONFLICT_COLOR, blackStrand); 772 // the min length is the number of minutes that will occupy 773 // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the 774 // minutes/pixel * minpx where the number of pixels are 3/4 the total 775 // dna height: 4*(mins/(px * 3/4)) 776 int minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top)); 777 778 // There are slightly fewer than half as many pixels in 1/6 the space, 779 // so round to 2.5x for the min minutes in the non-work area 780 int minOtherMinutes = minMinutes * 5 / 2; 781 int lastJulianDay = firstJulianDay + dayXs.length - 1; 782 783 Event event = new Event(); 784 // Go through all the events for the week 785 for (Event currEvent : events) { 786 // if this event is outside the weeks range skip it 787 if (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay) { 788 continue; 789 } 790 if (currEvent.drawAsAllday()) { 791 addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.length); 792 continue; 793 } 794 // Copy the event over so we can clip its start and end to our range 795 currEvent.copyTo(event); 796 if (event.startDay < firstJulianDay) { 797 event.startDay = firstJulianDay; 798 event.startTime = 0; 799 } 800 // If it starts after the work day make sure the start is at least 801 // minPixels from midnight 802 if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) { 803 event.startTime = DAY_IN_MINUTES - minOtherMinutes; 804 } 805 if (event.endDay > lastJulianDay) { 806 event.endDay = lastJulianDay; 807 event.endTime = DAY_IN_MINUTES - 1; 808 } 809 // If the end time is before the work day make sure it ends at least 810 // minPixels after midnight 811 if (event.endTime < minOtherMinutes) { 812 event.endTime = minOtherMinutes; 813 } 814 // If the start and end are on the same day make sure they are at 815 // least minPixels apart. This only needs to be done for times 816 // outside the work day as the min distance for within the work day 817 // is enforced in the segment code. 818 if (event.startDay == event.endDay && 819 event.endTime - event.startTime < minOtherMinutes) { 820 // If it's less than minPixels in an area before the work 821 // day 822 if (event.startTime < WORK_DAY_START_MINUTES) { 823 // extend the end to the first easy guarantee that it's 824 // minPixels 825 event.endTime = Math.min(event.startTime + minOtherMinutes, 826 WORK_DAY_START_MINUTES + minMinutes); 827 // if it's in the area after the work day 828 } else if (event.endTime > WORK_DAY_END_MINUTES) { 829 // First try shifting the end but not past midnight 830 event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1); 831 // if it's still too small move the start back 832 if (event.endTime - event.startTime < minOtherMinutes) { 833 event.startTime = event.endTime - minOtherMinutes; 834 } 835 } 836 } 837 838 // This handles adding the first segment 839 if (segments.size() == 0) { 840 addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes); 841 continue; 842 } 843 // Now compare our current start time to the end time of the last 844 // segment in the list 845 DNASegment lastSegment = segments.getLast(); 846 int startMinute = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime; 847 int endMinute = Math.max((event.endDay - firstJulianDay) * DAY_IN_MINUTES 848 + event.endTime, startMinute + minMinutes); 849 850 if (startMinute < 0) { 851 startMinute = 0; 852 } 853 if (endMinute >= WEEK_IN_MINUTES) { 854 endMinute = WEEK_IN_MINUTES - 1; 855 } 856 // If we start before the last segment in the list ends we need to 857 // start going through the list as this may conflict with other 858 // events 859 if (startMinute < lastSegment.endMinute) { 860 int i = segments.size(); 861 // find the last segment this event intersects with 862 while (--i >= 0 && endMinute < segments.get(i).startMinute); 863 864 DNASegment currSegment; 865 // for each segment this event intersects with 866 for (; i >= 0 && startMinute <= (currSegment = segments.get(i)).endMinute; i--) { 867 // if the segment is already a conflict ignore it 868 if (currSegment.color == CONFLICT_COLOR) { 869 continue; 870 } 871 // if the event ends before the segment and wouldn't create 872 // a segment that is too small split off the right side 873 if (endMinute < currSegment.endMinute - minMinutes) { 874 DNASegment rhs = new DNASegment(); 875 rhs.endMinute = currSegment.endMinute; 876 rhs.color = currSegment.color; 877 rhs.startMinute = endMinute + 1; 878 rhs.day = currSegment.day; 879 currSegment.endMinute = endMinute; 880 segments.add(i + 1, rhs); 881 strands.get(rhs.color).count++; 882 if (DEBUG) { 883 Log.d(TAG, "Added rhs, curr:" + currSegment.toString() + " i:" 884 + segments.get(i).toString()); 885 } 886 } 887 // if the event starts after the segment and wouldn't create 888 // a segment that is too small split off the left side 889 if (startMinute > currSegment.startMinute + minMinutes) { 890 DNASegment lhs = new DNASegment(); 891 lhs.startMinute = currSegment.startMinute; 892 lhs.color = currSegment.color; 893 lhs.endMinute = startMinute - 1; 894 lhs.day = currSegment.day; 895 currSegment.startMinute = startMinute; 896 // increment i so that we are at the right position when 897 // referencing the segments to the right and left of the 898 // current segment. 899 segments.add(i++, lhs); 900 strands.get(lhs.color).count++; 901 if (DEBUG) { 902 Log.d(TAG, "Added lhs, curr:" + currSegment.toString() + " i:" 903 + segments.get(i).toString()); 904 } 905 } 906 // if the right side is black merge this with the segment to 907 // the right if they're on the same day and overlap 908 if (i + 1 < segments.size()) { 909 DNASegment rhs = segments.get(i + 1); 910 if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day 911 && rhs.startMinute <= currSegment.endMinute + 1) { 912 rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute); 913 segments.remove(currSegment); 914 strands.get(currSegment.color).count--; 915 // point at the new current segment 916 currSegment = rhs; 917 } 918 } 919 // if the left side is black merge this with the segment to 920 // the left if they're on the same day and overlap 921 if (i - 1 >= 0) { 922 DNASegment lhs = segments.get(i - 1); 923 if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day 924 && lhs.endMinute >= currSegment.startMinute - 1) { 925 lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute); 926 segments.remove(currSegment); 927 strands.get(currSegment.color).count--; 928 // point at the new current segment 929 currSegment = lhs; 930 // point i at the new current segment in case new 931 // code is added 932 i--; 933 } 934 } 935 // if we're still not black, decrement the count for the 936 // color being removed, change this to black, and increment 937 // the black count 938 if (currSegment.color != CONFLICT_COLOR) { 939 strands.get(currSegment.color).count--; 940 currSegment.color = CONFLICT_COLOR; 941 strands.get(CONFLICT_COLOR).count++; 942 } 943 } 944 945 } 946 // If this event extends beyond the last segment add a new segment 947 if (endMinute > lastSegment.endMinute) { 948 addNewSegment(segments, event, strands, firstJulianDay, lastSegment.endMinute, 949 minMinutes); 950 } 951 } 952 weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs); 953 return strands; 954 } 955 956 // This figures out allDay colors as allDay events are found addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands, int firstJulianDay, int numDays)957 private static void addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands, 958 int firstJulianDay, int numDays) { 959 DNAStrand strand = getOrCreateStrand(strands, CONFLICT_COLOR); 960 // if we haven't initialized the allDay portion create it now 961 if (strand.allDays == null) { 962 strand.allDays = new int[numDays]; 963 } 964 965 // For each day this event is on update the color 966 int end = Math.min(event.endDay - firstJulianDay, numDays - 1); 967 for (int i = Math.max(event.startDay - firstJulianDay, 0); i <= end; i++) { 968 if (strand.allDays[i] != 0) { 969 // if this day already had a color, it is now a conflict 970 strand.allDays[i] = CONFLICT_COLOR; 971 } else { 972 // else it's just the color of the event 973 strand.allDays[i] = event.color; 974 } 975 } 976 } 977 978 // This processes all the segments, sorts them by color, and generates a 979 // list of points to draw weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay, HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs)980 private static void weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay, 981 HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs) { 982 // First, get rid of any colors that ended up with no segments 983 Iterator<DNAStrand> strandIterator = strands.values().iterator(); 984 while (strandIterator.hasNext()) { 985 DNAStrand strand = strandIterator.next(); 986 if (strand.count < 1 && strand.allDays == null) { 987 strandIterator.remove(); 988 continue; 989 } 990 strand.points = new float[strand.count * 4]; 991 strand.position = 0; 992 } 993 // Go through each segment and compute its points 994 for (DNASegment segment : segments) { 995 // Add the points to the strand of that color 996 DNAStrand strand = strands.get(segment.color); 997 int dayIndex = segment.day - firstJulianDay; 998 int dayStartMinute = segment.startMinute % DAY_IN_MINUTES; 999 int dayEndMinute = segment.endMinute % DAY_IN_MINUTES; 1000 int height = bottom - top; 1001 int workDayHeight = height * 3 / 4; 1002 int remainderHeight = (height - workDayHeight) / 2; 1003 1004 int x = dayXs[dayIndex]; 1005 int y0 = 0; 1006 int y1 = 0; 1007 1008 y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight); 1009 y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight); 1010 if (DEBUG) { 1011 Log.d(TAG, "Adding " + Integer.toHexString(segment.color) + " at x,y0,y1: " + x 1012 + " " + y0 + " " + y1 + " for " + dayStartMinute + " " + dayEndMinute); 1013 } 1014 strand.points[strand.position++] = x; 1015 strand.points[strand.position++] = y0; 1016 strand.points[strand.position++] = x; 1017 strand.points[strand.position++] = y1; 1018 } 1019 } 1020 1021 /** 1022 * Compute a pixel offset from the top for a given minute from the work day 1023 * height and the height of the top area. 1024 */ getPixelOffsetFromMinutes(int minute, int workDayHeight, int remainderHeight)1025 private static int getPixelOffsetFromMinutes(int minute, int workDayHeight, 1026 int remainderHeight) { 1027 int y; 1028 if (minute < WORK_DAY_START_MINUTES) { 1029 y = minute * remainderHeight / WORK_DAY_START_MINUTES; 1030 } else if (minute < WORK_DAY_END_MINUTES) { 1031 y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * workDayHeight 1032 / WORK_DAY_MINUTES; 1033 } else { 1034 y = remainderHeight + workDayHeight + (minute - WORK_DAY_END_MINUTES) * remainderHeight 1035 / WORK_DAY_END_LENGTH; 1036 } 1037 return y; 1038 } 1039 1040 /** 1041 * Add a new segment based on the event provided. This will handle splitting 1042 * segments across day boundaries and ensures a minimum size for segments. 1043 */ addNewSegment(LinkedList<DNASegment> segments, Event event, HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes)1044 private static void addNewSegment(LinkedList<DNASegment> segments, Event event, 1045 HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes) { 1046 if (event.startDay > event.endDay) { 1047 Log.wtf(TAG, "Event starts after it ends: " + event.toString()); 1048 } 1049 // If this is a multiday event split it up by day 1050 if (event.startDay != event.endDay) { 1051 Event lhs = new Event(); 1052 lhs.color = event.color; 1053 lhs.startDay = event.startDay; 1054 // the first day we want the start time to be the actual start time 1055 lhs.startTime = event.startTime; 1056 lhs.endDay = lhs.startDay; 1057 lhs.endTime = DAY_IN_MINUTES - 1; 1058 // Nearly recursive iteration! 1059 while (lhs.startDay != event.endDay) { 1060 addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes); 1061 // The days in between are all day, even though that shouldn't 1062 // actually happen due to the allday filtering 1063 lhs.startDay++; 1064 lhs.endDay = lhs.startDay; 1065 lhs.startTime = 0; 1066 minStart = 0; 1067 } 1068 // The last day we want the end time to be the actual end time 1069 lhs.endTime = event.endTime; 1070 event = lhs; 1071 } 1072 // Create the new segment and compute its fields 1073 DNASegment segment = new DNASegment(); 1074 int dayOffset = (event.startDay - firstJulianDay) * DAY_IN_MINUTES; 1075 int endOfDay = dayOffset + DAY_IN_MINUTES - 1; 1076 // clip the start if needed 1077 segment.startMinute = Math.max(dayOffset + event.startTime, minStart); 1078 // and extend the end if it's too small, but not beyond the end of the 1079 // day 1080 int minEnd = Math.min(segment.startMinute + minMinutes, endOfDay); 1081 segment.endMinute = Math.max(dayOffset + event.endTime, minEnd); 1082 if (segment.endMinute > endOfDay) { 1083 segment.endMinute = endOfDay; 1084 } 1085 1086 segment.color = event.color; 1087 segment.day = event.startDay; 1088 segments.add(segment); 1089 // increment the count for the correct color or add a new strand if we 1090 // don't have that color yet 1091 DNAStrand strand = getOrCreateStrand(strands, segment.color); 1092 strand.count++; 1093 } 1094 1095 /** 1096 * Try to get a strand of the given color. Create it if it doesn't exist. 1097 */ getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color)1098 private static DNAStrand getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color) { 1099 DNAStrand strand = strands.get(color); 1100 if (strand == null) { 1101 strand = new DNAStrand(); 1102 strand.color = color; 1103 strand.count = 0; 1104 strands.put(strand.color, strand); 1105 } 1106 return strand; 1107 } 1108 1109 /** 1110 * Sends an intent to launch the top level Calendar view. 1111 * 1112 * @param context 1113 */ returnToCalendarHome(Context context)1114 public static void returnToCalendarHome(Context context) { 1115 Intent launchIntent = new Intent(context, AllInOneActivity.class); 1116 launchIntent.setAction(Intent.ACTION_DEFAULT); 1117 launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1118 launchIntent.putExtra(INTENT_KEY_HOME, true); 1119 context.startActivity(launchIntent); 1120 } 1121 1122 /** 1123 * This sets up a search view to use Calendar's search suggestions provider 1124 * and to allow refining the search. 1125 * 1126 * @param view The {@link SearchView} to set up 1127 * @param act The activity using the view 1128 */ setUpSearchView(SearchView view, Activity act)1129 public static void setUpSearchView(SearchView view, Activity act) { 1130 SearchManager searchManager = (SearchManager) act.getSystemService(Context.SEARCH_SERVICE); 1131 view.setSearchableInfo(searchManager.getSearchableInfo(act.getComponentName())); 1132 view.setQueryRefinementEnabled(true); 1133 } 1134 1135 /** 1136 * Given a context and a time in millis since unix epoch figures out the 1137 * correct week of the year for that time. 1138 * 1139 * @param millisSinceEpoch 1140 * @return 1141 */ getWeekNumberFromTime(long millisSinceEpoch, Context context)1142 public static int getWeekNumberFromTime(long millisSinceEpoch, Context context) { 1143 Time weekTime = new Time(getTimeZone(context, null)); 1144 weekTime.set(millisSinceEpoch); 1145 weekTime.normalize(true); 1146 int firstDayOfWeek = getFirstDayOfWeek(context); 1147 // if the date is on Saturday or Sunday and the start of the week 1148 // isn't Monday we may need to shift the date to be in the correct 1149 // week 1150 if (weekTime.weekDay == Time.SUNDAY 1151 && (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)) { 1152 weekTime.monthDay++; 1153 weekTime.normalize(true); 1154 } else if (weekTime.weekDay == Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) { 1155 weekTime.monthDay += 2; 1156 weekTime.normalize(true); 1157 } 1158 return weekTime.getWeekNumber(); 1159 } 1160 1161 /** 1162 * Formats a day of the week string. This is either just the name of the day 1163 * or a combination of yesterday/today/tomorrow and the day of the week. 1164 * 1165 * @param julianDay The julian day to get the string for 1166 * @param todayJulianDay The julian day for today's date 1167 * @param millis A utc millis since epoch time that falls on julian day 1168 * @param context The calling context, used to get the timezone and do the 1169 * formatting 1170 * @return 1171 */ getDayOfWeekString(int julianDay, int todayJulianDay, long millis, Context context)1172 public static String getDayOfWeekString(int julianDay, int todayJulianDay, long millis, 1173 Context context) { 1174 getTimeZone(context, null); 1175 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 1176 String dayViewText; 1177 if (julianDay == todayJulianDay) { 1178 dayViewText = context.getString(R.string.agenda_today, 1179 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1180 } else if (julianDay == todayJulianDay - 1) { 1181 dayViewText = context.getString(R.string.agenda_yesterday, 1182 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1183 } else if (julianDay == todayJulianDay + 1) { 1184 dayViewText = context.getString(R.string.agenda_tomorrow, 1185 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1186 } else { 1187 dayViewText = mTZUtils.formatDateRange(context, millis, millis, flags).toString(); 1188 } 1189 dayViewText = dayViewText.toUpperCase(); 1190 return dayViewText; 1191 } 1192 1193 // Calculate the time until midnight + 1 second and set the handler to 1194 // do run the runnable setMidnightUpdater(Handler h, Runnable r, String timezone)1195 public static void setMidnightUpdater(Handler h, Runnable r, String timezone) { 1196 if (h == null || r == null || timezone == null) { 1197 return; 1198 } 1199 long now = System.currentTimeMillis(); 1200 Time time = new Time(timezone); 1201 time.set(now); 1202 long runInMillis = (24 * 3600 - time.hour * 3600 - time.minute * 60 - 1203 time.second + 1) * 1000; 1204 h.removeCallbacks(r); 1205 h.postDelayed(r, runInMillis); 1206 } 1207 1208 // Stop the midnight update thread resetMidnightUpdater(Handler h, Runnable r)1209 public static void resetMidnightUpdater(Handler h, Runnable r) { 1210 if (h == null || r == null) { 1211 return; 1212 } 1213 h.removeCallbacks(r); 1214 } 1215 1216 /** 1217 * Returns a string description of the specified time interval. 1218 */ getDisplayedDatetime(long startMillis, long endMillis, long currentMillis, String localTimezone, boolean allDay, Context context)1219 public static String getDisplayedDatetime(long startMillis, long endMillis, long currentMillis, 1220 String localTimezone, boolean allDay, Context context) { 1221 // Configure date/time formatting. 1222 int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY; 1223 int flagsTime = DateUtils.FORMAT_SHOW_TIME; 1224 if (DateFormat.is24HourFormat(context)) { 1225 flagsTime |= DateUtils.FORMAT_24HOUR; 1226 } 1227 1228 Time currentTime = new Time(localTimezone); 1229 currentTime.set(currentMillis); 1230 Resources resources = context.getResources(); 1231 String datetimeString = null; 1232 if (allDay) { 1233 // All day events require special timezone adjustment. 1234 long localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone); 1235 long localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone); 1236 if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) { 1237 // If possible, use "Today" or "Tomorrow" instead of a full date string. 1238 int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), 1239 localStartMillis, currentMillis, currentTime.gmtoff); 1240 if (TODAY == todayOrTomorrow) { 1241 datetimeString = resources.getString(R.string.today); 1242 } else if (TOMORROW == todayOrTomorrow) { 1243 datetimeString = resources.getString(R.string.tomorrow); 1244 } 1245 } 1246 if (datetimeString == null) { 1247 // For multi-day allday events or single-day all-day events that are not 1248 // today or tomorrow, use framework formatter. 1249 Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); 1250 datetimeString = DateUtils.formatDateRange(context, f, startMillis, 1251 endMillis, flagsDate, Time.TIMEZONE_UTC).toString(); 1252 } 1253 } else { 1254 if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) { 1255 // Format the time. 1256 String timeString = Utils.formatDateRange(context, startMillis, endMillis, 1257 flagsTime); 1258 1259 // If possible, use "Today" or "Tomorrow" instead of a full date string. 1260 int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), startMillis, 1261 currentMillis, currentTime.gmtoff); 1262 if (TODAY == todayOrTomorrow) { 1263 // Example: "Today at 1:00pm - 2:00 pm" 1264 datetimeString = resources.getString(R.string.today_at_time_fmt, 1265 timeString); 1266 } else if (TOMORROW == todayOrTomorrow) { 1267 // Example: "Tomorrow at 1:00pm - 2:00 pm" 1268 datetimeString = resources.getString(R.string.tomorrow_at_time_fmt, 1269 timeString); 1270 } else { 1271 // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm" 1272 String dateString = Utils.formatDateRange(context, startMillis, endMillis, 1273 flagsDate); 1274 datetimeString = resources.getString(R.string.date_time_fmt, dateString, 1275 timeString); 1276 } 1277 } else { 1278 // For multiday events, shorten day/month names. 1279 // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm" 1280 int flagsDatetime = flagsDate | flagsTime | DateUtils.FORMAT_ABBREV_MONTH | 1281 DateUtils.FORMAT_ABBREV_WEEKDAY; 1282 datetimeString = Utils.formatDateRange(context, startMillis, endMillis, 1283 flagsDatetime); 1284 } 1285 } 1286 return datetimeString; 1287 } 1288 1289 /** 1290 * Returns the timezone to display in the event info, if the local timezone is different 1291 * from the event timezone. Otherwise returns null. 1292 */ getDisplayedTimezone(long startMillis, String localTimezone, String eventTimezone)1293 public static String getDisplayedTimezone(long startMillis, String localTimezone, 1294 String eventTimezone) { 1295 String tzDisplay = null; 1296 if (!TextUtils.equals(localTimezone, eventTimezone)) { 1297 // Figure out if this is in DST 1298 TimeZone tz = TimeZone.getTimeZone(localTimezone); 1299 if (tz == null || tz.getID().equals("GMT")) { 1300 tzDisplay = localTimezone; 1301 } else { 1302 Time startTime = new Time(localTimezone); 1303 startTime.set(startMillis); 1304 tzDisplay = tz.getDisplayName(startTime.isDst != 0, TimeZone.SHORT); 1305 } 1306 } 1307 return tzDisplay; 1308 } 1309 1310 /** 1311 * Returns whether the specified time interval is in a single day. 1312 */ singleDayEvent(long startMillis, long endMillis, long localGmtOffset)1313 private static boolean singleDayEvent(long startMillis, long endMillis, long localGmtOffset) { 1314 if (startMillis == endMillis) { 1315 return true; 1316 } 1317 1318 // An event ending at midnight should still be a single-day event, so check 1319 // time end-1. 1320 int startDay = Time.getJulianDay(startMillis, localGmtOffset); 1321 int endDay = Time.getJulianDay(endMillis - 1, localGmtOffset); 1322 return startDay == endDay; 1323 } 1324 1325 // Using int constants as a return value instead of an enum to minimize resources. 1326 private static final int TODAY = 1; 1327 private static final int TOMORROW = 2; 1328 private static final int NONE = 0; 1329 1330 /** 1331 * Returns TODAY or TOMORROW if applicable. Otherwise returns NONE. 1332 */ isTodayOrTomorrow(Resources r, long dayMillis, long currentMillis, long localGmtOffset)1333 private static int isTodayOrTomorrow(Resources r, long dayMillis, 1334 long currentMillis, long localGmtOffset) { 1335 int startDay = Time.getJulianDay(dayMillis, localGmtOffset); 1336 int currentDay = Time.getJulianDay(currentMillis, localGmtOffset); 1337 1338 int days = startDay - currentDay; 1339 if (days == 1) { 1340 return TOMORROW; 1341 } else if (days == 0) { 1342 return TODAY; 1343 } else { 1344 return NONE; 1345 } 1346 } 1347 1348 /** 1349 * Create an intent for emailing attendees of an event. 1350 * 1351 * @param resources The resources for translating strings. 1352 * @param eventTitle The title of the event to use as the email subject. 1353 * @param body The default text for the email body. 1354 * @param toEmails The list of emails for the 'to' line. 1355 * @param ccEmails The list of emails for the 'cc' line. 1356 * @param ownerAccount The owner account to use as the email sender. 1357 */ createEmailAttendeesIntent(Resources resources, String eventTitle, String body, List<String> toEmails, List<String> ccEmails, String ownerAccount)1358 public static Intent createEmailAttendeesIntent(Resources resources, String eventTitle, 1359 String body, List<String> toEmails, List<String> ccEmails, String ownerAccount) { 1360 List<String> toList = toEmails; 1361 List<String> ccList = ccEmails; 1362 if (toEmails.size() <= 0) { 1363 if (ccEmails.size() <= 0) { 1364 // TODO: Return a SEND intent if no one to email to, to at least populate 1365 // a draft email with the subject (and no recipients). 1366 throw new IllegalArgumentException("Both toEmails and ccEmails are empty."); 1367 } 1368 1369 // Email app does not work with no "to" recipient. Move all 'cc' to 'to' 1370 // in this case. 1371 toList = ccEmails; 1372 ccList = null; 1373 } 1374 1375 // Use the event title as the email subject (prepended with 'Re: '). 1376 String subject = null; 1377 if (eventTitle != null) { 1378 subject = resources.getString(R.string.email_subject_prefix) + eventTitle; 1379 } 1380 1381 // Use the SENDTO intent with a 'mailto' URI, because using SEND will cause 1382 // the picker to show apps like text messaging, which does not make sense 1383 // for email addresses. We put all data in the URI instead of using the extra 1384 // Intent fields (ie. EXTRA_CC, etc) because some email apps might not handle 1385 // those (though gmail does). 1386 Uri.Builder uriBuilder = new Uri.Builder(); 1387 uriBuilder.scheme("mailto"); 1388 1389 // We will append the first email to the 'mailto' field later (because the 1390 // current state of the Email app requires it). Add the remaining 'to' values 1391 // here. When the email codebase is updated, we can simplify this. 1392 if (toList.size() > 1) { 1393 for (int i = 1; i < toList.size(); i++) { 1394 // The Email app requires repeated parameter settings instead of 1395 // a single comma-separated list. 1396 uriBuilder.appendQueryParameter("to", toList.get(i)); 1397 } 1398 } 1399 1400 // Add the subject parameter. 1401 if (subject != null) { 1402 uriBuilder.appendQueryParameter("subject", subject); 1403 } 1404 1405 // Add the subject parameter. 1406 if (body != null) { 1407 uriBuilder.appendQueryParameter("body", body); 1408 } 1409 1410 // Add the cc parameters. 1411 if (ccList != null && ccList.size() > 0) { 1412 for (String email : ccList) { 1413 uriBuilder.appendQueryParameter("cc", email); 1414 } 1415 } 1416 1417 // Insert the first email after 'mailto:' in the URI manually since Uri.Builder 1418 // doesn't seem to have a way to do this. 1419 String uri = uriBuilder.toString(); 1420 if (uri.startsWith("mailto:")) { 1421 StringBuilder builder = new StringBuilder(uri); 1422 builder.insert(7, Uri.encode(toList.get(0))); 1423 uri = builder.toString(); 1424 } 1425 1426 // Start the email intent. Email from the account of the calendar owner in case there 1427 // are multiple email accounts. 1428 Intent emailIntent = new Intent(android.content.Intent.ACTION_SENDTO, Uri.parse(uri)); 1429 emailIntent.putExtra("fromAccountString", ownerAccount); 1430 1431 // Workaround a Email bug that overwrites the body with this intent extra. If not 1432 // set, it clears the body. 1433 if (body != null) { 1434 emailIntent.putExtra(Intent.EXTRA_TEXT, body); 1435 } 1436 1437 return Intent.createChooser(emailIntent, resources.getString(R.string.email_picker_label)); 1438 } 1439 1440 /** 1441 * Example fake email addresses used as attendee emails are resources like conference rooms, 1442 * or another calendar, etc. These all end in "calendar.google.com". 1443 */ isValidEmail(String email)1444 public static boolean isValidEmail(String email) { 1445 return email != null && !email.endsWith(MACHINE_GENERATED_ADDRESS); 1446 } 1447 1448 /** 1449 * Returns true if: 1450 * (1) the email is not a resource like a conference room or another calendar. 1451 * Catch most of these by filtering out suffix calendar.google.com. 1452 * (2) the email is not equal to the sync account to prevent mailing himself. 1453 */ isEmailableFrom(String email, String syncAccountName)1454 public static boolean isEmailableFrom(String email, String syncAccountName) { 1455 return Utils.isValidEmail(email) && !email.equals(syncAccountName); 1456 } 1457 1458 /** 1459 * Inserts a drawable with today's day into the today's icon in the option menu 1460 * @param icon - today's icon from the options menu 1461 */ setTodayIcon(LayerDrawable icon, Context c, String timezone)1462 public static void setTodayIcon(LayerDrawable icon, Context c, String timezone) { 1463 DayOfMonthDrawable today; 1464 1465 // Reuse current drawable if possible 1466 Drawable currentDrawable = icon.findDrawableByLayerId(R.id.today_icon_day); 1467 if (currentDrawable != null && currentDrawable instanceof DayOfMonthDrawable) { 1468 today = (DayOfMonthDrawable)currentDrawable; 1469 } else { 1470 today = new DayOfMonthDrawable(c); 1471 } 1472 // Set the day and update the icon 1473 Time now = new Time(timezone); 1474 now.setToNow(); 1475 now.normalize(false); 1476 today.setDayOfMonth(now.monthDay); 1477 icon.mutate(); 1478 icon.setDrawableByLayerId(R.id.today_icon_day, today); 1479 } 1480 1481 private static class CalendarBroadcastReceiver extends BroadcastReceiver { 1482 1483 Runnable mCallBack; 1484 CalendarBroadcastReceiver(Runnable callback)1485 public CalendarBroadcastReceiver(Runnable callback) { 1486 super(); 1487 mCallBack = callback; 1488 } 1489 @Override onReceive(Context context, Intent intent)1490 public void onReceive(Context context, Intent intent) { 1491 if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED) || 1492 intent.getAction().equals(Intent.ACTION_TIME_CHANGED) || 1493 intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED) || 1494 intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { 1495 if (mCallBack != null) { 1496 mCallBack.run(); 1497 } 1498 } 1499 } 1500 } 1501 setTimeChangesReceiver(Context c, Runnable callback)1502 public static BroadcastReceiver setTimeChangesReceiver(Context c, Runnable callback) { 1503 IntentFilter filter = new IntentFilter(); 1504 filter.addAction(Intent.ACTION_TIME_CHANGED); 1505 filter.addAction(Intent.ACTION_DATE_CHANGED); 1506 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 1507 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 1508 1509 CalendarBroadcastReceiver r = new CalendarBroadcastReceiver(callback); 1510 c.registerReceiver(r, filter); 1511 return r; 1512 } 1513 clearTimeChangesReceiver(Context c, BroadcastReceiver r)1514 public static void clearTimeChangesReceiver(Context c, BroadcastReceiver r) { 1515 c.unregisterReceiver(r); 1516 } 1517 1518 /** 1519 * Get a list of quick responses used for emailing guests from the 1520 * SharedPreferences. If not are found, get the hard coded ones that shipped 1521 * with the app 1522 * 1523 * @param context 1524 * @return a list of quick responses. 1525 */ getQuickResponses(Context context)1526 public static String[] getQuickResponses(Context context) { 1527 String[] s = Utils.getSharedPreference(context, KEY_QUICK_RESPONSES, (String[]) null); 1528 1529 if (s == null) { 1530 s = context.getResources().getStringArray(R.array.quick_response_defaults); 1531 } 1532 1533 return s; 1534 } 1535 1536 /** 1537 * Return the app version code. 1538 */ getVersionCode(Context context)1539 public static String getVersionCode(Context context) { 1540 if (sVersion == null) { 1541 try { 1542 sVersion = context.getPackageManager().getPackageInfo( 1543 context.getPackageName(), 0).versionName; 1544 } catch (PackageManager.NameNotFoundException e) { 1545 // Can't find version; just leave it blank. 1546 Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName); 1547 } 1548 } 1549 return sVersion; 1550 } 1551 } 1552