/* * 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 eventHandlers = new LinkedHashMap(5); private final LinkedList mToBeRemovedEventHandlers = new LinkedList(); private final LinkedHashMap mToBeAddedEventHandlers = new LinkedHashMap< Integer, EventHandler>(); private Pair mFirstEventHandler; private Pair mToBeAddedFirstEventHandler; private volatile int mDispatchInProgressCounter = 0; private static WeakHashMap> instances = new WeakHashMap>(); private final WeakHashMap filters = new WeakHashMap(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(). *

* 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. *

* 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 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> handlers = eventHandlers.entrySet().iterator(); handlers.hasNext();) { Entry 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 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(key, eventHandler); } else { mFirstEventHandler = new Pair(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(); } }