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 17 package com.android.calendar; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.content.res.Resources; 22 import android.database.Cursor; 23 import android.os.Debug; 24 import android.preference.PreferenceManager; 25 import android.provider.Calendar.Attendees; 26 import android.provider.Calendar.Events; 27 import android.provider.Calendar.Instances; 28 import android.text.TextUtils; 29 import android.text.format.DateUtils; 30 import android.text.format.Time; 31 import android.util.Log; 32 33 import java.util.ArrayList; 34 import java.util.Iterator; 35 import java.util.concurrent.atomic.AtomicInteger; 36 37 // TODO: should Event be Parcelable so it can be passed via Intents? 38 public 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 = CalendarPreferenceActivity.getSharedPreferences(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 private 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. 436 // An event on the active list becomes inactive when its end time + margin time is less 437 // than or equal to the current event's start time. For more information about 438 // the margin time, see the comment in EVENT_OVERWRAP_MARGIN_TIME. 439 Iterator<Event> iter = activeList.iterator(); 440 while (iter.hasNext()) { 441 Event active = iter.next(); 442 final long duration = Math.max(active.getEndMillis() - active.getStartMillis(), 443 CalendarView.EVENT_OVERWRAP_MARGIN_TIME); 444 if ((active.getStartMillis() + duration) <= start) { 445 if (false && event.allDay) { 446 Event e = active; 447 Log.i("Cal", " removing: start,end day: " + e.startDay + "," + e.endDay 448 + " start,end time: " + e.startTime + "," + e.endTime 449 + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis() 450 + " " + e.title); 451 } 452 colMask &= ~(1L << active.getColumn()); 453 iter.remove(); 454 } 455 } 456 457 // If the active list is empty, then reset the max columns, clear 458 // the column bit mask, and empty the groupList. 459 if (activeList.isEmpty()) { 460 for (Event ev : groupList) { 461 ev.setMaxColumns(maxCols); 462 } 463 maxCols = 0; 464 colMask = 0; 465 groupList.clear(); 466 } 467 468 // Find the first empty column. Empty columns are represented by 469 // zero bits in the column mask "colMask". 470 int col = findFirstZeroBit(colMask); 471 if (col == 64) 472 col = 63; 473 colMask |= (1L << col); 474 event.setColumn(col); 475 activeList.add(event); 476 groupList.add(event); 477 int len = activeList.size(); 478 if (maxCols < len) 479 maxCols = len; 480 } 481 for (Event ev : groupList) { 482 ev.setMaxColumns(maxCols); 483 } 484 } 485 486 public static int findFirstZeroBit(long val) { 487 for (int ii = 0; ii < 64; ++ii) { 488 if ((val & (1L << ii)) == 0) 489 return ii; 490 } 491 return 64; 492 } 493 494 /** 495 * Returns a darker version of the given color. It does this by dividing 496 * each of the red, green, and blue components by 2. The alpha value is 497 * preserved. 498 */ 499 private static final int getDarkerColor(int color) { 500 int darker = (color >> 1) & 0x007f7f7f; 501 int alpha = color & 0xff000000; 502 return alpha | darker; 503 } 504 505 // For testing. This method can be removed at any time. 506 private static ArrayList<Event> createTestEventList() { 507 ArrayList<Event> evList = new ArrayList<Event>(); 508 createTestEvent(evList, 1, 5, 10); 509 createTestEvent(evList, 2, 5, 10); 510 createTestEvent(evList, 3, 15, 20); 511 createTestEvent(evList, 4, 20, 25); 512 createTestEvent(evList, 5, 30, 70); 513 createTestEvent(evList, 6, 32, 40); 514 createTestEvent(evList, 7, 32, 40); 515 createTestEvent(evList, 8, 34, 38); 516 createTestEvent(evList, 9, 34, 38); 517 createTestEvent(evList, 10, 42, 50); 518 createTestEvent(evList, 11, 45, 60); 519 createTestEvent(evList, 12, 55, 90); 520 createTestEvent(evList, 13, 65, 75); 521 522 createTestEvent(evList, 21, 105, 130); 523 createTestEvent(evList, 22, 110, 120); 524 createTestEvent(evList, 23, 115, 130); 525 createTestEvent(evList, 24, 125, 140); 526 createTestEvent(evList, 25, 127, 135); 527 528 createTestEvent(evList, 31, 150, 160); 529 createTestEvent(evList, 32, 152, 162); 530 createTestEvent(evList, 33, 153, 163); 531 createTestEvent(evList, 34, 155, 170); 532 createTestEvent(evList, 35, 158, 175); 533 createTestEvent(evList, 36, 165, 180); 534 535 return evList; 536 } 537 538 // For testing. This method can be removed at any time. 539 private static Event createTestEvent(ArrayList<Event> evList, int id, 540 int startMinute, int endMinute) { 541 Event ev = new Event(); 542 ev.title = "ev" + id; 543 ev.startDay = 1; 544 ev.endDay = 1; 545 ev.setStartMillis(startMinute); 546 ev.setEndMillis(endMinute); 547 evList.add(ev); 548 return ev; 549 } 550 551 public final void dump() { 552 Log.e("Cal", "+-----------------------------------------+"); 553 Log.e("Cal", "+ id = " + id); 554 Log.e("Cal", "+ color = " + color); 555 Log.e("Cal", "+ title = " + title); 556 Log.e("Cal", "+ location = " + location); 557 Log.e("Cal", "+ allDay = " + allDay); 558 Log.e("Cal", "+ startDay = " + startDay); 559 Log.e("Cal", "+ endDay = " + endDay); 560 Log.e("Cal", "+ startTime = " + startTime); 561 Log.e("Cal", "+ endTime = " + endTime); 562 Log.e("Cal", "+ organizer = " + organizer); 563 Log.e("Cal", "+ guestwrt = " + guestsCanModify); 564 } 565 566 public final boolean intersects(int julianDay, int startMinute, 567 int endMinute) { 568 if (endDay < julianDay) { 569 return false; 570 } 571 572 if (startDay > julianDay) { 573 return false; 574 } 575 576 if (endDay == julianDay) { 577 if (endTime < startMinute) { 578 return false; 579 } 580 // An event that ends at the start minute should not be considered 581 // as intersecting the given time span, but don't exclude 582 // zero-length (or very short) events. 583 if (endTime == startMinute 584 && (startTime != endTime || startDay != endDay)) { 585 return false; 586 } 587 } 588 589 if (startDay == julianDay && startTime > endMinute) { 590 return false; 591 } 592 593 return true; 594 } 595 596 /** 597 * Returns the event title and location separated by a comma. If the 598 * location is already part of the title (at the end of the title), then 599 * just the title is returned. 600 * 601 * @return the event title and location as a String 602 */ 603 public String getTitleAndLocation() { 604 String text = title.toString(); 605 606 // Append the location to the title, unless the title ends with the 607 // location (for example, "meeting in building 42" ends with the 608 // location). 609 if (location != null) { 610 String locationString = location.toString(); 611 if (!text.endsWith(locationString)) { 612 text += ", " + locationString; 613 } 614 } 615 return text; 616 } 617 618 public void setColumn(int column) { 619 mColumn = column; 620 } 621 622 public int getColumn() { 623 return mColumn; 624 } 625 626 public void setMaxColumns(int maxColumns) { 627 mMaxColumns = maxColumns; 628 } 629 630 public int getMaxColumns() { 631 return mMaxColumns; 632 } 633 634 public void setStartMillis(long startMillis) { 635 this.startMillis = startMillis; 636 } 637 638 public long getStartMillis() { 639 return startMillis; 640 } 641 642 public void setEndMillis(long endMillis) { 643 this.endMillis = endMillis; 644 } 645 646 public long getEndMillis() { 647 return endMillis; 648 } 649 } 650