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