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 
17 package com.android.calendar;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.content.SharedPreferences;
23 import android.content.res.Resources;
24 import android.database.Cursor;
25 import android.net.Uri;
26 import android.os.Debug;
27 import android.provider.CalendarContract.Attendees;
28 import android.provider.CalendarContract.Calendars;
29 import android.provider.CalendarContract.Events;
30 import android.provider.CalendarContract.Instances;
31 import android.text.TextUtils;
32 import android.text.format.DateUtils;
33 import android.util.Log;
34 
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.Iterator;
38 import java.util.concurrent.atomic.AtomicInteger;
39 
40 // TODO: should Event be Parcelable so it can be passed via Intents?
41 public 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
clone()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 
copyTo(Event dest)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 
newInstance()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      */
loadEvents(Context context, ArrayList<Event> events, int startDay, int days, int requestId, AtomicInteger sequenceNumber)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      */
instancesQuery(ContentResolver cr, String[] projection, int startDay, int endDay, String selection, String[] selectionArgs, String orderBy)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      */
buildEventsFromCursor( ArrayList<Event> events, Cursor cEvents, Context context, int startDay, int endDay)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      */
generateEventFromCursor(Cursor cEvents)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      */
computePositions(ArrayList<Event> eventsList, long minimumDurationMillis)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 
doComputePositions(ArrayList<Event> eventsList, long minimumDurationMillis, boolean doAlldayEvents)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 
removeAlldayActiveEvents(Event event, Iterator<Event> iter, long colMask)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 
removeNonAlldayActiveEvents( Event event, Iterator<Event> iter, long minDurationMillis, long colMask)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 
findFirstZeroBit(long val)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 
dump()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 
intersects(int julianDay, int startMinute, int endMinute)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      */
getTitleAndLocation()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 
setColumn(int column)606     public void setColumn(int column) {
607         mColumn = column;
608     }
609 
getColumn()610     public int getColumn() {
611         return mColumn;
612     }
613 
setMaxColumns(int maxColumns)614     public void setMaxColumns(int maxColumns) {
615         mMaxColumns = maxColumns;
616     }
617 
getMaxColumns()618     public int getMaxColumns() {
619         return mMaxColumns;
620     }
621 
setStartMillis(long startMillis)622     public void setStartMillis(long startMillis) {
623         this.startMillis = startMillis;
624     }
625 
getStartMillis()626     public long getStartMillis() {
627         return startMillis;
628     }
629 
setEndMillis(long endMillis)630     public void setEndMillis(long endMillis) {
631         this.endMillis = endMillis;
632     }
633 
getEndMillis()634     public long getEndMillis() {
635         return endMillis;
636     }
637 
drawAsAllday()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