diff options
Diffstat (limited to 'src/com/android/calendar/Event.java')
-rw-r--r-- | src/com/android/calendar/Event.java | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/src/com/android/calendar/Event.java b/src/com/android/calendar/Event.java new file mode 100644 index 00000000..d1551bb7 --- /dev/null +++ b/src/com/android/calendar/Event.java @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.calendar; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.Debug; +import android.pim.DateUtils; +import android.pim.Time; +import android.preference.PreferenceManager; +import android.provider.Calendar.Attendees; +import android.provider.Calendar.Instances; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; + +// TODO: should Event be Parcelable so it can be passed via Intents? +public class Event implements Comparable, Cloneable { + + private static final boolean PROFILE = false; + + private static final String[] PROJECTION = new String[] { + Instances.TITLE, // 0 + Instances.EVENT_LOCATION, // 1 + Instances.ALL_DAY, // 2 + Instances.COLOR, // 3 + Instances.EVENT_TIMEZONE, // 4 + Instances.EVENT_ID, // 5 + Instances.BEGIN, // 6 + Instances.END, // 7 + Instances._ID, // 8 + Instances.START_DAY, // 9 + Instances.END_DAY, // 10 + Instances.START_MINUTE, // 11 + Instances.END_MINUTE, // 12 + Instances.HAS_ALARM, // 13 + Instances.RRULE, // 14 + Instances.RDATE, // 15 + }; + + // The indices for the projection array above. + private static final int PROJECTION_TITLE_INDEX = 0; + private static final int PROJECTION_LOCATION_INDEX = 1; + private static final int PROJECTION_ALL_DAY_INDEX = 2; + private static final int PROJECTION_COLOR_INDEX = 3; + private static final int PROJECTION_TIMEZONE_INDEX = 4; + private static final int PROJECTION_EVENT_ID_INDEX = 5; + private static final int PROJECTION_BEGIN_INDEX = 6; + private static final int PROJECTION_END_INDEX = 7; + private static final int PROJECTION_START_DAY_INDEX = 9; + private static final int PROJECTION_END_DAY_INDEX = 10; + private static final int PROJECTION_START_MINUTE_INDEX = 11; + private static final int PROJECTION_END_MINUTE_INDEX = 12; + private static final int PROJECTION_HAS_ALARM_INDEX = 13; + private static final int PROJECTION_RRULE_INDEX = 14; + private static final int PROJECTION_RDATE_INDEX = 15; + + public long id; + public int color; + public CharSequence title; + public CharSequence location; + public boolean allDay; + + public int startDay; // start Julian day + public int endDay; // end Julian day + public int startTime; // Start and end time are in minutes since midnight + public int endTime; + + public long startMillis; // UTC milliseconds since the epoch + public long endMillis; // UTC milliseconds since the epoch + private int mColumn; + private int mMaxColumns; + + public boolean hasAlarm; + public boolean isRepeating; + + // The coordinates of the event rectangle drawn on the screen. + public float left; + public float right; + public float top; + public float bottom; + + // These 4 fields are used for navigating among events within the selected + // hour in the Day and Week view. + public Event nextRight; + public Event nextLeft; + public Event nextUp; + public Event nextDown; + + private static final int MIDNIGHT_IN_MINUTES = 24 * 60; + + @Override + public final Object clone() { + Event e = new Event(); + + e.title = title; + e.color = color; + e.location = location; + e.allDay = allDay; + e.startDay = startDay; + e.endDay = endDay; + e.startTime = startTime; + e.endTime = endTime; + e.startMillis = startMillis; + e.endMillis = endMillis; + e.hasAlarm = hasAlarm; + e.isRepeating = isRepeating; + + return e; + } + + public final void copyTo(Event dest) { + dest.id = id; + dest.title = title; + dest.color = color; + dest.location = location; + dest.allDay = allDay; + dest.startDay = startDay; + dest.endDay = endDay; + dest.startTime = startTime; + dest.endTime = endTime; + dest.startMillis = startMillis; + dest.endMillis = endMillis; + dest.hasAlarm = hasAlarm; + dest.isRepeating = isRepeating; + } + + public static final Event newInstance() { + Event e = new Event(); + + e.id = 0; + e.title = null; + e.color = 0; + e.location = null; + e.allDay = false; + e.startDay = 0; + e.endDay = 0; + e.startTime = 0; + e.endTime = 0; + e.startMillis = 0; + e.endMillis = 0; + e.hasAlarm = false; + e.isRepeating = false; + + return e; + } + + /** + * Compares this event to the given event. This is just used for checking + * if two events differ. It's not used for sorting anymore. + */ + public final int compareTo(Object obj) { + Event e = (Event) obj; + + // The earlier start day and time comes first + if (startDay < e.startDay) return -1; + if (startDay > e.startDay) return 1; + if (startTime < e.startTime) return -1; + if (startTime > e.startTime) return 1; + + // The later end time comes first (in order to put long strips on + // the left). + if (endDay < e.endDay) return 1; + if (endDay > e.endDay) return -1; + if (endTime < e.endTime) return 1; + if (endTime > e.endTime) return -1; + + // Sort all-day events before normal events. + if (allDay && !e.allDay) return -1; + if (!allDay && e.allDay) return 1; + + // If two events have the same time range, then sort them in + // alphabetical order based on their titles. + int cmp = compareStrings(title, e.title); + if (cmp != 0) { + return cmp; + } + + // If the titles are the same then compare the other fields + // so that we can use this function to check for differences + // between events. + cmp = compareStrings(location, e.location); + if (cmp != 0) { + return cmp; + } + return 0; + } + + /** + * Compare string a with string b, but if either string is null, + * then treat it (the null) as if it were the empty string (""). + * + * @param a the first string + * @param b the second string + * @return the result of comparing a with b after replacing null + * strings with "". + */ + private int compareStrings(CharSequence a, CharSequence b) { + String aStr, bStr; + if (a != null) { + aStr = a.toString(); + } else { + aStr = ""; + } + if (b != null) { + bStr = b.toString(); + } else { + bStr = ""; + } + return aStr.compareTo(bStr); + } + + /** + * Loads <i>days</i> days worth of instances starting at <i>start</i>. + */ + public static void loadEvents(Context context, ArrayList<Event> events, + long start, int days, int requestId, AtomicInteger sequenceNumber) { + + if (PROFILE) { + Debug.startMethodTracing("loadEvents"); + } + + Cursor c = null; + + events.clear(); + try { + Time local = new Time(); + int count; + + local.set(start); + int startDay = Time.getJulianDay(start, local.gmtoff); + int endDay = startDay + days; + + local.monthDay += days; + long end = local.normalize(true /* ignore isDst */); + + // Widen the time range that we query by one day on each end + // so that we can catch all-day events. All-day events are + // stored starting at midnight in UTC but should be included + // in the list of events starting at midnight local time. + // This may fetch more events than we actually want, so we + // filter them out below. + // + // The sort order is: events with an earlier start time occur + // first and if the start times are the same, then events with + // a later end time occur first. The later end time is ordered + // first so that long rectangles in the calendar views appear on + // the left side. If the start and end times of two events are + // the same then we sort alphabetically on the title. This isn't + // required for correctness, it just adds a nice touch. + + String orderBy = Instances.SORT_CALENDAR_VIEW; + + // Respect the preference to show/hide declined events + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean hideDeclined = prefs.getBoolean(CalendarPreferenceActivity.KEY_HIDE_DECLINED, + false); + + String where = null; + if (hideDeclined) { + where = Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED; + } + + c = Instances.query(context.getContentResolver(), PROJECTION, + start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, where, orderBy); + + if (c == null) { + Log.e("Cal", "loadEvents() returned null cursor!"); + return; + } + + // Check if we should return early because there are more recent + // load requests waiting. + if (requestId != sequenceNumber.get()) { + return; + } + + count = c.getCount(); + + if (count == 0) { + return; + } + + Resources res = context.getResources(); + while (c.moveToNext()) { + Event e = new Event(); + + e.id = c.getLong(PROJECTION_EVENT_ID_INDEX); + e.title = c.getString(PROJECTION_TITLE_INDEX); + e.location = c.getString(PROJECTION_LOCATION_INDEX); + e.allDay = c.getInt(PROJECTION_ALL_DAY_INDEX) != 0; + String timezone = c.getString(PROJECTION_TIMEZONE_INDEX); + + if (e.title == null || e.title.length() == 0) { + e.title = res.getString(R.string.no_title_label); + } + + if (!c.isNull(PROJECTION_COLOR_INDEX)) { + // Read the color from the database + e.color = c.getInt(PROJECTION_COLOR_INDEX); + } else { + e.color = res.getColor(R.color.event_center); + } + + long eStart = c.getLong(PROJECTION_BEGIN_INDEX); + long eEnd = c.getLong(PROJECTION_END_INDEX); + + e.startMillis = eStart; + e.startTime = c.getInt(PROJECTION_START_MINUTE_INDEX); + e.startDay = c.getInt(PROJECTION_START_DAY_INDEX); + + e.endMillis = eEnd; + e.endTime = c.getInt(PROJECTION_END_MINUTE_INDEX); + e.endDay = c.getInt(PROJECTION_END_DAY_INDEX); + + if (e.startDay > endDay || e.endDay < startDay) { + continue; + } + + e.hasAlarm = c.getInt(PROJECTION_HAS_ALARM_INDEX) != 0; + + // Check if this is a repeating event + String rrule = c.getString(PROJECTION_RRULE_INDEX); + String rdate = c.getString(PROJECTION_RDATE_INDEX); + if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) { + e.isRepeating = true; + } else { + e.isRepeating = false; + } + + events.add(e); + } + + computePositions(events); + } finally { + if (c != null) { + c.close(); + } + if (PROFILE) { + Debug.stopMethodTracing(); + } + } + } + + /** + * Computes a position for each event. Each event is displayed + * as a non-overlapping rectangle. For normal events, these rectangles + * are displayed in separate columns in the week view and day view. For + * all-day events, these rectangles are displayed in separate rows along + * the top. In both cases, each event is assigned two numbers: N, and + * Max, that specify that this event is the Nth event of Max number of + * events that are displayed in a group. The width and position of each + * rectangle depend on the maximum number of rectangles that occur at + * the same time. + * + * @param eventsList the list of events, sorted into increasing time order + */ + static void computePositions(ArrayList<Event> eventsList) { + if (eventsList == null) + return; + + // Compute the column positions separately for the all-day events + doComputePositions(eventsList, false); + doComputePositions(eventsList, true); + if (false) { + // Create a numbered log because adb logcat duplicates old entries + // at random times and this makes it hard to compare two different + // runs. We can post-process the numbered log using sort and uniq. + int logIndex = 0; + for (Event e : eventsList) { + if (!e.allDay) continue; + int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL + | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; + String timeRange = DateUtils.formatDateRange(e.startMillis, + e.endMillis, flags); + Log.i("Cal", logIndex + " allDay: " + e.allDay + + " days: " + e.startDay + "," + e.endDay + + " times: " + e.startTime + "," + e.endTime + + " " + timeRange + + " nth/max: " + e.getColumn() + "/" + e.getMaxColumns() + + " " + e.title); + logIndex += 1; + } + for (Event e : eventsList) { + if (e.allDay) continue; + int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL + | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; + String timeRange = DateUtils.formatDateRange(e.startMillis, + e.endMillis, flags); + Log.i("Cal", logIndex + " allDay: " + e.allDay + + " days: " + e.startDay + "," + e.endDay + + " times: " + e.startTime + "," + e.endTime + + " " + timeRange + + " nth/max: " + e.getColumn() + "/" + e.getMaxColumns() + + " " + e.title); + logIndex += 1; + } + } + } + + private static void doComputePositions(ArrayList<Event> eventsList, + boolean doAllDayEvents) { + ArrayList<Event> activeList = new ArrayList<Event>(); + ArrayList<Event> groupList = new ArrayList<Event>(); + + long colMask = 0; + int maxCols = 0; + for (Event event : eventsList) { + // Process all-day events separately + if (event.allDay != doAllDayEvents) + continue; + + long start = event.getStartMillis(); + if (false && event.allDay) { + Event e = event; + Log.i("Cal", "event start,end day: " + e.startDay + "," + e.endDay + + " start,end time: " + e.startTime + "," + e.endTime + + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis() + + " " + e.title); + } + + // Remove the inactive events. An event on the active list + // becomes inactive when its end time is less than or equal to + // the current event's start time. + Iterator<Event> iter = activeList.iterator(); + while (iter.hasNext()) { + Event active = iter.next(); + if (active.getEndMillis() <= start) { + if (false && event.allDay) { + Event e = active; + Log.i("Cal", " removing: start,end day: " + e.startDay + "," + e.endDay + + " start,end time: " + e.startTime + "," + e.endTime + + " start,end millis: " + e.getStartMillis() + "," + e.getEndMillis() + + " " + e.title); + } + colMask &= ~(1L << active.getColumn()); + iter.remove(); + } + } + + // If the active list is empty, then reset the max columns, clear + // the column bit mask, and empty the groupList. + if (activeList.isEmpty()) { + for (Event ev : groupList) { + ev.setMaxColumns(maxCols); + } + maxCols = 0; + colMask = 0; + groupList.clear(); + } + + // Find the first empty column. Empty columns are represented by + // zero bits in the column mask "colMask". + int col = findFirstZeroBit(colMask); + if (col == 64) + col = 63; + colMask |= (1L << col); + event.setColumn(col); + activeList.add(event); + groupList.add(event); + int len = activeList.size(); + if (maxCols < len) + maxCols = len; + } + for (Event ev : groupList) { + ev.setMaxColumns(maxCols); + } + } + + public static int findFirstZeroBit(long val) { + for (int ii = 0; ii < 64; ++ii) { + if ((val & (1L << ii)) == 0) + return ii; + } + return 64; + } + + /** + * Returns a darker version of the given color. It does this by dividing + * each of the red, green, and blue components by 2. The alpha value is + * preserved. + */ + private static final int getDarkerColor(int color) { + int darker = (color >> 1) & 0x007f7f7f; + int alpha = color & 0xff000000; + return alpha | darker; + } + + // For testing. This method can be removed at any time. + private static ArrayList<Event> createTestEventList() { + ArrayList<Event> evList = new ArrayList<Event>(); + createTestEvent(evList, 1, 5, 10); + createTestEvent(evList, 2, 5, 10); + createTestEvent(evList, 3, 15, 20); + createTestEvent(evList, 4, 20, 25); + createTestEvent(evList, 5, 30, 70); + createTestEvent(evList, 6, 32, 40); + createTestEvent(evList, 7, 32, 40); + createTestEvent(evList, 8, 34, 38); + createTestEvent(evList, 9, 34, 38); + createTestEvent(evList, 10, 42, 50); + createTestEvent(evList, 11, 45, 60); + createTestEvent(evList, 12, 55, 90); + createTestEvent(evList, 13, 65, 75); + + createTestEvent(evList, 21, 105, 130); + createTestEvent(evList, 22, 110, 120); + createTestEvent(evList, 23, 115, 130); + createTestEvent(evList, 24, 125, 140); + createTestEvent(evList, 25, 127, 135); + + createTestEvent(evList, 31, 150, 160); + createTestEvent(evList, 32, 152, 162); + createTestEvent(evList, 33, 153, 163); + createTestEvent(evList, 34, 155, 170); + createTestEvent(evList, 35, 158, 175); + createTestEvent(evList, 36, 165, 180); + + return evList; + } + + // For testing. This method can be removed at any time. + private static Event createTestEvent(ArrayList<Event> evList, int id, + int startMinute, int endMinute) { + Event ev = new Event(); + ev.title = "ev" + id; + ev.startDay = 1; + ev.endDay = 1; + ev.setStartMillis(startMinute); + ev.setEndMillis(endMinute); + evList.add(ev); + return ev; + } + + public final void dump() { + Log.e("Cal", "+-----------------------------------------+"); + Log.e("Cal", "+ id = " + id); + Log.e("Cal", "+ color = " + color); + Log.e("Cal", "+ title = " + title); + Log.e("Cal", "+ location = " + location); + Log.e("Cal", "+ allDay = " + allDay); + Log.e("Cal", "+ startDay = " + startDay); + Log.e("Cal", "+ endDay = " + endDay); + Log.e("Cal", "+ startTime = " + startTime); + Log.e("Cal", "+ endTime = " + endTime); + } + + public final boolean intersects(int julianDay, int startMinute, + int endMinute) { + if (endDay < julianDay) { + return false; + } + + if (startDay > julianDay) { + return false; + } + + if (endDay == julianDay) { + if (endTime < startMinute) { + return false; + } + // An event that ends at the start minute should not be considered + // as intersecting the given time span, but don't exclude + // zero-length (or very short) events. + if (endTime == startMinute + && (startTime != endTime || startDay != endDay)) { + return false; + } + } + + if (startDay == julianDay && startTime > endMinute) { + return false; + } + + return true; + } + + /** + * Returns the event title and location separated by a comma. If the + * location is already part of the title (at the end of the title), then + * just the title is returned. + * + * @return the event title and location as a String + */ + public String getTitleAndLocation() { + String text = title.toString(); + + // Append the location to the title, unless the title ends with the + // location (for example, "meeting in building 42" ends with the + // location). + if (location != null) { + String locationString = location.toString(); + if (!text.endsWith(locationString)) { + text += ", " + locationString; + } + } + return text; + } + + public void setColumn(int column) { + mColumn = column; + } + + public int getColumn() { + return mColumn; + } + + public void setMaxColumns(int maxColumns) { + mMaxColumns = maxColumns; + } + + public int getMaxColumns() { + return mMaxColumns; + } + + public void setStartMillis(long startMillis) { + this.startMillis = startMillis; + } + + public long getStartMillis() { + return startMillis; + } + + public void setEndMillis(long endMillis) { + this.endMillis = endMillis; + } + + public long getEndMillis() { + return endMillis; + } +} |