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