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