Utils.java revision 4ecf0648e68bc9339a3d64745008a4617f7bedda
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 * Scan through a cursor of calendars and check if names are duplicated. 530 * This travels a cursor containing calendar display names and fills in the 531 * provided map with whether or not each name is repeated. 532 * 533 * @param isDuplicateName The map to put the duplicate check results in. 534 * @param cursor The query of calendars to check 535 * @param nameIndex The column of the query that contains the display name 536 */ 537 public static void checkForDuplicateNames( 538 Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) { 539 isDuplicateName.clear(); 540 cursor.moveToPosition(-1); 541 while (cursor.moveToNext()) { 542 String displayName = cursor.getString(nameIndex); 543 // Set it to true if we've seen this name before, false otherwise 544 if (displayName != null) { 545 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName)); 546 } 547 } 548 } 549 550 /** 551 * Null-safe object comparison 552 * 553 * @param s1 554 * @param s2 555 * @return 556 */ 557 public static boolean equals(Object o1, Object o2) { 558 return o1 == null ? o2 == null : o1.equals(o2); 559 } 560 561 public static void setAllowWeekForDetailView(boolean allowWeekView) { 562 mAllowWeekForDetailView = allowWeekView; 563 } 564 565 public static boolean getAllowWeekForDetailView() { 566 return mAllowWeekForDetailView; 567 } 568 569 public static boolean isMultiPaneConfiguration (Context c) { 570 return (c.getResources().getConfiguration().screenLayout & 571 Configuration.SCREENLAYOUT_SIZE_XLARGE) != 0; 572 } 573 574 public static boolean getConfigBool(Context c, int key) { 575 return c.getResources().getBoolean(key); 576 } 577 578 public static int getDisplayColorFromColor(int color) { 579 float[] hsv = new float[3]; 580 Color.colorToHSV(color, hsv); 581 hsv[1] = Math.max(hsv[1] - SATURATION_ADJUST, 0.0f); 582 return Color.HSVToColor(hsv); 583 } 584 585 // This takes a color and computes what it would look like blended with 586 // white. The result is the color that should be used for declined events. 587 public static int getDeclinedColorFromColor(int color) { 588 int bg = 0xffffffff; 589 int a = DECLINED_EVENT_ALPHA; 590 int r = (((color & 0x00ff0000) * a) + ((bg & 0x00ff0000) * (0xff - a))) & 0xff000000; 591 int g = (((color & 0x0000ff00) * a) + ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000; 592 int b = (((color & 0x000000ff) * a) + ((bg & 0x000000ff) * (0xff - a))) & 0x0000ff00; 593 return (0xff000000) | ((r | g | b) >> 8); 594 } 595 596 // A single strand represents one color of events. Events are divided up by 597 // color to make them convenient to draw. The black strand is special in 598 // that it holds conflicting events as well as color settings for allday on 599 // each day. 600 public static class DNAStrand { 601 public float[] points; 602 public int[] allDays; // color for the allday, 0 means no event 603 int position; 604 public int color; 605 int count; 606 } 607 608 // A segment is a single continuous length of time occupied by a single 609 // color. Segments should never span multiple days. 610 private static class DNASegment { 611 int startMinute; // in minutes since the start of the week 612 int endMinute; 613 int color; // Calendar color or black for conflicts 614 int day; // quick reference to the day this segment is on 615 } 616 617 /** 618 * Converts a list of events to a list of segments to draw. Assumes list is 619 * ordered by start time of the events. The function processes events for a 620 * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1. 621 * The algorithm goes over all the events and creates a set of segments 622 * ordered by start time. This list of segments is then converted into a 623 * HashMap of strands which contain the draw points and are organized by 624 * color. The strands can then be drawn by setting the paint color to each 625 * strand's color and calling drawLines on its set of points. The points are 626 * set up using the following parameters. 627 * <ul> 628 * <li>Events between midnight and WORK_DAY_START_MINUTES are compressed 629 * into the first 1/8th of the space between top and bottom.</li> 630 * <li>Events between WORK_DAY_END_MINUTES and the following midnight are 631 * compressed into the last 1/8th of the space between top and bottom</li> 632 * <li>Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use 633 * the remaining 3/4ths of the space</li> 634 * <li>All segments drawn will maintain at least minPixels height, except 635 * for conflicts in the first or last 1/8th, which may be smaller</li> 636 * </ul> 637 * 638 * @param firstJulianDay The julian day of the first day of events 639 * @param events A list of events sorted by start time 640 * @param top The lowest y value the dna should be drawn at 641 * @param bottom The highest y value the dna should be drawn at 642 * @param dayXs An array of x values to draw the dna at, one for each day 643 * @param conflictColor the color to use for conflicts 644 * @return 645 */ 646 public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay, 647 ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, 648 Context context) { 649 650 if (!mMinutesLoaded) { 651 if (context == null) { 652 Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA."); 653 } 654 Resources res = context.getResources(); 655 CONFLICT_COLOR = res.getColor(R.color.month_dna_conflict_time_color); 656 WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes); 657 WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes); 658 WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES; 659 WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES; 660 mMinutesLoaded = true; 661 } 662 663 if (events == null || events.isEmpty() || dayXs == null || dayXs.length < 1 664 || bottom - top < 8 || minPixels < 0) { 665 Log.e(TAG, 666 "Bad values for createDNAStrands! events:" + events + " dayXs:" 667 + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:" 668 + minPixels); 669 return null; 670 } 671 672 LinkedList<DNASegment> segments = new LinkedList<DNASegment>(); 673 HashMap<Integer, DNAStrand> strands = new HashMap<Integer, DNAStrand>(); 674 // add a black strand by default, other colors will get added in 675 // the loop 676 DNAStrand blackStrand = new DNAStrand(); 677 blackStrand.color = CONFLICT_COLOR; 678 strands.put(CONFLICT_COLOR, blackStrand); 679 // the min length is the number of minutes that will occupy 680 // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the 681 // minutes/pixel * minpx where the number of pixels are 3/4 the total 682 // dna height: 4*(mins/(px * 3/4)) 683 int minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top)); 684 685 // There are slightly fewer than half as many pixels in 1/6 the space, 686 // so round to 2.5x for the min minutes in the non-work area 687 int minOtherMinutes = minMinutes * 5 / 2; 688 int lastJulianDay = firstJulianDay + dayXs.length - 1; 689 690 Event event = new Event(); 691 // Go through all the events for the week 692 for (Event currEvent : events) { 693 // if this event is outside the weeks range skip it 694 if (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay) { 695 continue; 696 } 697 if (currEvent.drawAsAllday()) { 698 addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.length); 699 continue; 700 } 701 // Copy the event over so we can clip its start and end to our range 702 currEvent.copyTo(event); 703 if (event.startDay < firstJulianDay) { 704 event.startDay = firstJulianDay; 705 event.startTime = 0; 706 } 707 // If it starts after the work day make sure the start is at least 708 // minPixels from midnight 709 if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) { 710 event.startTime = DAY_IN_MINUTES - minOtherMinutes; 711 } 712 if (event.endDay > lastJulianDay) { 713 event.endDay = lastJulianDay; 714 event.endTime = DAY_IN_MINUTES - 1; 715 } 716 // If the end time is before the work day make sure it ends at least 717 // minPixels after midnight 718 if (event.endTime < minOtherMinutes) { 719 event.endTime = minOtherMinutes; 720 } 721 // If the start and end are on the same day make sure they are at 722 // least minPixels apart. This only needs to be done for times 723 // outside the work day as the min distance for within the work day 724 // is enforced in the segment code. 725 if (event.startDay == event.endDay && 726 event.endTime - event.startTime < minOtherMinutes) { 727 // If it's less than minPixels in an area before the work 728 // day 729 if (event.startTime < WORK_DAY_START_MINUTES) { 730 // extend the end to the first easy guarantee that it's 731 // minPixels 732 event.endTime = Math.min(event.startTime + minOtherMinutes, 733 WORK_DAY_START_MINUTES + minMinutes); 734 // if it's in the area after the work day 735 } else if (event.endTime > WORK_DAY_END_MINUTES) { 736 // First try shifting the end but not past midnight 737 event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1); 738 // if it's still too small move the start back 739 if (event.endTime - event.startTime < minOtherMinutes) { 740 event.startTime = event.endTime - minOtherMinutes; 741 } 742 } 743 } 744 745 // This handles adding the first segment 746 if (segments.size() == 0) { 747 addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes); 748 continue; 749 } 750 // Now compare our current start time to the end time of the last 751 // segment in the list 752 DNASegment lastSegment = segments.getLast(); 753 int startMinute = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime; 754 int endMinute = Math.max((event.endDay - firstJulianDay) * DAY_IN_MINUTES 755 + event.endTime, startMinute + minMinutes); 756 757 if (startMinute < 0) { 758 startMinute = 0; 759 } 760 if (endMinute >= WEEK_IN_MINUTES) { 761 endMinute = WEEK_IN_MINUTES - 1; 762 } 763 // If we start before the last segment in the list ends we need to 764 // start going through the list as this may conflict with other 765 // events 766 if (startMinute < lastSegment.endMinute) { 767 int i = segments.size(); 768 // find the last segment this event intersects with 769 while (--i >= 0 && endMinute < segments.get(i).startMinute); 770 771 DNASegment currSegment; 772 // for each segment this event intersects with 773 for (; i >= 0 && startMinute <= (currSegment = segments.get(i)).endMinute; i--) { 774 // if the segment is already a conflict ignore it 775 if (currSegment.color == CONFLICT_COLOR) { 776 continue; 777 } 778 // if the event ends before the segment and wouldn't create 779 // a segment that is too small split off the right side 780 if (endMinute < currSegment.endMinute - minMinutes) { 781 DNASegment rhs = new DNASegment(); 782 rhs.endMinute = currSegment.endMinute; 783 rhs.color = currSegment.color; 784 rhs.startMinute = endMinute + 1; 785 rhs.day = currSegment.day; 786 currSegment.endMinute = endMinute; 787 segments.add(i + 1, rhs); 788 strands.get(rhs.color).count++; 789 if (DEBUG) { 790 Log.d(TAG, "Added rhs, curr:" + currSegment.toString() + " i:" 791 + segments.get(i).toString()); 792 } 793 } 794 // if the event starts after the segment and wouldn't create 795 // a segment that is too small split off the left side 796 if (startMinute > currSegment.startMinute + minMinutes) { 797 DNASegment lhs = new DNASegment(); 798 lhs.startMinute = currSegment.startMinute; 799 lhs.color = currSegment.color; 800 lhs.endMinute = startMinute - 1; 801 lhs.day = currSegment.day; 802 currSegment.startMinute = startMinute; 803 // increment i so that we are at the right position when 804 // referencing the segments to the right and left of the 805 // current segment. 806 segments.add(i++, lhs); 807 strands.get(lhs.color).count++; 808 if (DEBUG) { 809 Log.d(TAG, "Added lhs, curr:" + currSegment.toString() + " i:" 810 + segments.get(i).toString()); 811 } 812 } 813 // if the right side is black merge this with the segment to 814 // the right if they're on the same day and overlap 815 if (i + 1 < segments.size()) { 816 DNASegment rhs = segments.get(i + 1); 817 if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day 818 && rhs.startMinute <= currSegment.endMinute + 1) { 819 rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute); 820 segments.remove(currSegment); 821 strands.get(currSegment.color).count--; 822 // point at the new current segment 823 currSegment = rhs; 824 } 825 } 826 // if the left side is black merge this with the segment to 827 // the left if they're on the same day and overlap 828 if (i - 1 >= 0) { 829 DNASegment lhs = segments.get(i - 1); 830 if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day 831 && lhs.endMinute >= currSegment.startMinute - 1) { 832 lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute); 833 segments.remove(currSegment); 834 strands.get(currSegment.color).count--; 835 // point at the new current segment 836 currSegment = lhs; 837 // point i at the new current segment in case new 838 // code is added 839 i--; 840 } 841 } 842 // if we're still not black, decrement the count for the 843 // color being removed, change this to black, and increment 844 // the black count 845 if (currSegment.color != CONFLICT_COLOR) { 846 strands.get(currSegment.color).count--; 847 currSegment.color = CONFLICT_COLOR; 848 strands.get(CONFLICT_COLOR).count++; 849 } 850 } 851 852 } 853 // If this event extends beyond the last segment add a new segment 854 if (endMinute > lastSegment.endMinute) { 855 addNewSegment(segments, event, strands, firstJulianDay, lastSegment.endMinute, 856 minMinutes); 857 } 858 } 859 weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs); 860 return strands; 861 } 862 863 // This figures out allDay colors as allDay events are found 864 private static void addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands, 865 int firstJulianDay, int numDays) { 866 DNAStrand strand = getOrCreateStrand(strands, CONFLICT_COLOR); 867 // if we haven't initialized the allDay portion create it now 868 if (strand.allDays == null) { 869 strand.allDays = new int[numDays]; 870 } 871 872 // For each day this event is on update the color 873 int end = Math.min(event.endDay - firstJulianDay, numDays - 1); 874 for (int i = Math.max(event.startDay - firstJulianDay, 0); i <= end; i++) { 875 if (strand.allDays[i] != 0) { 876 // if this day already had a color, it is now a conflict 877 strand.allDays[i] = CONFLICT_COLOR; 878 } else { 879 // else it's just the color of the event 880 strand.allDays[i] = event.color; 881 } 882 } 883 } 884 885 // This processes all the segments, sorts them by color, and generates a 886 // list of points to draw 887 private static void weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay, 888 HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs) { 889 // First, get rid of any colors that ended up with no segments 890 Iterator<DNAStrand> strandIterator = strands.values().iterator(); 891 while (strandIterator.hasNext()) { 892 DNAStrand strand = strandIterator.next(); 893 if (strand.count < 1 && strand.allDays == null) { 894 strandIterator.remove(); 895 continue; 896 } 897 strand.points = new float[strand.count * 4]; 898 strand.position = 0; 899 } 900 // Go through each segment and compute its points 901 for (DNASegment segment : segments) { 902 // Add the points to the strand of that color 903 DNAStrand strand = strands.get(segment.color); 904 int dayIndex = segment.day - firstJulianDay; 905 int dayStartMinute = segment.startMinute % DAY_IN_MINUTES; 906 int dayEndMinute = segment.endMinute % DAY_IN_MINUTES; 907 int height = bottom - top; 908 int workDayHeight = height * 3 / 4; 909 int remainderHeight = (height - workDayHeight) / 2; 910 911 int x = dayXs[dayIndex]; 912 int y0 = 0; 913 int y1 = 0; 914 915 y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight); 916 y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight); 917 if (DEBUG) { 918 Log.d(TAG, "Adding " + Integer.toHexString(segment.color) + " at x,y0,y1: " + x 919 + " " + y0 + " " + y1 + " for " + dayStartMinute + " " + dayEndMinute); 920 } 921 strand.points[strand.position++] = x; 922 strand.points[strand.position++] = y0; 923 strand.points[strand.position++] = x; 924 strand.points[strand.position++] = y1; 925 } 926 } 927 928 /** 929 * Compute a pixel offset from the top for a given minute from the work day 930 * height and the height of the top area. 931 */ 932 private static int getPixelOffsetFromMinutes(int minute, int workDayHeight, 933 int remainderHeight) { 934 int y; 935 if (minute < WORK_DAY_START_MINUTES) { 936 y = minute * remainderHeight / WORK_DAY_START_MINUTES; 937 } else if (minute < WORK_DAY_END_MINUTES) { 938 y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * workDayHeight 939 / WORK_DAY_MINUTES; 940 } else { 941 y = remainderHeight + workDayHeight + (minute - WORK_DAY_END_MINUTES) * remainderHeight 942 / WORK_DAY_END_LENGTH; 943 } 944 return y; 945 } 946 947 /** 948 * Add a new segment based on the event provided. This will handle splitting 949 * segments across day boundaries and ensures a minimum size for segments. 950 */ 951 private static void addNewSegment(LinkedList<DNASegment> segments, Event event, 952 HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes) { 953 if (event.startDay > event.endDay) { 954 Log.wtf(TAG, "Event starts after it ends: " + event.toString()); 955 } 956 // If this is a multiday event split it up by day 957 if (event.startDay != event.endDay) { 958 Event lhs = new Event(); 959 lhs.color = event.color; 960 lhs.startDay = event.startDay; 961 // the first day we want the start time to be the actual start time 962 lhs.startTime = event.startTime; 963 lhs.endDay = lhs.startDay; 964 lhs.endTime = DAY_IN_MINUTES - 1; 965 // Nearly recursive iteration! 966 while (lhs.startDay != event.endDay) { 967 addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes); 968 // The days in between are all day, even though that shouldn't 969 // actually happen due to the allday filtering 970 lhs.startDay++; 971 lhs.endDay = lhs.startDay; 972 lhs.startTime = 0; 973 minStart = 0; 974 } 975 // The last day we want the end time to be the actual end time 976 lhs.endTime = event.endTime; 977 event = lhs; 978 } 979 // Create the new segment and compute its fields 980 DNASegment segment = new DNASegment(); 981 int dayOffset = (event.startDay - firstJulianDay) * DAY_IN_MINUTES; 982 int endOfDay = dayOffset + DAY_IN_MINUTES - 1; 983 // clip the start if needed 984 segment.startMinute = Math.max(dayOffset + event.startTime, minStart); 985 // and extend the end if it's too small, but not beyond the end of the 986 // day 987 int minEnd = Math.min(segment.startMinute + minMinutes, endOfDay); 988 segment.endMinute = Math.max(dayOffset + event.endTime, minEnd); 989 if (segment.endMinute > endOfDay) { 990 segment.endMinute = endOfDay; 991 } 992 993 segment.color = event.color; 994 segment.day = event.startDay; 995 segments.add(segment); 996 // increment the count for the correct color or add a new strand if we 997 // don't have that color yet 998 DNAStrand strand = getOrCreateStrand(strands, segment.color); 999 strand.count++; 1000 } 1001 1002 /** 1003 * Try to get a strand of the given color. Create it if it doesn't exist. 1004 */ 1005 private static DNAStrand getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color) { 1006 DNAStrand strand = strands.get(color); 1007 if (strand == null) { 1008 strand = new DNAStrand(); 1009 strand.color = color; 1010 strand.count = 0; 1011 strands.put(strand.color, strand); 1012 } 1013 return strand; 1014 } 1015 1016 /** 1017 * Sends an intent to launch the top level Calendar view. 1018 * 1019 * @param context 1020 */ 1021 public static void returnToCalendarHome(Context context) { 1022 Intent launchIntent = new Intent(context, AllInOneActivity.class); 1023 launchIntent.setAction(Intent.ACTION_DEFAULT); 1024 launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1025 launchIntent.putExtra(INTENT_KEY_HOME, true); 1026 context.startActivity(launchIntent); 1027 } 1028 1029 /** 1030 * This sets up a search view to use Calendar's search suggestions provider 1031 * and to allow refining the search. 1032 * 1033 * @param view The {@link SearchView} to set up 1034 * @param act The activity using the view 1035 */ 1036 public static void setUpSearchView(SearchView view, Activity act) { 1037 SearchManager searchManager = (SearchManager) act.getSystemService(Context.SEARCH_SERVICE); 1038 view.setSearchableInfo(searchManager.getSearchableInfo(act.getComponentName())); 1039 view.setQueryRefinementEnabled(true); 1040 } 1041 1042 /** 1043 * Given a context and a time in millis since unix epoch figures out the 1044 * correct week of the year for that time. 1045 * 1046 * @param millisSinceEpoch 1047 * @return 1048 */ 1049 public static int getWeekNumberFromTime(long millisSinceEpoch, Context context) { 1050 Time weekTime = new Time(getTimeZone(context, null)); 1051 weekTime.set(millisSinceEpoch); 1052 weekTime.normalize(true); 1053 int firstDayOfWeek = getFirstDayOfWeek(context); 1054 // if the date is on Saturday or Sunday and the start of the week 1055 // isn't Monday we may need to shift the date to be in the correct 1056 // week 1057 if (weekTime.weekDay == Time.SUNDAY 1058 && (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)) { 1059 weekTime.monthDay++; 1060 weekTime.normalize(true); 1061 } else if (weekTime.weekDay == Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) { 1062 weekTime.monthDay += 2; 1063 weekTime.normalize(true); 1064 } 1065 return weekTime.getWeekNumber(); 1066 } 1067 1068 /** 1069 * Formats a day of the week string. This is either just the name of the day 1070 * or a combination of yesterday/today/tomorrow and the day of the week. 1071 * 1072 * @param julianDay The julian day to get the string for 1073 * @param todayJulianDay The julian day for today's date 1074 * @param millis A utc millis since epoch time that falls on julian day 1075 * @param context The calling context, used to get the timezone and do the 1076 * formatting 1077 * @return 1078 */ 1079 public static String getDayOfWeekString(int julianDay, int todayJulianDay, long millis, 1080 Context context) { 1081 String tz = getTimeZone(context, null); 1082 int flags = DateUtils.FORMAT_SHOW_WEEKDAY; 1083 String dayViewText; 1084 if (julianDay == todayJulianDay) { 1085 dayViewText = context.getString(R.string.agenda_today, 1086 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1087 } else if (julianDay == todayJulianDay - 1) { 1088 dayViewText = context.getString(R.string.agenda_yesterday, 1089 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1090 } else if (julianDay == todayJulianDay + 1) { 1091 dayViewText = context.getString(R.string.agenda_tomorrow, 1092 mTZUtils.formatDateRange(context, millis, millis, flags).toString()); 1093 } else { 1094 dayViewText = mTZUtils.formatDateRange(context, millis, millis, flags).toString(); 1095 } 1096 dayViewText = dayViewText.toUpperCase(); 1097 return dayViewText; 1098 } 1099} 1100