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