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