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