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