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