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