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