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