Utils.java revision 72a9459e1f4cec02ad9e8dbdf824d66920b762ee
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.Calendar.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.database.Cursor; 29import android.database.MatrixCursor; 30import android.net.Uri; 31import android.os.Bundle; 32import android.text.TextUtils; 33import android.text.format.DateUtils; 34import android.text.format.Time; 35import android.util.CalendarUtils.TimeZoneUtils; 36import android.util.Log; 37 38import java.util.ArrayList; 39import java.util.Arrays; 40import java.util.Calendar; 41import java.util.Formatter; 42import java.util.Iterator; 43import java.util.List; 44import java.util.Map; 45 46public class Utils { 47 private static final boolean DEBUG = true; 48 private static final String TAG = "CalUtils"; 49 // Set to 0 until we have UI to perform undo 50 public static final long UNDO_DELAY = 0; 51 52 // For recurring events which instances of the series are being modified 53 public static final int MODIFY_UNINITIALIZED = 0; 54 public static final int MODIFY_SELECTED = 1; 55 public static final int MODIFY_ALL_FOLLOWING = 2; 56 public static final int MODIFY_ALL = 3; 57 58 // When the edit event view finishes it passes back the appropriate exit 59 // code. 60 public static final int DONE_REVERT = 1 << 0; 61 public static final int DONE_SAVE = 1 << 1; 62 public static final int DONE_DELETE = 1 << 2; 63 // And should re run with DONE_EXIT if it should also leave the view, just 64 // exiting is identical to reverting 65 public static final int DONE_EXIT = 1 << 0; 66 67 protected static final String OPEN_EMAIL_MARKER = " <"; 68 protected static final String CLOSE_EMAIL_MARKER = ">"; 69 70 public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW"; 71 public static final String INTENT_KEY_VIEW_TYPE = "VIEW"; 72 public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY"; 73 74 public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; 75 76 // The name of the shared preferences file. This name must be maintained for 77 // historical 78 // reasons, as it's what PreferenceManager assigned the first time the file 79 // was created. 80 private static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; 81 82 private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME); 83 private static boolean mAllowWeekForDetailView = false; 84 private static long mTardis = 0; 85 86 public static int getViewTypeFromIntentAndSharedPref(Activity activity) { 87 Intent intent = activity.getIntent(); 88 Bundle extras = intent.getExtras(); 89 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity); 90 91 if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) { 92 return ViewType.EDIT; 93 } 94 if (extras != null) { 95 if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) { 96 // This is the "detail" view which is either agenda or day view 97 return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW, 98 GeneralPreferences.DEFAULT_DETAILED_VIEW); 99 } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) { 100 // Not sure who uses this. This logic came from LaunchActivity 101 return ViewType.DAY; 102 } 103 } 104 105 // Default to the last view 106 return prefs.getInt( 107 GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW); 108 } 109 110 /** 111 * Writes a new home time zone to the db. Updates the home time zone in the 112 * db asynchronously and updates the local cache. Sending a time zone of 113 * **tbd** will cause it to be set to the device's time zone. null or empty 114 * tz will be ignored. 115 * 116 * @param context The calling activity 117 * @param timeZone The time zone to set Calendar to, or **tbd** 118 */ 119 public static void setTimeZone(Context context, String timeZone) { 120 mTZUtils.setTimeZone(context, timeZone); 121 } 122 123 /** 124 * Gets the time zone that Calendar should be displayed in This is a helper 125 * method to get the appropriate time zone for Calendar. If this is the 126 * first time this method has been called it will initiate an asynchronous 127 * query to verify that the data in preferences is correct. The callback 128 * supplied will only be called if this query returns a value other than 129 * what is stored in preferences and should cause the calling activity to 130 * refresh anything that depends on calling this method. 131 * 132 * @param context The calling activity 133 * @param callback The runnable that should execute if a query returns new 134 * values 135 * @return The string value representing the time zone Calendar should 136 * display 137 */ 138 public static String getTimeZone(Context context, Runnable callback) { 139 return mTZUtils.getTimeZone(context, callback); 140 } 141 142 /** 143 * Formats a date or a time range according to the local conventions. 144 * 145 * @param context the context is required only if the time is shown 146 * @param startMillis the start time in UTC milliseconds 147 * @param endMillis the end time in UTC milliseconds 148 * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter, 149 * long, long, int, String) formatDateRange} 150 * @return a string containing the formatted date/time range. 151 */ 152 public static String formatDateRange( 153 Context context, long startMillis, long endMillis, int flags) { 154 return mTZUtils.formatDateRange(context, startMillis, endMillis, flags); 155 } 156 157 public static String getSharedPreference(Context context, String key, String defaultValue) { 158 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 159 return prefs.getString(key, defaultValue); 160 } 161 162 public static int getSharedPreference(Context context, String key, int defaultValue) { 163 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 164 return prefs.getInt(key, defaultValue); 165 } 166 167 public static boolean getSharedPreference(Context context, String key, boolean defaultValue) { 168 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 169 return prefs.getBoolean(key, defaultValue); 170 } 171 172 /** 173 * Asynchronously sets the preference with the given key to the given value 174 * 175 * @param context the context to use to get preferences from 176 * @param key the key of the preference to set 177 * @param value the value to set 178 */ 179 public static void setSharedPreference(Context context, String key, String value) { 180 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 181 prefs.edit().putString(key, value).apply(); 182 } 183 184 protected static void tardis() { 185 mTardis = System.currentTimeMillis(); 186 } 187 188 protected static long getTardis() { 189 return mTardis; 190 } 191 192 static void setSharedPreference(Context context, String key, boolean value) { 193 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 194 SharedPreferences.Editor editor = prefs.edit(); 195 editor.putBoolean(key, value); 196 editor.apply(); 197 } 198 199 static void setSharedPreference(Context context, String key, int value) { 200 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 201 SharedPreferences.Editor editor = prefs.edit(); 202 editor.putInt(key, value); 203 editor.apply(); 204 } 205 206 /** 207 * Save default agenda/day/week/month view for next time 208 * 209 * @param context 210 * @param viewId {@link CalendarController.ViewType} 211 */ 212 static void setDefaultView(Context context, int viewId) { 213 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 214 SharedPreferences.Editor editor = prefs.edit(); 215 216 boolean validDetailView = false; 217 if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) { 218 validDetailView = true; 219 } else { 220 validDetailView = viewId == CalendarController.ViewType.AGENDA 221 || viewId == CalendarController.ViewType.DAY; 222 } 223 224 if (validDetailView) { 225 // Record the detail start view 226 editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId); 227 } 228 229 // Record the (new) start view 230 editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId); 231 editor.apply(); 232 } 233 234 public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { 235 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames()); 236 int numColumns = cursor.getColumnCount(); 237 String data[] = new String[numColumns]; 238 cursor.moveToPosition(-1); 239 while (cursor.moveToNext()) { 240 for (int i = 0; i < numColumns; i++) { 241 data[i] = cursor.getString(i); 242 } 243 newCursor.addRow(data); 244 } 245 return newCursor; 246 } 247 248 /** 249 * Compares two cursors to see if they contain the same data. 250 * 251 * @return Returns true of the cursors contain the same data and are not 252 * null, false otherwise 253 */ 254 public static boolean compareCursors(Cursor c1, Cursor c2) { 255 if (c1 == null || c2 == null) { 256 return false; 257 } 258 259 int numColumns = c1.getColumnCount(); 260 if (numColumns != c2.getColumnCount()) { 261 return false; 262 } 263 264 if (c1.getCount() != c2.getCount()) { 265 return false; 266 } 267 268 c1.moveToPosition(-1); 269 c2.moveToPosition(-1); 270 while (c1.moveToNext() && c2.moveToNext()) { 271 for (int i = 0; i < numColumns; i++) { 272 if (!TextUtils.equals(c1.getString(i), c2.getString(i))) { 273 return false; 274 } 275 } 276 } 277 278 return true; 279 } 280 281 /** 282 * If the given intent specifies a time (in milliseconds since the epoch), 283 * then that time is returned. Otherwise, the current time is returned. 284 */ 285 public static final long timeFromIntentInMillis(Intent intent) { 286 // If the time was specified, then use that. Otherwise, use the current 287 // time. 288 Uri data = intent.getData(); 289 long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1); 290 if (millis == -1 && data != null && data.isHierarchical()) { 291 List<String> path = data.getPathSegments(); 292 if (path.size() == 2 && path.get(0).equals("time")) { 293 try { 294 millis = Long.valueOf(data.getLastPathSegment()); 295 } catch (NumberFormatException e) { 296 Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " 297 + "found. Using current time."); 298 } 299 } 300 } 301 if (millis <= 0) { 302 millis = System.currentTimeMillis(); 303 } 304 return millis; 305 } 306 307 /** 308 * Formats the given Time object so that it gives the month and year (for 309 * example, "September 2007"). 310 * 311 * @param time the time to format 312 * @return the string containing the weekday and the date 313 */ 314 public static String formatMonthYear(Context context, Time time) { 315 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY 316 | DateUtils.FORMAT_SHOW_YEAR; 317 long millis = time.toMillis(true); 318 return formatDateRange(context, millis, millis, flags); 319 } 320 321 /** 322 * Returns a list joined together by the provided delimiter, for example, 323 * ["a", "b", "c"] could be joined into "a,b,c" 324 * 325 * @param things the things to join together 326 * @param delim the delimiter to use 327 * @return a string contained the things joined together 328 */ 329 public static String join(List<?> things, String delim) { 330 StringBuilder builder = new StringBuilder(); 331 boolean first = true; 332 for (Object thing : things) { 333 if (first) { 334 first = false; 335 } else { 336 builder.append(delim); 337 } 338 builder.append(thing.toString()); 339 } 340 return builder.toString(); 341 } 342 343 /** 344 * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970) 345 * adjusted for first day of week. 346 * 347 * This takes a julian day and the week start day and calculates which 348 * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting 349 * at 0. *Do not* use this to compute the ISO week number for the year. 350 * 351 * @param julianDay The julian day to calculate the week number for 352 * @param firstDayOfWeek Which week day is the first day of the week, 353 * see {@link Time#SUNDAY} 354 * @return Weeks since the epoch 355 */ 356 public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) { 357 int diff = Time.THURSDAY - firstDayOfWeek; 358 if (diff < 0) { 359 diff += 7; 360 } 361 int refDay = Time.EPOCH_JULIAN_DAY - diff; 362 return (julianDay - refDay) / 7; 363 } 364 365 /** 366 * Takes a number of weeks since the epoch and calculates the Julian day of 367 * the Monday for that week. 368 * 369 * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY} 370 * is considered week 0. It returns the Julian day for the Monday 371 * {@code week} weeks after the Monday of the week containing the epoch. 372 * 373 * @param week Number of weeks since the epoch 374 * @return The julian day for the Monday of the given week since the epoch 375 */ 376 public static int getJulianMondayFromWeeksSinceEpoch(int week) { 377 return MONDAY_BEFORE_JULIAN_EPOCH + week * 7; 378 } 379 380 /** 381 * Get first day of week as android.text.format.Time constant. 382 * 383 * @return the first day of week in android.text.format.Time 384 */ 385 public static int getFirstDayOfWeek(Context context) { 386 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 387 String pref = prefs.getString( 388 GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT); 389 390 int startDay; 391 if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) { 392 startDay = Calendar.getInstance().getFirstDayOfWeek(); 393 } else { 394 startDay = Integer.parseInt(pref); 395 } 396 397 if (startDay == Calendar.SATURDAY) { 398 return Time.SATURDAY; 399 } else if (startDay == Calendar.MONDAY) { 400 return Time.MONDAY; 401 } else { 402 return Time.SUNDAY; 403 } 404 } 405 406 /** 407 * @return true when week number should be shown. 408 */ 409 public static boolean getShowWeekNumber(Context context) { 410 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 411 return prefs.getBoolean( 412 GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM); 413 } 414 415 /** 416 * @return true when declined events should be hidden. 417 */ 418 public static boolean getHideDeclinedEvents(Context context) { 419 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 420 return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false); 421 } 422 423 public static int getDaysPerWeek(Context context) { 424 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 425 return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7); 426 } 427 428 /** 429 * Determine whether the column position is Saturday or not. 430 * 431 * @param column the column position 432 * @param firstDayOfWeek the first day of week in android.text.format.Time 433 * @return true if the column is Saturday position 434 */ 435 public static boolean isSaturday(int column, int firstDayOfWeek) { 436 return (firstDayOfWeek == Time.SUNDAY && column == 6) 437 || (firstDayOfWeek == Time.MONDAY && column == 5) 438 || (firstDayOfWeek == Time.SATURDAY && column == 0); 439 } 440 441 /** 442 * Determine whether the column position is Sunday or not. 443 * 444 * @param column the column position 445 * @param firstDayOfWeek the first day of week in android.text.format.Time 446 * @return true if the column is Sunday position 447 */ 448 public static boolean isSunday(int column, int firstDayOfWeek) { 449 return (firstDayOfWeek == Time.SUNDAY && column == 0) 450 || (firstDayOfWeek == Time.MONDAY && column == 6) 451 || (firstDayOfWeek == Time.SATURDAY && column == 1); 452 } 453 454 /** 455 * Convert given UTC time into current local time. This assumes it is for an 456 * allday event and will adjust the time to be on a midnight boundary. 457 * 458 * @param recycle Time object to recycle, otherwise null. 459 * @param utcTime Time to convert, in UTC. 460 * @param tz The time zone to convert this time to. 461 */ 462 public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) { 463 if (recycle == null) { 464 recycle = new Time(); 465 } 466 recycle.timezone = Time.TIMEZONE_UTC; 467 recycle.set(utcTime); 468 recycle.timezone = tz; 469 return recycle.normalize(true); 470 } 471 472 public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) { 473 if (recycle == null) { 474 recycle = new Time(); 475 } 476 recycle.timezone = tz; 477 recycle.set(localTime); 478 recycle.timezone = Time.TIMEZONE_UTC; 479 return recycle.normalize(true); 480 } 481 482 /** 483 * Scan through a cursor of calendars and check if names are duplicated. 484 * This travels a cursor containing calendar display names and fills in the 485 * provided map with whether or not each name is repeated. 486 * 487 * @param isDuplicateName The map to put the duplicate check results in. 488 * @param cursor The query of calendars to check 489 * @param nameIndex The column of the query that contains the display name 490 */ 491 public static void checkForDuplicateNames( 492 Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) { 493 isDuplicateName.clear(); 494 cursor.moveToPosition(-1); 495 while (cursor.moveToNext()) { 496 String displayName = cursor.getString(nameIndex); 497 // Set it to true if we've seen this name before, false otherwise 498 if (displayName != null) { 499 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName)); 500 } 501 } 502 } 503 504 /** 505 * Null-safe object comparison 506 * 507 * @param s1 508 * @param s2 509 * @return 510 */ 511 public static boolean equals(Object o1, Object o2) { 512 return o1 == null ? o2 == null : o1.equals(o2); 513 } 514 515 public static void setAllowWeekForDetailView(boolean allowWeekView) { 516 mAllowWeekForDetailView = allowWeekView; 517 } 518 519 public static boolean getAllowWeekForDetailView() { 520 return mAllowWeekForDetailView; 521 } 522 523 public static boolean isMultiPaneConfiguration (Context c) { 524 return (c.getResources().getConfiguration().screenLayout & 525 Configuration.SCREENLAYOUT_SIZE_XLARGE) != 0; 526 } 527 528 public static boolean getConfigBool(Context c, int key) { 529 return c.getResources().getBoolean(key); 530 } 531 532 533 /** 534 * Helper class for createBusyBitSegments method. 535 * Contains information about a segment of time (in pixels): 536 * 1. start and end of area to draw. 537 * 2. an indication if the segment represent a period of time with overlapping events (so that 538 * the drawing function can draw it in a different way) 539 */ 540 541 public static class BusyBitsSegment { 542 private int mStartPixel, mEndPixel; 543 private boolean mIsOverlapping; 544 545 public int getStart() { 546 return mStartPixel; 547 } 548 549 public void setStart(int start) { 550 this.mStartPixel = start; 551 } 552 553 public int getEnd() { 554 return mEndPixel; 555 } 556 557 public void setEnd(int end) { 558 this.mEndPixel = end; 559 } 560 561 public boolean isOverlapping() { 562 return mIsOverlapping; 563 } 564 565 public void setIsOverlapping(boolean isOverlapping) { 566 this.mIsOverlapping = isOverlapping; 567 } 568 569 public BusyBitsSegment(int s, int e, boolean isOverlapping) { 570 mStartPixel = s; 571 mEndPixel = e; 572 mIsOverlapping = isOverlapping; 573 } 574 575 @Override 576 public int hashCode() { 577 final int prime = 31; 578 int result = 1; 579 result = prime * result + mEndPixel; 580 result = prime * result + (mIsOverlapping ? 1231 : 1237); 581 result = prime * result + mStartPixel; 582 return result; 583 } 584 585 @Override 586 public boolean equals(Object obj) { 587 if (this == obj) { 588 return true; 589 } 590 if (obj == null) { 591 return false; 592 } 593 if (getClass() != obj.getClass()) { 594 return false; 595 } 596 BusyBitsSegment other = (BusyBitsSegment) obj; 597 if (mEndPixel != other.mEndPixel) { 598 return false; 599 } 600 if (mIsOverlapping != other.mIsOverlapping) { 601 return false; 602 } 603 if (mStartPixel != other.mStartPixel) { 604 return false; 605 } 606 return true; 607 } 608 } 609 610 611 /** 612 * This is a helper class for the createBusyBitSegments method 613 * The class contains information about a specific time that corresponds to either a start 614 * of an event or an end of an event (or both): 615 * 1. The time itself 616 * 2 .The number of event starts and ends (number of starts - number of ends) 617 */ 618 619 private static class BusyBitsEventTime { 620 621 public static final int EVENT_START = 1; 622 public static final int EVENT_END = -1; 623 624 public int mTime; // in minutes 625 // Number of events that start and end in this time (+1 for each start, 626 // -1 for each end) 627 public int mStartEndChanges; 628 629 public BusyBitsEventTime(int t, int c) { 630 mTime = t; 631 mStartEndChanges = c; 632 } 633 634 public void addStart() { 635 mStartEndChanges++; 636 } 637 638 public void addEnd() { 639 mStartEndChanges--; 640 } 641 } 642 643 /** 644 * Corrects segments that are overlapping. 645 * The function makes sure the last two segments do not overlap (meaning: 646 * the start pixel of the last segment is bigger than the end pixel of the 647 * "one before last" segment. 648 * The function assumes an overlap could be only 1 pixel. 649 * The function removes segments if necessary 650 * Segment size is from start to end (inclusive) 651 * 652 * @param segments a list of BusyBitsSegment 653 */ 654 655 public static void correctOverlappingSegment(ArrayList<BusyBitsSegment> segments) { 656 657 if (segments.size() <= 1) 658 return; 659 660 BusyBitsSegment seg1 = segments.get(segments.size() - 2); 661 BusyBitsSegment seg2 = segments.get(segments.size() - 1); 662 663 // If segments do not touch, no need to change 664 if (seg1.getEnd() < seg2.getStart()) { 665 return; 666 } 667 // If segments are identical , remove the last one 668 // This can only happen if both segments are the size of 1 pixel 669 if (seg1.equals(seg2)) { 670 segments.remove(segments.size() - 1); 671 return; 672 } 673 674 // Always prefer an overlapping segment to non-overlapping one 675 // If by cropping a segment it disappears (start > end), remove it (OK if start == end, 676 // because it is a 1 pixel segment) 677 if (seg1.isOverlapping()) { 678 seg2.setStart(seg2.getStart() + 1); 679 if (seg2.getStart() > seg2.getEnd()) { 680 segments.remove(segments.size() - 1); 681 } 682 return; 683 } else if (seg2.isOverlapping()) { 684 seg1.setEnd(seg1.getEnd() - 1); 685 if (seg1.getStart() > seg1.getEnd()) { 686 segments.remove(segments.size() - 2); 687 } 688 return; 689 } else { 690 // same kind of segments , just shorten the last one 691 seg2.setStart(seg2.getStart() + 1); 692 if (seg2.getStart() > seg2.getEnd()) { 693 segments.remove(segments.size() - 1); 694 } 695 } 696 } 697 698 699 /** 700 * Converts a list of events to a list of busy segments to draw. 701 * Assumes list is ordered according to start time of events 702 * The function processes events of a specific day only or part of that day 703 * 704 * The algorithm goes over all the events and creates an ordered list of times. 705 * Each item on the list corresponds to a time where an event started,ended or both. 706 * The item has a count of how many events started and how many events ended at that time. 707 * In the second stage, the algorithm go over the list of times and finds what change happened 708 * at each time. A change can be a switch between either of the free time/busy time/overlapping 709 * time. Every time a change happens, the algorithm creates a segment (in pixels) to be 710 * displayed with the relevant status (free/busy/overlapped). 711 * The algorithm also checks if segments overlap and truncates one of them if needed. 712 * 713 * @param startPixel defines the start of the draw area 714 * @param endPixel defines the end of the draw area 715 * @param startTimeMinute start time (in minutes) of the time frame to be displayed as busy bits 716 * @param endTimeMinute end time (in minutes) of the time frame to be displayed as busy bits 717 * @param julianDay the day of the time frame 718 * @param daysEvents - a list of events that took place in the specified day (including 719 * recurring events, events that start before the day and/or end after 720 * the day 721 722 * @return A list of segments to draw. Each segment includes the start and end 723 * pixels (inclusive). 724 */ 725 726 public static ArrayList<BusyBitsSegment> createBusyBitSegments(int startPixel, int endPixel, 727 int startTimeMinute, int endTimeMinute, int julianDay, 728 ArrayList<Event> daysEvents) { 729 730 // No events or illegal parameters , do nothing 731 732 if (daysEvents == null || daysEvents.size() == 0 || startPixel >= endPixel || 733 startTimeMinute < 0 || startTimeMinute > 24 * 60 || endTimeMinute < 0 || 734 endTimeMinute > 24 * 60 || startTimeMinute >= endTimeMinute) { 735 Log.wtf(TAG, "Illegal parameter in createBusyBitSegments, " + 736 "daysEvents = " + daysEvents + " , " + 737 "startPixel = " + startPixel + " , " + 738 "endPixel = " + endPixel + " , " + 739 "startTimeMinute = " + startTimeMinute + " , " + 740 "endTimeMinute = " + endTimeMinute + " , "); 741 return null; 742 } 743 744 // Go over all events and create a sorted list of times that include all 745 // the start and end times of all events. 746 747 ArrayList<BusyBitsEventTime> times = new ArrayList<BusyBitsEventTime>(); 748 749 Iterator<Event> iter = daysEvents.iterator(); 750 // Pointer to the search start in the "times" list. It prevents searching from the beginning 751 // of the list for each event. It is updated every time a new start time is inserted into 752 // the times list, since the events are time ordered, there is no point on searching before 753 // the last start time that was inserted 754 int initialSearchIndex = 0; 755 while (iter.hasNext()) { 756 Event event = iter.next(); 757 758 // Take into account the start and end day. This is important for events that span 759 // multiple days. 760 int eStart = event.startTime - (julianDay - event.startDay) * 24 * 60; 761 int eEnd = event.endTime + (event.endDay - julianDay) * 24 * 60; 762 763 // Skip all day events, and events that are not in the time frame 764 if (event.drawAsAllday() || eStart >= endTimeMinute || eEnd <= startTimeMinute) { 765 continue; 766 } 767 768 // If event spans before or after start or end time , truncate it 769 // because we care only about the time span that is passed to the function 770 if (eStart < startTimeMinute) { 771 eStart = startTimeMinute; 772 } 773 if (eEnd > endTimeMinute) { 774 eEnd = endTimeMinute; 775 } 776 // Skip events that are zero length 777 if (eStart == eEnd) { 778 continue; 779 } 780 781 // First event , just put it in the "times" list 782 if (times.size() == 0) { 783 BusyBitsEventTime es = new BusyBitsEventTime(eStart, BusyBitsEventTime.EVENT_START); 784 BusyBitsEventTime ee = new BusyBitsEventTime(eEnd, BusyBitsEventTime.EVENT_END); 785 times.add(es); 786 times.add(ee); 787 continue; 788 } 789 790 // Insert start and end times of event in "times" list. 791 // Loop through the "times" list and put the event start and ends times in the correct 792 // place. 793 boolean startInserted = false; 794 boolean endInserted = false; 795 int i = initialSearchIndex; // Skip times that are before the event time 796 // Two pointers for looping through the "times" list. Current item and next item. 797 int t1, t2; 798 do { 799 t1 = times.get(i).mTime; 800 t2 = times.get(i + 1).mTime; 801 if (!startInserted) { 802 // Start time equals an existing item in the "times" list, just update the 803 // starts count of the specific item 804 if (eStart == t1) { 805 times.get(i).addStart(); 806 initialSearchIndex = i; 807 startInserted = true; 808 } else if (eStart == t2) { 809 times.get(i + 1).addStart(); 810 initialSearchIndex = i + 1; 811 startInserted = true; 812 } else if (eStart > t1 && eStart < t2) { 813 // The start time is between the times of the current item and next item: 814 // insert a new start time in between the items. 815 BusyBitsEventTime e = new BusyBitsEventTime(eStart, 816 BusyBitsEventTime.EVENT_START); 817 times.add(i + 1, e); 818 initialSearchIndex = i + 1; 819 t2 = eStart; 820 startInserted = true; 821 } 822 } 823 if (!endInserted) { 824 // End time equals an existing item in the "times" list, just update the 825 // ends count of the specific item 826 if (eEnd == t1) { 827 times.get(i).addEnd(); 828 endInserted = true; 829 } else if (eEnd == t2) { 830 times.get(i + 1).addEnd(); 831 endInserted = true; 832 } else if (eEnd > t1 && eEnd < t2) { 833 // The end time is between the times of the current item and next item: 834 // insert a new end time in between the items. 835 BusyBitsEventTime e = new BusyBitsEventTime(eEnd, 836 BusyBitsEventTime.EVENT_END); 837 times.add(i + 1, e); 838 t2 = eEnd; 839 endInserted = true; 840 } 841 } 842 i++; 843 } while (!endInserted && i + 1 < times.size()); 844 845 // Deal with the last event if not inserted in the list 846 if (!startInserted) { 847 BusyBitsEventTime e = new BusyBitsEventTime(eStart, BusyBitsEventTime.EVENT_START); 848 times.add(e); 849 initialSearchIndex = times.size() - 1; 850 } 851 if (!endInserted) { 852 BusyBitsEventTime e = new BusyBitsEventTime(eEnd, BusyBitsEventTime.EVENT_END); 853 times.add(e); 854 } 855 } 856 857 // No events , return 858 if (times.size() == 0) { 859 return null; 860 } 861 862 // Loop through the created "times" list and find busy time segments and overlapping 863 // segments. In the loop, keep the status of time (free/busy/overlapping) and the time 864 // of when last status started. When there is a change in the status, create a segment with 865 // the previous status from the time of the last status started until the time of the 866 // current change. 867 // The loop keeps a count of how many events are overlapping. Zero means free time, one 868 // means a busy time and more than one means overlapping time. The count is updated by 869 // the number of starts and ends from the items in the "times" list. A change is a switch 870 // from free/busy/overlap status to a different one. 871 872 ArrayList<BusyBitsSegment> segments = new ArrayList<BusyBitsSegment>(); 873 874 int segmentStartTime = 0; // default start time 875 int overlappedCount = 0; // assume starting with free time 876 int pixelSize = endPixel - startPixel; 877 int timeFrame = endTimeMinute - startTimeMinute; 878 879 Iterator<BusyBitsEventTime> tIter = times.iterator(); 880 while (tIter.hasNext()) { 881 BusyBitsEventTime t = tIter.next(); 882 // Get the new count of overlapping events 883 int newCount = overlappedCount + t.mStartEndChanges; 884 885 // No need for a new segment because the free/busy/overlapping status didn't change 886 if (overlappedCount == newCount || (overlappedCount >= 2 && newCount >= 2)) { 887 overlappedCount = newCount; 888 continue; 889 } 890 if (overlappedCount == 0 && newCount == 1) { 891 // A busy time started - start a new segment 892 if (segmentStartTime != 0) { 893 // Unknown status, blow up 894 Log.wtf(TAG, "Unknown state in createBusyBitSegments, segmentStartTime = " + 895 segmentStartTime + ", nolc = " + newCount); 896 } 897 segmentStartTime = t.mTime; 898 } else if (overlappedCount == 0 && newCount >= 2) { 899 // An overlapping time started - start a new segment 900 if (segmentStartTime != 0) { 901 // Unknown status, blow up 902 Log.wtf(TAG, "Unknown state in createBusyBitSegments, segmentStartTime = " + 903 segmentStartTime + ", nolc = " + newCount); 904 } 905 segmentStartTime = t.mTime; 906 } else if (overlappedCount == 1 && newCount >= 2) { 907 // A busy time ended and overlapping segment started, 908 // Save busy segment and start overlapping segment 909 BusyBitsSegment s = new BusyBitsSegment( 910 (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel, 911 (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, false); 912 segments.add(s); 913 correctOverlappingSegment(segments); 914 segmentStartTime = t.mTime; 915 } else if (overlappedCount >= 2 && newCount == 1) { 916 // An overlapping time ended and busy segment started. 917 // Save overlapping segment and start busy segment 918 BusyBitsSegment s = new BusyBitsSegment( 919 (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel, 920 (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, true); 921 segments.add(s); 922 correctOverlappingSegment(segments); 923 segmentStartTime = t.mTime; 924 } else if (overlappedCount >= 2 && newCount == 0) { 925 // An overlapping segment ended, and a free time segment started 926 // Save overlapping segment 927 BusyBitsSegment s = new BusyBitsSegment( 928 (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel, 929 (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, true); 930 segments.add(s); 931 correctOverlappingSegment(segments); 932 segmentStartTime = 0; 933 } else if (overlappedCount == 1 && newCount == 0) { 934 // A busy segment ended, and a free time segment started, save busy segment 935 BusyBitsSegment s = new BusyBitsSegment( 936 (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel, 937 (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, false); 938 segments.add(s); 939 correctOverlappingSegment(segments); 940 segmentStartTime = 0; 941 } else { 942 // Unknown status, blow up 943 Log.wtf(TAG, "Unknown state in createBusyBitSegments: time = " + t.mTime + 944 " , olc = " + overlappedCount + " nolc = " + newCount); 945 } 946 overlappedCount = newCount; // Update count 947 } 948 return segments; 949 } 950} 951