Initial Contribution
[android/platform/packages/apps/Calendar.git] / src / com / android / calendar / Event.java
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.pim.DateUtils;
25 import android.pim.Time;
26 import android.preference.PreferenceManager;
27 import android.provider.Calendar.Attendees;
28 import android.provider.Calendar.Instances;
29 import android.text.TextUtils;
30 import android.util.Log;
31
32 import java.util.ArrayList;
33 import java.util.Iterator;
34 import java.util.concurrent.atomic.AtomicInteger;
35
36 // TODO: should Event be Parcelable so it can be passed via Intents?
37 public 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         if (false) {
385             // Create a numbered log because adb logcat duplicates old entries
386             // at random times and this makes it hard to compare two different
387             // runs.  We can post-process the numbered log using sort and uniq.
388             int logIndex = 0;
389             for (Event e : eventsList) {
390                 if (!e.allDay) continue;
391                 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
392                 | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
393         String timeRange = DateUtils.formatDateRange(e.startMillis,
394                         e.endMillis, flags);
395                 Log.i("Cal", logIndex + " allDay: " + e.allDay
396                                 + " days: " + e.startDay + "," + e.endDay
397                                 + " times: " + e.startTime + "," + e.endTime
398                                 + " " + timeRange
399                                 + " nth/max: " + e.getColumn() + "/" + e.getMaxColumns()
400                                 + " "  + e.title);
401                 logIndex += 1;
402             }
403             for (Event e : eventsList) {
404                 if (e.allDay) continue;
405                 int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL
406                         | DateUtils.FORMAT_CAP_NOON_MIDNIGHT;
407                 String timeRange = DateUtils.formatDateRange(e.startMillis,
408                                 e.endMillis, flags);
409                 Log.i("Cal", logIndex + " allDay: " + e.allDay
410                                 + " days: " + e.startDay + "," + e.endDay
411                                 + " times: " + e.startTime + "," + e.endTime
412                                 + " " + timeRange
413                                 + " nth/max: " + e.getColumn() + "/" + e.getMaxColumns()
414                                 + " "  + e.title);
415                 logIndex += 1;
416             }
417         }
418     }
419
420     private static void doComputePositions(ArrayList<Event> eventsList,
421             boolean doAllDayEvents) {
422         ArrayList<Event> activeList = new ArrayList<Event>();
423         ArrayList<Event> groupList = new ArrayList<Event>();
424
425         long colMask = 0;
426         int maxCols = 0;
427         for (Event event : eventsList) {
428             // Process all-day events separately
429             if (event.allDay != doAllDayEvents)
430                 continue;
431
432             long start = event.getStartMillis();
433             if (false && event.allDay) {
434                 Event e = event;
435                 Log.i("Cal", "event start,end day: " + e.startDay + "," + e.endDay
436                         + " start,end time: " + e.startTime + "," + e.endTime
437                         + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis()
438                         + " "  + e.title);
439             }
440
441             // Remove the inactive events. An event on the active list
442             // becomes inactive when its end time is less than or equal to
443             // the current event's start time.
444             Iterator<Event> iter = activeList.iterator();
445             while (iter.hasNext()) {
446                 Event active = iter.next();
447                 if (active.getEndMillis() <= start) {
448                     if (false && event.allDay) {
449                         Event e = active;
450                         Log.i("Cal", "  removing: start,end day: " + e.startDay + "," + e.endDay
451                                 + " start,end time: " + e.startTime + "," + e.endTime
452                                 + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis()
453                                 + " "  + e.title);
454                     }
455                     colMask &= ~(1L << active.getColumn());
456                     iter.remove();
457                 }
458             }
459
460             // If the active list is empty, then reset the max columns, clear
461             // the column bit mask, and empty the groupList.
462             if (activeList.isEmpty()) {
463                 for (Event ev : groupList) {
464                     ev.setMaxColumns(maxCols);
465                 }
466                 maxCols = 0;
467                 colMask = 0;
468                 groupList.clear();
469             }
470
471             // Find the first empty column.  Empty columns are represented by
472             // zero bits in the column mask "colMask".
473             int col = findFirstZeroBit(colMask);
474             if (col == 64)
475                 col = 63;
476             colMask |= (1L << col);
477             event.setColumn(col);
478             activeList.add(event);
479             groupList.add(event);
480             int len = activeList.size();
481             if (maxCols < len)
482                 maxCols = len;
483         }
484         for (Event ev : groupList) {
485             ev.setMaxColumns(maxCols);
486         }
487     }
488
489     public static int findFirstZeroBit(long val) {
490         for (int ii = 0; ii < 64; ++ii) {
491             if ((val & (1L << ii)) == 0)
492                 return ii;
493         }
494         return 64;
495     }
496
497     /**
498      * Returns a darker version of the given color.  It does this by dividing
499      * each of the red, green, and blue components by 2.  The alpha value is
500      * preserved.
501      */
502     private static final int getDarkerColor(int color) {
503         int darker = (color >> 1) & 0x007f7f7f;
504         int alpha = color & 0xff000000;
505         return alpha | darker;
506     }
507
508     // For testing. This method can be removed at any time.
509     private static ArrayList<Event> createTestEventList() {
510         ArrayList<Event> evList = new ArrayList<Event>();
511         createTestEvent(evList, 1, 5, 10);
512         createTestEvent(evList, 2, 5, 10);
513         createTestEvent(evList, 3, 15, 20);
514         createTestEvent(evList, 4, 20, 25);
515         createTestEvent(evList, 5, 30, 70);
516         createTestEvent(evList, 6, 32, 40);
517         createTestEvent(evList, 7, 32, 40);
518         createTestEvent(evList, 8, 34, 38);
519         createTestEvent(evList, 9, 34, 38);
520         createTestEvent(evList, 10, 42, 50);
521         createTestEvent(evList, 11, 45, 60);
522         createTestEvent(evList, 12, 55, 90);
523         createTestEvent(evList, 13, 65, 75);
524
525         createTestEvent(evList, 21, 105, 130);
526         createTestEvent(evList, 22, 110, 120);
527         createTestEvent(evList, 23, 115, 130);
528         createTestEvent(evList, 24, 125, 140);
529         createTestEvent(evList, 25, 127, 135);
530
531         createTestEvent(evList, 31, 150, 160);
532         createTestEvent(evList, 32, 152, 162);
533         createTestEvent(evList, 33, 153, 163);
534         createTestEvent(evList, 34, 155, 170);
535         createTestEvent(evList, 35, 158, 175);
536         createTestEvent(evList, 36, 165, 180);
537
538         return evList;
539     }
540
541     // For testing. This method can be removed at any time.
542     private static Event createTestEvent(ArrayList<Event> evList, int id,
543             int startMinute, int endMinute) {
544         Event ev = new Event();
545         ev.title = "ev" + id;
546         ev.startDay = 1;
547         ev.endDay = 1;
548         ev.setStartMillis(startMinute);
549         ev.setEndMillis(endMinute);
550         evList.add(ev);
551         return ev;
552     }
553
554     public final void dump() {
555         Log.e("Cal", "+-----------------------------------------+");
556         Log.e("Cal", "+        id = " + id);
557         Log.e("Cal", "+     color = " + color);
558         Log.e("Cal", "+     title = " + title);
559         Log.e("Cal", "+  location = " + location);
560         Log.e("Cal", "+    allDay = " + allDay);
561         Log.e("Cal", "+  startDay = " + startDay);
562         Log.e("Cal", "+    endDay = " + endDay);
563         Log.e("Cal", "+ startTime = " + startTime);
564         Log.e("Cal", "+   endTime = " + endTime);
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 }