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