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