diff options
Diffstat (limited to 'src/com/android/calendar/CalendarController.java')
-rw-r--r-- | src/com/android/calendar/CalendarController.java | 713 |
1 files changed, 713 insertions, 0 deletions
diff --git a/src/com/android/calendar/CalendarController.java b/src/com/android/calendar/CalendarController.java new file mode 100644 index 00000000..37286f2e --- /dev/null +++ b/src/com/android/calendar/CalendarController.java @@ -0,0 +1,713 @@ +/* + * Copyright (C) 2010 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 static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; +import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; +import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; +import static android.provider.CalendarContract.Attendees.ATTENDEE_STATUS; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract.Attendees; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.Events; +import android.text.format.Time; +import android.util.Log; +import android.util.Pair; + +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map.Entry; +import java.util.WeakHashMap; + +public class CalendarController { + private static final boolean DEBUG = false; + private static final String TAG = "CalendarController"; + + public static final String EVENT_EDIT_ON_LAUNCH = "editMode"; + + public static final int MIN_CALENDAR_YEAR = 1970; + public static final int MAX_CALENDAR_YEAR = 2036; + public static final int MIN_CALENDAR_WEEK = 0; + public static final int MAX_CALENDAR_WEEK = 3497; // weeks between 1/1/1970 and 1/1/2037 + + private final Context mContext; + + // This uses a LinkedHashMap so that we can replace fragments based on the + // view id they are being expanded into since we can't guarantee a reference + // to the handler will be findable + private final LinkedHashMap<Integer,EventHandler> eventHandlers = + new LinkedHashMap<Integer,EventHandler>(5); + private final LinkedList<Integer> mToBeRemovedEventHandlers = new LinkedList<Integer>(); + private final LinkedHashMap<Integer, EventHandler> mToBeAddedEventHandlers = new LinkedHashMap< + Integer, EventHandler>(); + private Pair<Integer, EventHandler> mFirstEventHandler; + private Pair<Integer, EventHandler> mToBeAddedFirstEventHandler; + private volatile int mDispatchInProgressCounter = 0; + + private static WeakHashMap<Context, WeakReference<CalendarController>> instances = + new WeakHashMap<Context, WeakReference<CalendarController>>(); + + private final WeakHashMap<Object, Long> filters = new WeakHashMap<Object, Long>(1); + + private int mViewType = -1; + private int mDetailViewType = -1; + private int mPreviousViewType = -1; + private long mEventId = -1; + private final Time mTime = new Time(); + private long mDateFlags = 0; + + private final Runnable mUpdateTimezone = new Runnable() { + @Override + public void run() { + mTime.switchTimezone(Utils.getTimeZone(mContext, this)); + } + }; + + /** + * One of the event types that are sent to or from the controller + */ + public interface EventType { + // Simple view of an event + final long VIEW_EVENT = 1L << 1; + + // Full detail view in read only mode + final long VIEW_EVENT_DETAILS = 1L << 2; + + // full detail view in edit mode + final long EDIT_EVENT = 1L << 3; + + final long GO_TO = 1L << 5; + + final long EVENTS_CHANGED = 1L << 7; + + final long USER_HOME = 1L << 9; + + // date range has changed, update the title + final long UPDATE_TITLE = 1L << 10; + } + + /** + * One of the Agenda/Day/Week/Month view types + */ + public interface ViewType { + final int DETAIL = -1; + final int CURRENT = 0; + final int AGENDA = 1; + final int DAY = 2; + final int WEEK = 3; + final int MONTH = 4; + final int EDIT = 5; + final int MAX_VALUE = 5; + } + + public static class EventInfo { + + private static final long ATTENTEE_STATUS_MASK = 0xFF; + private static final long ALL_DAY_MASK = 0x100; + private static final int ATTENDEE_STATUS_NONE_MASK = 0x01; + private static final int ATTENDEE_STATUS_ACCEPTED_MASK = 0x02; + private static final int ATTENDEE_STATUS_DECLINED_MASK = 0x04; + private static final int ATTENDEE_STATUS_TENTATIVE_MASK = 0x08; + + public long eventType; // one of the EventType + public int viewType; // one of the ViewType + public long id; // event id + public Time selectedTime; // the selected time in focus + + // Event start and end times. All-day events are represented in: + // - local time for GO_TO commands + // - UTC time for VIEW_EVENT and other event-related commands + public Time startTime; + public Time endTime; + + public int x; // x coordinate in the activity space + public int y; // y coordinate in the activity space + public String query; // query for a user search + public ComponentName componentName; // used in combination with query + public String eventTitle; + public long calendarId; + + /** + * For EventType.VIEW_EVENT: + * It is the default attendee response and an all day event indicator. + * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED, + * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE. + * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response. + * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay(). + * <p> + * For EventType.GO_TO: + * Set to {@link #EXTRA_GOTO_TIME} to go to the specified date/time. + * Set to {@link #EXTRA_GOTO_DATE} to consider the date but ignore the time. + * Set to {@link #EXTRA_GOTO_BACK_TO_PREVIOUS} if back should bring back previous view. + * Set to {@link #EXTRA_GOTO_TODAY} if this is a user request to go to the current time. + * <p> + * For EventType.UPDATE_TITLE: + * Set formatting flags for Utils.formatDateRange + */ + public long extraLong; + + public boolean isAllDay() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to isAllDay , wrong event type " + eventType); + return false; + } + return ((extraLong & ALL_DAY_MASK) != 0) ? true : false; + } + + public int getResponse() { + if (eventType != EventType.VIEW_EVENT) { + Log.wtf(TAG, "illegal call to getResponse , wrong event type " + eventType); + return Attendees.ATTENDEE_STATUS_NONE; + } + + int response = (int)(extraLong & ATTENTEE_STATUS_MASK); + switch (response) { + case ATTENDEE_STATUS_NONE_MASK: + return Attendees.ATTENDEE_STATUS_NONE; + case ATTENDEE_STATUS_ACCEPTED_MASK: + return Attendees.ATTENDEE_STATUS_ACCEPTED; + case ATTENDEE_STATUS_DECLINED_MASK: + return Attendees.ATTENDEE_STATUS_DECLINED; + case ATTENDEE_STATUS_TENTATIVE_MASK: + return Attendees.ATTENDEE_STATUS_TENTATIVE; + default: + Log.wtf(TAG,"Unknown attendee response " + response); + } + return ATTENDEE_STATUS_NONE_MASK; + } + + // Used to build the extra long for a VIEW event. + public static long buildViewExtraLong(int response, boolean allDay) { + long extra = allDay ? ALL_DAY_MASK : 0; + + switch (response) { + case Attendees.ATTENDEE_STATUS_NONE: + extra |= ATTENDEE_STATUS_NONE_MASK; + break; + case Attendees.ATTENDEE_STATUS_ACCEPTED: + extra |= ATTENDEE_STATUS_ACCEPTED_MASK; + break; + case Attendees.ATTENDEE_STATUS_DECLINED: + extra |= ATTENDEE_STATUS_DECLINED_MASK; + break; + case Attendees.ATTENDEE_STATUS_TENTATIVE: + extra |= ATTENDEE_STATUS_TENTATIVE_MASK; + break; + default: + Log.wtf(TAG,"Unknown attendee response " + response); + extra |= ATTENDEE_STATUS_NONE_MASK; + break; + } + return extra; + } + } + + /** + * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time + * can be ignored + */ + public static final long EXTRA_GOTO_DATE = 1; + public static final long EXTRA_GOTO_TIME = 2; + public static final long EXTRA_GOTO_BACK_TO_PREVIOUS = 4; + public static final long EXTRA_GOTO_TODAY = 8; + + public interface EventHandler { + long getSupportedEventTypes(); + void handleEvent(EventInfo event); + + /** + * This notifies the handler that the database has changed and it should + * update its view. + */ + void eventsChanged(); + } + + /** + * Creates and/or returns an instance of CalendarController associated with + * the supplied context. It is best to pass in the current Activity. + * + * @param context The activity if at all possible. + */ + public static CalendarController getInstance(Context context) { + synchronized (instances) { + CalendarController controller = null; + WeakReference<CalendarController> weakController = instances.get(context); + if (weakController != null) { + controller = weakController.get(); + } + + if (controller == null) { + controller = new CalendarController(context); + instances.put(context, new WeakReference(controller)); + } + return controller; + } + } + + /** + * Removes an instance when it is no longer needed. This should be called in + * an activity's onDestroy method. + * + * @param context The activity used to create the controller + */ + public static void removeInstance(Context context) { + instances.remove(context); + } + + private CalendarController(Context context) { + mContext = context; + mUpdateTimezone.run(); + mTime.setToNow(); + mDetailViewType = Utils.getSharedPreference(mContext, + GeneralPreferences.KEY_DETAILED_VIEW, + GeneralPreferences.DEFAULT_DETAILED_VIEW); + } + + public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis, + long endMillis, int x, int y, long selectedMillis) { + // TODO: pass the real allDay status or at least a status that says we don't know the + // status and have the receiver query the data. + // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo + // so currently the missing allDay status has no effect. + sendEventRelatedEventWithExtra(sender, eventType, eventId, startMillis, endMillis, x, y, + EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false), + selectedMillis); + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of {@link EventType} + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + */ + public void sendEventRelatedEventWithExtra(Object sender, long eventType, long eventId, + long startMillis, long endMillis, int x, int y, long extraLong, long selectedMillis) { + sendEventRelatedEventWithExtraWithTitleWithCalendarId(sender, eventType, eventId, + startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1); + } + + /** + * Helper for sending New/View/Edit/Delete events + * + * @param sender object of the caller + * @param eventType one of {@link EventType} + * @param eventId event id + * @param startMillis start time + * @param endMillis end time + * @param x x coordinate in the activity space + * @param y y coordinate in the activity space + * @param extraLong default response value for the "simple event view" and all day indication. + * Use Attendees.ATTENDEE_STATUS_NONE for no response. + * @param selectedMillis The time to specify as selected + * @param title The title of the event + * @param calendarId The id of the calendar which the event belongs to + */ + public void sendEventRelatedEventWithExtraWithTitleWithCalendarId(Object sender, long eventType, + long eventId, long startMillis, long endMillis, int x, int y, long extraLong, + long selectedMillis, String title, long calendarId) { + EventInfo info = new EventInfo(); + info.eventType = eventType; + if (eventType == EventType.VIEW_EVENT_DETAILS) { + info.viewType = ViewType.CURRENT; + } + + info.id = eventId; + info.startTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); + info.startTime.set(startMillis); + if (selectedMillis != -1) { + info.selectedTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); + info.selectedTime.set(selectedMillis); + } else { + info.selectedTime = info.startTime; + } + info.endTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone)); + info.endTime.set(endMillis); + info.x = x; + info.y = y; + info.extraLong = extraLong; + info.eventTitle = title; + info.calendarId = calendarId; + this.sendEvent(sender, info); + } + /** + * Helper for sending non-calendar-event events + * + * @param sender object of the caller + * @param eventType one of {@link EventType} + * @param start start time + * @param end end time + * @param eventId event id + * @param viewType {@link ViewType} + */ + public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId, + int viewType) { + sendEvent(sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null, + null); + } + + /** + * sendEvent() variant with extraLong, search query, and search component name. + */ + public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId, + int viewType, long extraLong, String query, ComponentName componentName) { + sendEvent(sender, eventType, start, end, start, eventId, viewType, extraLong, query, + componentName); + } + + public void sendEvent(Object sender, long eventType, Time start, Time end, Time selected, + long eventId, int viewType, long extraLong, String query, ComponentName componentName) { + EventInfo info = new EventInfo(); + info.eventType = eventType; + info.startTime = start; + info.selectedTime = selected; + info.endTime = end; + info.id = eventId; + info.viewType = viewType; + info.query = query; + info.componentName = componentName; + info.extraLong = extraLong; + this.sendEvent(sender, info); + } + + public void sendEvent(Object sender, final EventInfo event) { + // TODO Throw exception on invalid events + + if (DEBUG) { + Log.d(TAG, eventInfoToString(event)); + } + + Long filteredTypes = filters.get(sender); + if (filteredTypes != null && (filteredTypes.longValue() & event.eventType) != 0) { + // Suppress event per filter + if (DEBUG) { + Log.d(TAG, "Event suppressed"); + } + return; + } + + mPreviousViewType = mViewType; + + // Fix up view if not specified + if (event.viewType == ViewType.DETAIL) { + event.viewType = mDetailViewType; + mViewType = mDetailViewType; + } else if (event.viewType == ViewType.CURRENT) { + event.viewType = mViewType; + } else if (event.viewType != ViewType.EDIT) { + mViewType = event.viewType; + + if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY + || (Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK)) { + mDetailViewType = mViewType; + } + } + + if (DEBUG) { + Log.d(TAG, "vvvvvvvvvvvvvvv"); + Log.d(TAG, "Start " + (event.startTime == null ? "null" : event.startTime.toString())); + Log.d(TAG, "End " + (event.endTime == null ? "null" : event.endTime.toString())); + Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString())); + Log.d(TAG, "mTime " + (mTime == null ? "null" : mTime.toString())); + } + + long startMillis = 0; + if (event.startTime != null) { + startMillis = event.startTime.toMillis(false); + } + + // Set mTime if selectedTime is set + if (event.selectedTime != null && event.selectedTime.toMillis(false) != 0) { + mTime.set(event.selectedTime); + } else { + if (startMillis != 0) { + // selectedTime is not set so set mTime to startTime iff it is not + // within start and end times + long mtimeMillis = mTime.toMillis(false); + if (mtimeMillis < startMillis + || (event.endTime != null && mtimeMillis > event.endTime.toMillis(false))) { + mTime.set(event.startTime); + } + } + event.selectedTime = mTime; + } + // Store the formatting flags if this is an update to the title + if (event.eventType == EventType.UPDATE_TITLE) { + mDateFlags = event.extraLong; + } + + // Fix up start time if not specified + if (startMillis == 0) { + event.startTime = mTime; + } + if (DEBUG) { + Log.d(TAG, "Start " + (event.startTime == null ? "null" : event.startTime.toString())); + Log.d(TAG, "End " + (event.endTime == null ? "null" : event.endTime.toString())); + Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString())); + Log.d(TAG, "mTime " + (mTime == null ? "null" : mTime.toString())); + Log.d(TAG, "^^^^^^^^^^^^^^^"); + } + + // Store the eventId if we're entering edit event + if ((event.eventType + & (EventType.VIEW_EVENT_DETAILS)) + != 0) { + if (event.id > 0) { + mEventId = event.id; + } else { + mEventId = -1; + } + } + + boolean handled = false; + synchronized (this) { + mDispatchInProgressCounter ++; + + if (DEBUG) { + Log.d(TAG, "sendEvent: Dispatching to " + eventHandlers.size() + " handlers"); + } + // Dispatch to event handler(s) + if (mFirstEventHandler != null) { + // Handle the 'first' one before handling the others + EventHandler handler = mFirstEventHandler.second; + if (handler != null && (handler.getSupportedEventTypes() & event.eventType) != 0 + && !mToBeRemovedEventHandlers.contains(mFirstEventHandler.first)) { + handler.handleEvent(event); + handled = true; + } + } + for (Iterator<Entry<Integer, EventHandler>> handlers = + eventHandlers.entrySet().iterator(); handlers.hasNext();) { + Entry<Integer, EventHandler> entry = handlers.next(); + int key = entry.getKey(); + if (mFirstEventHandler != null && key == mFirstEventHandler.first) { + // If this was the 'first' handler it was already handled + continue; + } + EventHandler eventHandler = entry.getValue(); + if (eventHandler != null + && (eventHandler.getSupportedEventTypes() & event.eventType) != 0) { + if (mToBeRemovedEventHandlers.contains(key)) { + continue; + } + eventHandler.handleEvent(event); + handled = true; + } + } + + mDispatchInProgressCounter --; + + if (mDispatchInProgressCounter == 0) { + + // Deregister removed handlers + if (mToBeRemovedEventHandlers.size() > 0) { + for (Integer zombie : mToBeRemovedEventHandlers) { + eventHandlers.remove(zombie); + if (mFirstEventHandler != null && zombie.equals(mFirstEventHandler.first)) { + mFirstEventHandler = null; + } + } + mToBeRemovedEventHandlers.clear(); + } + // Add new handlers + if (mToBeAddedFirstEventHandler != null) { + mFirstEventHandler = mToBeAddedFirstEventHandler; + mToBeAddedFirstEventHandler = null; + } + if (mToBeAddedEventHandlers.size() > 0) { + for (Entry<Integer, EventHandler> food : mToBeAddedEventHandlers.entrySet()) { + eventHandlers.put(food.getKey(), food.getValue()); + } + } + } + } + } + + /** + * Adds or updates an event handler. This uses a LinkedHashMap so that we can + * replace fragments based on the view id they are being expanded into. + * + * @param key The view id or placeholder for this handler + * @param eventHandler Typically a fragment or activity in the calendar app + */ + public void registerEventHandler(int key, EventHandler eventHandler) { + synchronized (this) { + if (mDispatchInProgressCounter > 0) { + mToBeAddedEventHandlers.put(key, eventHandler); + } else { + eventHandlers.put(key, eventHandler); + } + } + } + + public void registerFirstEventHandler(int key, EventHandler eventHandler) { + synchronized (this) { + registerEventHandler(key, eventHandler); + if (mDispatchInProgressCounter > 0) { + mToBeAddedFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler); + } else { + mFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler); + } + } + } + + public void deregisterEventHandler(Integer key) { + synchronized (this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.add(key); + } else { + eventHandlers.remove(key); + if (mFirstEventHandler != null && mFirstEventHandler.first == key) { + mFirstEventHandler = null; + } + } + } + } + + public void deregisterAllEventHandlers() { + synchronized (this) { + if (mDispatchInProgressCounter > 0) { + // To avoid ConcurrencyException, stash away the event handler for now. + mToBeRemovedEventHandlers.addAll(eventHandlers.keySet()); + } else { + eventHandlers.clear(); + mFirstEventHandler = null; + } + } + } + + // FRAG_TODO doesn't work yet + public void filterBroadcasts(Object sender, long eventTypes) { + filters.put(sender, eventTypes); + } + + /** + * @return the time that this controller is currently pointed at + */ + public long getTime() { + return mTime.toMillis(false); + } + + /** + * @return the last set of date flags sent with + * {@link EventType#UPDATE_TITLE} + */ + public long getDateFlags() { + return mDateFlags; + } + + /** + * Set the time this controller is currently pointed at + * + * @param millisTime Time since epoch in millis + */ + public void setTime(long millisTime) { + mTime.set(millisTime); + } + + /** + * @return the last event ID the edit view was launched with + */ + public long getEventId() { + return mEventId; + } + + public int getViewType() { + return mViewType; + } + + public int getPreviousViewType() { + return mPreviousViewType; + } + + public void launchViewEvent(long eventId, long startMillis, long endMillis, int response) { + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); + intent.setData(eventUri); + intent.setClass(mContext, AllInOneActivity.class); + intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis); + intent.putExtra(EXTRA_EVENT_END_TIME, endMillis); + intent.putExtra(ATTENDEE_STATUS, response); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + mContext.startActivity(intent); + } + + // Forces the viewType. Should only be used for initialization. + public void setViewType(int viewType) { + mViewType = viewType; + } + + // Sets the eventId. Should only be used for initialization. + public void setEventId(long eventId) { + mEventId = eventId; + } + + private String eventInfoToString(EventInfo eventInfo) { + String tmp = "Unknown"; + + StringBuilder builder = new StringBuilder(); + if ((eventInfo.eventType & EventType.GO_TO) != 0) { + tmp = "Go to time/event"; + } else if ((eventInfo.eventType & EventType.VIEW_EVENT) != 0) { + tmp = "View event"; + } else if ((eventInfo.eventType & EventType.VIEW_EVENT_DETAILS) != 0) { + tmp = "View details"; + } else if ((eventInfo.eventType & EventType.EVENTS_CHANGED) != 0) { + tmp = "Refresh events"; + } else if ((eventInfo.eventType & EventType.USER_HOME) != 0) { + tmp = "Gone home"; + } else if ((eventInfo.eventType & EventType.UPDATE_TITLE) != 0) { + tmp = "Update title"; + } + builder.append(tmp); + builder.append(": id="); + builder.append(eventInfo.id); + builder.append(", selected="); + builder.append(eventInfo.selectedTime); + builder.append(", start="); + builder.append(eventInfo.startTime); + builder.append(", end="); + builder.append(eventInfo.endTime); + builder.append(", viewType="); + builder.append(eventInfo.viewType); + builder.append(", x="); + builder.append(eventInfo.x); + builder.append(", y="); + builder.append(eventInfo.y); + return builder.toString(); + } +} |