Code drop from //branches/cupcake/...@124589
[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.preference.PreferenceManager;
25 import android.provider.Calendar.Attendees;
26 import android.provider.Calendar.Instances;
27 import android.text.TextUtils;
28 import android.text.format.DateUtils;
29 import android.text.format.Time;
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     }
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 }