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