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