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