Home | History | Annotate | Download | only in calendar
      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
    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