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