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