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