Event.java revision 981874e61ecf29a96a77601a3172b2503b6537ee
1/* 2 * Copyright (C) 2007 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 android.content.Context; 20import android.content.SharedPreferences; 21import android.content.res.Resources; 22import android.database.Cursor; 23import android.os.Debug; 24import android.provider.Calendar.Attendees; 25import android.provider.Calendar.Events; 26import android.provider.Calendar.Instances; 27import android.text.TextUtils; 28import android.text.format.DateUtils; 29import android.text.format.Time; 30import android.util.Log; 31 32import java.util.ArrayList; 33import java.util.Iterator; 34import java.util.concurrent.atomic.AtomicInteger; 35 36// TODO: should Event be Parcelable so it can be passed via Intents? 37public class Event implements Comparable<Event>, Cloneable { 38 39 private static final String TAG = "CalEvent"; 40 private static final boolean PROFILE = false; 41 42 // The projection to use when querying instances to build a list of events 43 public static final String[] EVENT_PROJECTION = new String[] { 44 Instances.TITLE, // 0 45 Instances.EVENT_LOCATION, // 1 46 Instances.ALL_DAY, // 2 47 Instances.COLOR, // 3 48 Instances.EVENT_TIMEZONE, // 4 49 Instances.EVENT_ID, // 5 50 Instances.BEGIN, // 6 51 Instances.END, // 7 52 Instances._ID, // 8 53 Instances.START_DAY, // 9 54 Instances.END_DAY, // 10 55 Instances.START_MINUTE, // 11 56 Instances.END_MINUTE, // 12 57 Instances.HAS_ALARM, // 13 58 Instances.RRULE, // 14 59 Instances.RDATE, // 15 60 Instances.SELF_ATTENDEE_STATUS, // 16 61 Events.ORGANIZER, // 17 62 Events.GUESTS_CAN_MODIFY, // 18 63 }; 64 65 // The indices for the projection array above. 66 private static final int PROJECTION_TITLE_INDEX = 0; 67 private static final int PROJECTION_LOCATION_INDEX = 1; 68 private static final int PROJECTION_ALL_DAY_INDEX = 2; 69 private static final int PROJECTION_COLOR_INDEX = 3; 70 private static final int PROJECTION_TIMEZONE_INDEX = 4; 71 private static final int PROJECTION_EVENT_ID_INDEX = 5; 72 private static final int PROJECTION_BEGIN_INDEX = 6; 73 private static final int PROJECTION_END_INDEX = 7; 74 private static final int PROJECTION_START_DAY_INDEX = 9; 75 private static final int PROJECTION_END_DAY_INDEX = 10; 76 private static final int PROJECTION_START_MINUTE_INDEX = 11; 77 private static final int PROJECTION_END_MINUTE_INDEX = 12; 78 private static final int PROJECTION_HAS_ALARM_INDEX = 13; 79 private static final int PROJECTION_RRULE_INDEX = 14; 80 private static final int PROJECTION_RDATE_INDEX = 15; 81 private static final int PROJECTION_SELF_ATTENDEE_STATUS_INDEX = 16; 82 private static final int PROJECTION_ORGANIZER_INDEX = 17; 83 private static final int PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX = 18; 84 85 public long id; 86 public int color; 87 public CharSequence title; 88 public CharSequence location; 89 public boolean allDay; 90 public String organizer; 91 public boolean guestsCanModify; 92 93 public int startDay; // start Julian day 94 public int endDay; // end Julian day 95 public int startTime; // Start and end time are in minutes since midnight 96 public int endTime; 97 98 public long startMillis; // UTC milliseconds since the epoch 99 public long endMillis; // UTC milliseconds since the epoch 100 private int mColumn; 101 private int mMaxColumns; 102 103 public boolean hasAlarm; 104 public boolean isRepeating; 105 106 public int selfAttendeeStatus; 107 108 // The coordinates of the event rectangle drawn on the screen. 109 public float left; 110 public float right; 111 public float top; 112 public float bottom; 113 114 // These 4 fields are used for navigating among events within the selected 115 // hour in the Day and Week view. 116 public Event nextRight; 117 public Event nextLeft; 118 public Event nextUp; 119 public Event nextDown; 120 121 @Override 122 public final Object clone() throws CloneNotSupportedException { 123 super.clone(); 124 Event e = new Event(); 125 126 e.title = title; 127 e.color = color; 128 e.location = location; 129 e.allDay = allDay; 130 e.startDay = startDay; 131 e.endDay = endDay; 132 e.startTime = startTime; 133 e.endTime = endTime; 134 e.startMillis = startMillis; 135 e.endMillis = endMillis; 136 e.hasAlarm = hasAlarm; 137 e.isRepeating = isRepeating; 138 e.selfAttendeeStatus = selfAttendeeStatus; 139 e.organizer = organizer; 140 e.guestsCanModify = guestsCanModify; 141 142 return e; 143 } 144 145 public final void copyTo(Event dest) { 146 dest.id = id; 147 dest.title = title; 148 dest.color = color; 149 dest.location = location; 150 dest.allDay = allDay; 151 dest.startDay = startDay; 152 dest.endDay = endDay; 153 dest.startTime = startTime; 154 dest.endTime = endTime; 155 dest.startMillis = startMillis; 156 dest.endMillis = endMillis; 157 dest.hasAlarm = hasAlarm; 158 dest.isRepeating = isRepeating; 159 dest.selfAttendeeStatus = selfAttendeeStatus; 160 dest.organizer = organizer; 161 dest.guestsCanModify = guestsCanModify; 162 } 163 164 public static final Event newInstance() { 165 Event e = new Event(); 166 167 e.id = 0; 168 e.title = null; 169 e.color = 0; 170 e.location = null; 171 e.allDay = false; 172 e.startDay = 0; 173 e.endDay = 0; 174 e.startTime = 0; 175 e.endTime = 0; 176 e.startMillis = 0; 177 e.endMillis = 0; 178 e.hasAlarm = false; 179 e.isRepeating = false; 180 e.selfAttendeeStatus = Attendees.ATTENDEE_STATUS_NONE; 181 182 return e; 183 } 184 185 /** 186 * Compares this event to the given event. This is just used for checking 187 * if two events differ. It's not used for sorting anymore. 188 */ 189 public final int compareTo(Event obj) { 190 // The earlier start day and time comes first 191 if (startDay < obj.startDay) return -1; 192 if (startDay > obj.startDay) return 1; 193 if (startTime < obj.startTime) return -1; 194 if (startTime > obj.startTime) return 1; 195 196 // The later end time comes first (in order to put long strips on 197 // the left). 198 if (endDay < obj.endDay) return 1; 199 if (endDay > obj.endDay) return -1; 200 if (endTime < obj.endTime) return 1; 201 if (endTime > obj.endTime) return -1; 202 203 // Sort all-day events before normal events. 204 if (allDay && !obj.allDay) return -1; 205 if (!allDay && obj.allDay) return 1; 206 207 if (guestsCanModify && !obj.guestsCanModify) return -1; 208 if (!guestsCanModify && obj.guestsCanModify) return 1; 209 210 // If two events have the same time range, then sort them in 211 // alphabetical order based on their titles. 212 int cmp = compareStrings(title, obj.title); 213 if (cmp != 0) { 214 return cmp; 215 } 216 217 // If the titles are the same then compare the other fields 218 // so that we can use this function to check for differences 219 // between events. 220 cmp = compareStrings(location, obj.location); 221 if (cmp != 0) { 222 return cmp; 223 } 224 225 cmp = compareStrings(organizer, obj.organizer); 226 if (cmp != 0) { 227 return cmp; 228 } 229 return 0; 230 } 231 232 /** 233 * Compare string a with string b, but if either string is null, 234 * then treat it (the null) as if it were the empty string (""). 235 * 236 * @param a the first string 237 * @param b the second string 238 * @return the result of comparing a with b after replacing null 239 * strings with "". 240 */ 241 private int compareStrings(CharSequence a, CharSequence b) { 242 String aStr, bStr; 243 if (a != null) { 244 aStr = a.toString(); 245 } else { 246 aStr = ""; 247 } 248 if (b != null) { 249 bStr = b.toString(); 250 } else { 251 bStr = ""; 252 } 253 return aStr.compareTo(bStr); 254 } 255 256 /** 257 * Loads <i>days</i> days worth of instances starting at <i>start</i>. 258 */ 259 public static void loadEvents(Context context, ArrayList<Event> events, 260 long start, int days, int requestId, AtomicInteger sequenceNumber) { 261 262 if (PROFILE) { 263 Debug.startMethodTracing("loadEvents"); 264 } 265 266 Cursor c = null; 267 268 events.clear(); 269 try { 270 Time local = new Time(); 271 int count; 272 273 local.set(start); 274 int startDay = Time.getJulianDay(start, local.gmtoff); 275 int endDay = startDay + days; 276 277 local.monthDay += days; 278 long end = local.normalize(true /* ignore isDst */); 279 280 // Widen the time range that we query by one day on each end 281 // so that we can catch all-day events. All-day events are 282 // stored starting at midnight in UTC but should be included 283 // in the list of events starting at midnight local time. 284 // This may fetch more events than we actually want, so we 285 // filter them out below. 286 // 287 // The sort order is: events with an earlier start time occur 288 // first and if the start times are the same, then events with 289 // a later end time occur first. The later end time is ordered 290 // first so that long rectangles in the calendar views appear on 291 // the left side. If the start and end times of two events are 292 // the same then we sort alphabetically on the title. This isn't 293 // required for correctness, it just adds a nice touch. 294 295 String orderBy = Instances.SORT_CALENDAR_VIEW; 296 297 // Respect the preference to show/hide declined events 298 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 299 boolean hideDeclined = prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, 300 false); 301 302 String where = null; 303 if (hideDeclined) { 304 where = Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED; 305 } 306 307 c = Instances.query(context.getContentResolver(), EVENT_PROJECTION, 308 start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, where, orderBy); 309 310 // Check if we should return early because there are more recent 311 // load requests waiting. 312 if (requestId != sequenceNumber.get()) { 313 return; 314 } 315 316 buildEventsFromCursor(events, c, context, startDay, endDay); 317 318 } finally { 319 if (c != null) { 320 c.close(); 321 } 322 if (PROFILE) { 323 Debug.stopMethodTracing(); 324 } 325 } 326 } 327 328 public static void buildEventsFromCursor(ArrayList<Event> events, Cursor c, Context context, 329 int startDay, int endDay) { 330 if (c == null || events == null) { 331 Log.e(TAG, "buildEventsFromCursor: null cursor or null events list!"); 332 return; 333 } 334 335 int count = c.getCount(); 336 337 if (count == 0) { 338 return; 339 } 340 341 Resources res = context.getResources(); 342 while (c.moveToNext()) { 343 Event e = new Event(); 344 345 e.id = c.getLong(PROJECTION_EVENT_ID_INDEX); 346 e.title = c.getString(PROJECTION_TITLE_INDEX); 347 e.location = c.getString(PROJECTION_LOCATION_INDEX); 348 e.allDay = c.getInt(PROJECTION_ALL_DAY_INDEX) != 0; 349 e.organizer = c.getString(PROJECTION_ORGANIZER_INDEX); 350 e.guestsCanModify = c.getInt(PROJECTION_GUESTS_CAN_INVITE_OTHERS_INDEX) != 0; 351 352 String timezone = c.getString(PROJECTION_TIMEZONE_INDEX); 353 354 if (e.title == null || e.title.length() == 0) { 355 e.title = res.getString(R.string.no_title_label); 356 } 357 358 if (!c.isNull(PROJECTION_COLOR_INDEX)) { 359 // Read the color from the database 360 e.color = c.getInt(PROJECTION_COLOR_INDEX); 361 } else { 362 e.color = res.getColor(R.color.event_center); 363 } 364 365 long eStart = c.getLong(PROJECTION_BEGIN_INDEX); 366 long eEnd = c.getLong(PROJECTION_END_INDEX); 367 368 e.startMillis = eStart; 369 e.startTime = c.getInt(PROJECTION_START_MINUTE_INDEX); 370 e.startDay = c.getInt(PROJECTION_START_DAY_INDEX); 371 372 e.endMillis = eEnd; 373 e.endTime = c.getInt(PROJECTION_END_MINUTE_INDEX); 374 e.endDay = c.getInt(PROJECTION_END_DAY_INDEX); 375 376 if (e.startDay > endDay || e.endDay < startDay) { 377 continue; 378 } 379 380 e.hasAlarm = c.getInt(PROJECTION_HAS_ALARM_INDEX) != 0; 381 382 // Check if this is a repeating event 383 String rrule = c.getString(PROJECTION_RRULE_INDEX); 384 String rdate = c.getString(PROJECTION_RDATE_INDEX); 385 if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) { 386 e.isRepeating = true; 387 } else { 388 e.isRepeating = false; 389 } 390 391 e.selfAttendeeStatus = c.getInt(PROJECTION_SELF_ATTENDEE_STATUS_INDEX); 392 393 events.add(e); 394 } 395 } 396 397 /** 398 * Computes a position for each event. Each event is displayed 399 * as a non-overlapping rectangle. For normal events, these rectangles 400 * are displayed in separate columns in the week view and day view. For 401 * all-day events, these rectangles are displayed in separate rows along 402 * the top. In both cases, each event is assigned two numbers: N, and 403 * Max, that specify that this event is the Nth event of Max number of 404 * events that are displayed in a group. The width and position of each 405 * rectangle depend on the maximum number of rectangles that occur at 406 * the same time. 407 * 408 * @param eventsList the list of events, sorted into increasing time order 409 * @param minimumDurationMillis minimum duration acceptable as cell height of each event 410 * rectangle in millisecond. Should be 0 when it is not determined. 411 */ 412 /* package */ static void computePositions(ArrayList<Event> eventsList, 413 long minimumDurationMillis) { 414 if (eventsList == null) { 415 return; 416 } 417 418 // Compute the column positions separately for the all-day events 419 doComputePositions(eventsList, minimumDurationMillis, false); 420 doComputePositions(eventsList, minimumDurationMillis, true); 421 } 422 423 private static void doComputePositions(ArrayList<Event> eventsList, 424 long minimumDurationMillis, boolean doAllDayEvents) { 425 final ArrayList<Event> activeList = new ArrayList<Event>(); 426 final ArrayList<Event> groupList = new ArrayList<Event>(); 427 428 if (minimumDurationMillis < 0) { 429 minimumDurationMillis = 0; 430 } 431 432 long colMask = 0; 433 int maxCols = 0; 434 for (Event event : eventsList) { 435 // Process all-day events separately 436 if (event.allDay != doAllDayEvents) 437 continue; 438 439 long start = event.getStartMillis(); 440 // Remove the inactive events. An event on the active list 441 // becomes inactive when its end time is less than or equal to 442 // the current event's start time. 443 Iterator<Event> iter = activeList.iterator(); 444 while (iter.hasNext()) { 445 final Event active = iter.next(); 446 447 final long duration = 448 Math.max(active.getEndMillis() - active.getStartMillis(), 449 minimumDurationMillis); 450 if ((active.getStartMillis() + duration) <= start) { 451 colMask &= ~(1L << active.getColumn()); 452 iter.remove(); 453 } 454 } 455 456 // If the active list is empty, then reset the max columns, clear 457 // the column bit mask, and empty the groupList. 458 if (activeList.isEmpty()) { 459 for (Event ev : groupList) { 460 ev.setMaxColumns(maxCols); 461 } 462 maxCols = 0; 463 colMask = 0; 464 groupList.clear(); 465 } 466 467 // Find the first empty column. Empty columns are represented by 468 // zero bits in the column mask "colMask". 469 int col = findFirstZeroBit(colMask); 470 if (col == 64) 471 col = 63; 472 colMask |= (1L << col); 473 event.setColumn(col); 474 activeList.add(event); 475 groupList.add(event); 476 int len = activeList.size(); 477 if (maxCols < len) 478 maxCols = len; 479 } 480 for (Event ev : groupList) { 481 ev.setMaxColumns(maxCols); 482 } 483 } 484 485 public static int findFirstZeroBit(long val) { 486 for (int ii = 0; ii < 64; ++ii) { 487 if ((val & (1L << ii)) == 0) 488 return ii; 489 } 490 return 64; 491 } 492 493 /** 494 * Returns a darker version of the given color. It does this by dividing 495 * each of the red, green, and blue components by 2. The alpha value is 496 * preserved. 497 */ 498 private static final int getDarkerColor(int color) { 499 int darker = (color >> 1) & 0x007f7f7f; 500 int alpha = color & 0xff000000; 501 return alpha | darker; 502 } 503 504 // For testing. This method can be removed at any time. 505 private static ArrayList<Event> createTestEventList() { 506 ArrayList<Event> evList = new ArrayList<Event>(); 507 createTestEvent(evList, 1, 5, 10); 508 createTestEvent(evList, 2, 5, 10); 509 createTestEvent(evList, 3, 15, 20); 510 createTestEvent(evList, 4, 20, 25); 511 createTestEvent(evList, 5, 30, 70); 512 createTestEvent(evList, 6, 32, 40); 513 createTestEvent(evList, 7, 32, 40); 514 createTestEvent(evList, 8, 34, 38); 515 createTestEvent(evList, 9, 34, 38); 516 createTestEvent(evList, 10, 42, 50); 517 createTestEvent(evList, 11, 45, 60); 518 createTestEvent(evList, 12, 55, 90); 519 createTestEvent(evList, 13, 65, 75); 520 521 createTestEvent(evList, 21, 105, 130); 522 createTestEvent(evList, 22, 110, 120); 523 createTestEvent(evList, 23, 115, 130); 524 createTestEvent(evList, 24, 125, 140); 525 createTestEvent(evList, 25, 127, 135); 526 527 createTestEvent(evList, 31, 150, 160); 528 createTestEvent(evList, 32, 152, 162); 529 createTestEvent(evList, 33, 153, 163); 530 createTestEvent(evList, 34, 155, 170); 531 createTestEvent(evList, 35, 158, 175); 532 createTestEvent(evList, 36, 165, 180); 533 534 return evList; 535 } 536 537 // For testing. This method can be removed at any time. 538 private static Event createTestEvent(ArrayList<Event> evList, int id, 539 int startMinute, int endMinute) { 540 Event ev = new Event(); 541 ev.title = "ev" + id; 542 ev.startDay = 1; 543 ev.endDay = 1; 544 ev.setStartMillis(startMinute); 545 ev.setEndMillis(endMinute); 546 evList.add(ev); 547 return ev; 548 } 549 550 public final void dump() { 551 Log.e("Cal", "+-----------------------------------------+"); 552 Log.e("Cal", "+ id = " + id); 553 Log.e("Cal", "+ color = " + color); 554 Log.e("Cal", "+ title = " + title); 555 Log.e("Cal", "+ location = " + location); 556 Log.e("Cal", "+ allDay = " + allDay); 557 Log.e("Cal", "+ startDay = " + startDay); 558 Log.e("Cal", "+ endDay = " + endDay); 559 Log.e("Cal", "+ startTime = " + startTime); 560 Log.e("Cal", "+ endTime = " + endTime); 561 Log.e("Cal", "+ organizer = " + organizer); 562 Log.e("Cal", "+ guestwrt = " + guestsCanModify); 563 } 564 565 public final boolean intersects(int julianDay, int startMinute, 566 int endMinute) { 567 if (endDay < julianDay) { 568 return false; 569 } 570 571 if (startDay > julianDay) { 572 return false; 573 } 574 575 if (endDay == julianDay) { 576 if (endTime < startMinute) { 577 return false; 578 } 579 // An event that ends at the start minute should not be considered 580 // as intersecting the given time span, but don't exclude 581 // zero-length (or very short) events. 582 if (endTime == startMinute 583 && (startTime != endTime || startDay != endDay)) { 584 return false; 585 } 586 } 587 588 if (startDay == julianDay && startTime > endMinute) { 589 return false; 590 } 591 592 return true; 593 } 594 595 /** 596 * Returns the event title and location separated by a comma. If the 597 * location is already part of the title (at the end of the title), then 598 * just the title is returned. 599 * 600 * @return the event title and location as a String 601 */ 602 public String getTitleAndLocation() { 603 String text = title.toString(); 604 605 // Append the location to the title, unless the title ends with the 606 // location (for example, "meeting in building 42" ends with the 607 // location). 608 if (location != null) { 609 String locationString = location.toString(); 610 if (!text.endsWith(locationString)) { 611 text += ", " + locationString; 612 } 613 } 614 return text; 615 } 616 617 public void setColumn(int column) { 618 mColumn = column; 619 } 620 621 public int getColumn() { 622 return mColumn; 623 } 624 625 public void setMaxColumns(int maxColumns) { 626 mMaxColumns = maxColumns; 627 } 628 629 public int getMaxColumns() { 630 return mMaxColumns; 631 } 632 633 public void setStartMillis(long startMillis) { 634 this.startMillis = startMillis; 635 } 636 637 public long getStartMillis() { 638 return startMillis; 639 } 640 641 public void setEndMillis(long endMillis) { 642 this.endMillis = endMillis; 643 } 644 645 public long getEndMillis() { 646 return endMillis; 647 } 648} 649