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