1/*
2 * Copyright (C) 2010 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
17package com.android.calendar;
18
19import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY;
20import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
21import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME;
22import static android.provider.CalendarContract.Attendees.ATTENDEE_STATUS;
23
24import android.accounts.Account;
25import android.accounts.AccountManager;
26import android.app.Activity;
27import android.app.SearchManager;
28import android.app.SearchableInfo;
29import android.content.ComponentName;
30import android.content.ContentResolver;
31import android.content.ContentUris;
32import android.content.Context;
33import android.content.Intent;
34import android.net.Uri;
35import android.os.Bundle;
36import android.provider.CalendarContract.Attendees;
37import android.provider.CalendarContract.Calendars;
38import android.provider.CalendarContract.Events;
39import android.text.format.Time;
40import android.util.Log;
41import android.util.Pair;
42
43import com.android.calendar.event.EditEventActivity;
44import com.android.calendar.selectcalendars.SelectVisibleCalendarsActivity;
45
46import java.lang.ref.WeakReference;
47import java.util.Iterator;
48import java.util.LinkedHashMap;
49import java.util.LinkedList;
50import java.util.Map.Entry;
51import java.util.WeakHashMap;
52
53public class CalendarController {
54    private static final boolean DEBUG = false;
55    private static final String TAG = "CalendarController";
56
57    public static final String EVENT_EDIT_ON_LAUNCH = "editMode";
58
59    public static final int MIN_CALENDAR_YEAR = 1970;
60    public static final int MAX_CALENDAR_YEAR = 2036;
61    public static final int MIN_CALENDAR_WEEK = 0;
62    public static final int MAX_CALENDAR_WEEK = 3497; // weeks between 1/1/1970 and 1/1/2037
63
64    private final Context mContext;
65
66    // This uses a LinkedHashMap so that we can replace fragments based on the
67    // view id they are being expanded into since we can't guarantee a reference
68    // to the handler will be findable
69    private final LinkedHashMap<Integer,EventHandler> eventHandlers =
70            new LinkedHashMap<Integer,EventHandler>(5);
71    private final LinkedList<Integer> mToBeRemovedEventHandlers = new LinkedList<Integer>();
72    private final LinkedHashMap<Integer, EventHandler> mToBeAddedEventHandlers = new LinkedHashMap<
73            Integer, EventHandler>();
74    private Pair<Integer, EventHandler> mFirstEventHandler;
75    private Pair<Integer, EventHandler> mToBeAddedFirstEventHandler;
76    private volatile int mDispatchInProgressCounter = 0;
77
78    private static WeakHashMap<Context, WeakReference<CalendarController>> instances =
79        new WeakHashMap<Context, WeakReference<CalendarController>>();
80
81    private final WeakHashMap<Object, Long> filters = new WeakHashMap<Object, Long>(1);
82
83    private int mViewType = -1;
84    private int mDetailViewType = -1;
85    private int mPreviousViewType = -1;
86    private long mEventId = -1;
87    private final Time mTime = new Time();
88    private long mDateFlags = 0;
89
90    private final Runnable mUpdateTimezone = new Runnable() {
91        @Override
92        public void run() {
93            mTime.switchTimezone(Utils.getTimeZone(mContext, this));
94        }
95    };
96
97    /**
98     * One of the event types that are sent to or from the controller
99     */
100    public interface EventType {
101        final long CREATE_EVENT = 1L;
102
103        // Simple view of an event
104        final long VIEW_EVENT = 1L << 1;
105
106        // Full detail view in read only mode
107        final long VIEW_EVENT_DETAILS = 1L << 2;
108
109        // full detail view in edit mode
110        final long EDIT_EVENT = 1L << 3;
111
112        final long DELETE_EVENT = 1L << 4;
113
114        final long GO_TO = 1L << 5;
115
116        final long LAUNCH_SETTINGS = 1L << 6;
117
118        final long EVENTS_CHANGED = 1L << 7;
119
120        final long SEARCH = 1L << 8;
121
122        // User has pressed the home key
123        final long USER_HOME = 1L << 9;
124
125        // date range has changed, update the title
126        final long UPDATE_TITLE = 1L << 10;
127
128        // select which calendars to display
129        final long LAUNCH_SELECT_VISIBLE_CALENDARS = 1L << 11;
130    }
131
132    /**
133     * One of the Agenda/Day/Week/Month view types
134     */
135    public interface ViewType {
136        final int DETAIL = -1;
137        final int CURRENT = 0;
138        final int AGENDA = 1;
139        final int DAY = 2;
140        final int WEEK = 3;
141        final int MONTH = 4;
142        final int EDIT = 5;
143        final int MAX_VALUE = 5;
144    }
145
146    public static class EventInfo {
147
148        private static final long ATTENTEE_STATUS_MASK = 0xFF;
149        private static final long ALL_DAY_MASK = 0x100;
150        private static final int ATTENDEE_STATUS_NONE_MASK = 0x01;
151        private static final int ATTENDEE_STATUS_ACCEPTED_MASK = 0x02;
152        private static final int ATTENDEE_STATUS_DECLINED_MASK = 0x04;
153        private static final int ATTENDEE_STATUS_TENTATIVE_MASK = 0x08;
154
155        public long eventType; // one of the EventType
156        public int viewType; // one of the ViewType
157        public long id; // event id
158        public Time selectedTime; // the selected time in focus
159
160        // Event start and end times.  All-day events are represented in:
161        // - local time for GO_TO commands
162        // - UTC time for VIEW_EVENT and other event-related commands
163        public Time startTime;
164        public Time endTime;
165
166        public int x; // x coordinate in the activity space
167        public int y; // y coordinate in the activity space
168        public String query; // query for a user search
169        public ComponentName componentName;  // used in combination with query
170        public String eventTitle;
171        public long calendarId;
172
173        /**
174         * For EventType.VIEW_EVENT:
175         * It is the default attendee response and an all day event indicator.
176         * Set to Attendees.ATTENDEE_STATUS_NONE, Attendees.ATTENDEE_STATUS_ACCEPTED,
177         * Attendees.ATTENDEE_STATUS_DECLINED, or Attendees.ATTENDEE_STATUS_TENTATIVE.
178         * To signal the event is an all-day event, "or" ALL_DAY_MASK with the response.
179         * Alternatively, use buildViewExtraLong(), getResponse(), and isAllDay().
180         * <p>
181         * For EventType.CREATE_EVENT:
182         * Set to {@link #EXTRA_CREATE_ALL_DAY} for creating an all-day event.
183         * <p>
184         * For EventType.GO_TO:
185         * Set to {@link #EXTRA_GOTO_TIME} to go to the specified date/time.
186         * Set to {@link #EXTRA_GOTO_DATE} to consider the date but ignore the time.
187         * Set to {@link #EXTRA_GOTO_BACK_TO_PREVIOUS} if back should bring back previous view.
188         * Set to {@link #EXTRA_GOTO_TODAY} if this is a user request to go to the current time.
189         * <p>
190         * For EventType.UPDATE_TITLE:
191         * Set formatting flags for Utils.formatDateRange
192         */
193        public long extraLong;
194
195        public boolean isAllDay() {
196            if (eventType != EventType.VIEW_EVENT) {
197                Log.wtf(TAG, "illegal call to isAllDay , wrong event type " + eventType);
198                return false;
199            }
200            return ((extraLong & ALL_DAY_MASK) != 0) ? true : false;
201        }
202
203        public  int getResponse() {
204            if (eventType != EventType.VIEW_EVENT) {
205                Log.wtf(TAG, "illegal call to getResponse , wrong event type " + eventType);
206                return Attendees.ATTENDEE_STATUS_NONE;
207            }
208
209            int response = (int)(extraLong & ATTENTEE_STATUS_MASK);
210            switch (response) {
211                case ATTENDEE_STATUS_NONE_MASK:
212                    return Attendees.ATTENDEE_STATUS_NONE;
213                case ATTENDEE_STATUS_ACCEPTED_MASK:
214                    return Attendees.ATTENDEE_STATUS_ACCEPTED;
215                case ATTENDEE_STATUS_DECLINED_MASK:
216                    return Attendees.ATTENDEE_STATUS_DECLINED;
217                case ATTENDEE_STATUS_TENTATIVE_MASK:
218                    return Attendees.ATTENDEE_STATUS_TENTATIVE;
219                default:
220                    Log.wtf(TAG,"Unknown attendee response " + response);
221            }
222            return ATTENDEE_STATUS_NONE_MASK;
223        }
224
225        // Used to build the extra long for a VIEW event.
226        public static long buildViewExtraLong(int response, boolean allDay) {
227            long extra = allDay ? ALL_DAY_MASK : 0;
228
229            switch (response) {
230                case Attendees.ATTENDEE_STATUS_NONE:
231                    extra |= ATTENDEE_STATUS_NONE_MASK;
232                    break;
233                case Attendees.ATTENDEE_STATUS_ACCEPTED:
234                    extra |= ATTENDEE_STATUS_ACCEPTED_MASK;
235                    break;
236                case Attendees.ATTENDEE_STATUS_DECLINED:
237                    extra |= ATTENDEE_STATUS_DECLINED_MASK;
238                    break;
239                case Attendees.ATTENDEE_STATUS_TENTATIVE:
240                    extra |= ATTENDEE_STATUS_TENTATIVE_MASK;
241                    break;
242                default:
243                    Log.wtf(TAG,"Unknown attendee response " + response);
244                    extra |= ATTENDEE_STATUS_NONE_MASK;
245                    break;
246            }
247            return extra;
248        }
249    }
250
251    /**
252     * Pass to the ExtraLong parameter for EventType.CREATE_EVENT to create
253     * an all-day event
254     */
255    public static final long EXTRA_CREATE_ALL_DAY = 0x10;
256
257    /**
258     * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time
259     * can be ignored
260     */
261    public static final long EXTRA_GOTO_DATE = 1;
262    public static final long EXTRA_GOTO_TIME = 2;
263    public static final long EXTRA_GOTO_BACK_TO_PREVIOUS = 4;
264    public static final long EXTRA_GOTO_TODAY = 8;
265
266    public interface EventHandler {
267        long getSupportedEventTypes();
268        void handleEvent(EventInfo event);
269
270        /**
271         * This notifies the handler that the database has changed and it should
272         * update its view.
273         */
274        void eventsChanged();
275    }
276
277    /**
278     * Creates and/or returns an instance of CalendarController associated with
279     * the supplied context. It is best to pass in the current Activity.
280     *
281     * @param context The activity if at all possible.
282     */
283    public static CalendarController getInstance(Context context) {
284        synchronized (instances) {
285            CalendarController controller = null;
286            WeakReference<CalendarController> weakController = instances.get(context);
287            if (weakController != null) {
288                controller = weakController.get();
289            }
290
291            if (controller == null) {
292                controller = new CalendarController(context);
293                instances.put(context, new WeakReference(controller));
294            }
295            return controller;
296        }
297    }
298
299    /**
300     * Removes an instance when it is no longer needed. This should be called in
301     * an activity's onDestroy method.
302     *
303     * @param context The activity used to create the controller
304     */
305    public static void removeInstance(Context context) {
306        instances.remove(context);
307    }
308
309    private CalendarController(Context context) {
310        mContext = context;
311        mUpdateTimezone.run();
312        mTime.setToNow();
313        mDetailViewType = Utils.getSharedPreference(mContext,
314                GeneralPreferences.KEY_DETAILED_VIEW,
315                GeneralPreferences.DEFAULT_DETAILED_VIEW);
316    }
317
318    public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis,
319            long endMillis, int x, int y, long selectedMillis) {
320        // TODO: pass the real allDay status or at least a status that says we don't know the
321        // status and have the receiver query the data.
322        // The current use of this method for VIEW_EVENT is by the day view to show an EventInfo
323        // so currently the missing allDay status has no effect.
324        sendEventRelatedEventWithExtra(sender, eventType, eventId, startMillis, endMillis, x, y,
325                EventInfo.buildViewExtraLong(Attendees.ATTENDEE_STATUS_NONE, false),
326                selectedMillis);
327    }
328
329    /**
330     * Helper for sending New/View/Edit/Delete events
331     *
332     * @param sender object of the caller
333     * @param eventType one of {@link EventType}
334     * @param eventId event id
335     * @param startMillis start time
336     * @param endMillis end time
337     * @param x x coordinate in the activity space
338     * @param y y coordinate in the activity space
339     * @param extraLong default response value for the "simple event view" and all day indication.
340     *        Use Attendees.ATTENDEE_STATUS_NONE for no response.
341     * @param selectedMillis The time to specify as selected
342     */
343    public void sendEventRelatedEventWithExtra(Object sender, long eventType, long eventId,
344            long startMillis, long endMillis, int x, int y, long extraLong, long selectedMillis) {
345        sendEventRelatedEventWithExtraWithTitleWithCalendarId(sender, eventType, eventId,
346            startMillis, endMillis, x, y, extraLong, selectedMillis, null, -1);
347    }
348
349    /**
350     * Helper for sending New/View/Edit/Delete events
351     *
352     * @param sender object of the caller
353     * @param eventType one of {@link EventType}
354     * @param eventId event id
355     * @param startMillis start time
356     * @param endMillis end time
357     * @param x x coordinate in the activity space
358     * @param y y coordinate in the activity space
359     * @param extraLong default response value for the "simple event view" and all day indication.
360     *        Use Attendees.ATTENDEE_STATUS_NONE for no response.
361     * @param selectedMillis The time to specify as selected
362     * @param title The title of the event
363     * @param calendarId The id of the calendar which the event belongs to
364     */
365    public void sendEventRelatedEventWithExtraWithTitleWithCalendarId(Object sender, long eventType,
366          long eventId, long startMillis, long endMillis, int x, int y, long extraLong,
367          long selectedMillis, String title, long calendarId) {
368        EventInfo info = new EventInfo();
369        info.eventType = eventType;
370        if (eventType == EventType.EDIT_EVENT || eventType == EventType.VIEW_EVENT_DETAILS) {
371            info.viewType = ViewType.CURRENT;
372        }
373
374        info.id = eventId;
375        info.startTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone));
376        info.startTime.set(startMillis);
377        if (selectedMillis != -1) {
378            info.selectedTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone));
379            info.selectedTime.set(selectedMillis);
380        } else {
381            info.selectedTime = info.startTime;
382        }
383        info.endTime = new Time(Utils.getTimeZone(mContext, mUpdateTimezone));
384        info.endTime.set(endMillis);
385        info.x = x;
386        info.y = y;
387        info.extraLong = extraLong;
388        info.eventTitle = title;
389        info.calendarId = calendarId;
390        this.sendEvent(sender, info);
391    }
392    /**
393     * Helper for sending non-calendar-event events
394     *
395     * @param sender object of the caller
396     * @param eventType one of {@link EventType}
397     * @param start start time
398     * @param end end time
399     * @param eventId event id
400     * @param viewType {@link ViewType}
401     */
402    public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId,
403            int viewType) {
404        sendEvent(sender, eventType, start, end, start, eventId, viewType, EXTRA_GOTO_TIME, null,
405                null);
406    }
407
408    /**
409     * sendEvent() variant with extraLong, search query, and search component name.
410     */
411    public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId,
412            int viewType, long extraLong, String query, ComponentName componentName) {
413        sendEvent(sender, eventType, start, end, start, eventId, viewType, extraLong, query,
414                componentName);
415    }
416
417    public void sendEvent(Object sender, long eventType, Time start, Time end, Time selected,
418            long eventId, int viewType, long extraLong, String query, ComponentName componentName) {
419        EventInfo info = new EventInfo();
420        info.eventType = eventType;
421        info.startTime = start;
422        info.selectedTime = selected;
423        info.endTime = end;
424        info.id = eventId;
425        info.viewType = viewType;
426        info.query = query;
427        info.componentName = componentName;
428        info.extraLong = extraLong;
429        this.sendEvent(sender, info);
430    }
431
432    public void sendEvent(Object sender, final EventInfo event) {
433        // TODO Throw exception on invalid events
434
435        if (DEBUG) {
436            Log.d(TAG, eventInfoToString(event));
437        }
438
439        Long filteredTypes = filters.get(sender);
440        if (filteredTypes != null && (filteredTypes.longValue() & event.eventType) != 0) {
441            // Suppress event per filter
442            if (DEBUG) {
443                Log.d(TAG, "Event suppressed");
444            }
445            return;
446        }
447
448        mPreviousViewType = mViewType;
449
450        // Fix up view if not specified
451        if (event.viewType == ViewType.DETAIL) {
452            event.viewType = mDetailViewType;
453            mViewType = mDetailViewType;
454        } else if (event.viewType == ViewType.CURRENT) {
455            event.viewType = mViewType;
456        } else if (event.viewType != ViewType.EDIT) {
457            mViewType = event.viewType;
458
459            if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY
460                    || (Utils.getAllowWeekForDetailView() && event.viewType == ViewType.WEEK)) {
461                mDetailViewType = mViewType;
462            }
463        }
464
465        if (DEBUG) {
466            Log.d(TAG, "vvvvvvvvvvvvvvv");
467            Log.d(TAG, "Start  " + (event.startTime == null ? "null" : event.startTime.toString()));
468            Log.d(TAG, "End    " + (event.endTime == null ? "null" : event.endTime.toString()));
469            Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString()));
470            Log.d(TAG, "mTime  " + (mTime == null ? "null" : mTime.toString()));
471        }
472
473        long startMillis = 0;
474        if (event.startTime != null) {
475            startMillis = event.startTime.toMillis(false);
476        }
477
478        // Set mTime if selectedTime is set
479        if (event.selectedTime != null && event.selectedTime.toMillis(false) != 0) {
480            mTime.set(event.selectedTime);
481        } else {
482            if (startMillis != 0) {
483                // selectedTime is not set so set mTime to startTime iff it is not
484                // within start and end times
485                long mtimeMillis = mTime.toMillis(false);
486                if (mtimeMillis < startMillis
487                        || (event.endTime != null && mtimeMillis > event.endTime.toMillis(false))) {
488                    mTime.set(event.startTime);
489                }
490            }
491            event.selectedTime = mTime;
492        }
493        // Store the formatting flags if this is an update to the title
494        if (event.eventType == EventType.UPDATE_TITLE) {
495            mDateFlags = event.extraLong;
496        }
497
498        // Fix up start time if not specified
499        if (startMillis == 0) {
500            event.startTime = mTime;
501        }
502        if (DEBUG) {
503            Log.d(TAG, "Start  " + (event.startTime == null ? "null" : event.startTime.toString()));
504            Log.d(TAG, "End    " + (event.endTime == null ? "null" : event.endTime.toString()));
505            Log.d(TAG, "Select " + (event.selectedTime == null ? "null" : event.selectedTime.toString()));
506            Log.d(TAG, "mTime  " + (mTime == null ? "null" : mTime.toString()));
507            Log.d(TAG, "^^^^^^^^^^^^^^^");
508        }
509
510        // Store the eventId if we're entering edit event
511        if ((event.eventType
512                & (EventType.CREATE_EVENT | EventType.EDIT_EVENT | EventType.VIEW_EVENT_DETAILS))
513                != 0) {
514            if (event.id > 0) {
515                mEventId = event.id;
516            } else {
517                mEventId = -1;
518            }
519        }
520
521        boolean handled = false;
522        synchronized (this) {
523            mDispatchInProgressCounter ++;
524
525            if (DEBUG) {
526                Log.d(TAG, "sendEvent: Dispatching to " + eventHandlers.size() + " handlers");
527            }
528            // Dispatch to event handler(s)
529            if (mFirstEventHandler != null) {
530                // Handle the 'first' one before handling the others
531                EventHandler handler = mFirstEventHandler.second;
532                if (handler != null && (handler.getSupportedEventTypes() & event.eventType) != 0
533                        && !mToBeRemovedEventHandlers.contains(mFirstEventHandler.first)) {
534                    handler.handleEvent(event);
535                    handled = true;
536                }
537            }
538            for (Iterator<Entry<Integer, EventHandler>> handlers =
539                    eventHandlers.entrySet().iterator(); handlers.hasNext();) {
540                Entry<Integer, EventHandler> entry = handlers.next();
541                int key = entry.getKey();
542                if (mFirstEventHandler != null && key == mFirstEventHandler.first) {
543                    // If this was the 'first' handler it was already handled
544                    continue;
545                }
546                EventHandler eventHandler = entry.getValue();
547                if (eventHandler != null
548                        && (eventHandler.getSupportedEventTypes() & event.eventType) != 0) {
549                    if (mToBeRemovedEventHandlers.contains(key)) {
550                        continue;
551                    }
552                    eventHandler.handleEvent(event);
553                    handled = true;
554                }
555            }
556
557            mDispatchInProgressCounter --;
558
559            if (mDispatchInProgressCounter == 0) {
560
561                // Deregister removed handlers
562                if (mToBeRemovedEventHandlers.size() > 0) {
563                    for (Integer zombie : mToBeRemovedEventHandlers) {
564                        eventHandlers.remove(zombie);
565                        if (mFirstEventHandler != null && zombie.equals(mFirstEventHandler.first)) {
566                            mFirstEventHandler = null;
567                        }
568                    }
569                    mToBeRemovedEventHandlers.clear();
570                }
571                // Add new handlers
572                if (mToBeAddedFirstEventHandler != null) {
573                    mFirstEventHandler = mToBeAddedFirstEventHandler;
574                    mToBeAddedFirstEventHandler = null;
575                }
576                if (mToBeAddedEventHandlers.size() > 0) {
577                    for (Entry<Integer, EventHandler> food : mToBeAddedEventHandlers.entrySet()) {
578                        eventHandlers.put(food.getKey(), food.getValue());
579                    }
580                }
581            }
582        }
583
584        if (!handled) {
585            // Launch Settings
586            if (event.eventType == EventType.LAUNCH_SETTINGS) {
587                launchSettings();
588                return;
589            }
590
591            // Launch Calendar Visible Selector
592            if (event.eventType == EventType.LAUNCH_SELECT_VISIBLE_CALENDARS) {
593                launchSelectVisibleCalendars();
594                return;
595            }
596
597            // Create/View/Edit/Delete Event
598            long endTime = (event.endTime == null) ? -1 : event.endTime.toMillis(false);
599            if (event.eventType == EventType.CREATE_EVENT) {
600                launchCreateEvent(event.startTime.toMillis(false), endTime,
601                        event.extraLong == EXTRA_CREATE_ALL_DAY, event.eventTitle,
602                        event.calendarId);
603                return;
604            } else if (event.eventType == EventType.VIEW_EVENT) {
605                launchViewEvent(event.id, event.startTime.toMillis(false), endTime,
606                        event.getResponse());
607                return;
608            } else if (event.eventType == EventType.EDIT_EVENT) {
609                launchEditEvent(event.id, event.startTime.toMillis(false), endTime, true);
610                return;
611            } else if (event.eventType == EventType.VIEW_EVENT_DETAILS) {
612                launchEditEvent(event.id, event.startTime.toMillis(false), endTime, false);
613                return;
614            } else if (event.eventType == EventType.DELETE_EVENT) {
615                launchDeleteEvent(event.id, event.startTime.toMillis(false), endTime);
616                return;
617            } else if (event.eventType == EventType.SEARCH) {
618                launchSearch(event.id, event.query, event.componentName);
619                return;
620            }
621        }
622    }
623
624    /**
625     * Adds or updates an event handler. This uses a LinkedHashMap so that we can
626     * replace fragments based on the view id they are being expanded into.
627     *
628     * @param key The view id or placeholder for this handler
629     * @param eventHandler Typically a fragment or activity in the calendar app
630     */
631    public void registerEventHandler(int key, EventHandler eventHandler) {
632        synchronized (this) {
633            if (mDispatchInProgressCounter > 0) {
634                mToBeAddedEventHandlers.put(key, eventHandler);
635            } else {
636                eventHandlers.put(key, eventHandler);
637            }
638        }
639    }
640
641    public void registerFirstEventHandler(int key, EventHandler eventHandler) {
642        synchronized (this) {
643            registerEventHandler(key, eventHandler);
644            if (mDispatchInProgressCounter > 0) {
645                mToBeAddedFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler);
646            } else {
647                mFirstEventHandler = new Pair<Integer, EventHandler>(key, eventHandler);
648            }
649        }
650    }
651
652    public void deregisterEventHandler(Integer key) {
653        synchronized (this) {
654            if (mDispatchInProgressCounter > 0) {
655                // To avoid ConcurrencyException, stash away the event handler for now.
656                mToBeRemovedEventHandlers.add(key);
657            } else {
658                eventHandlers.remove(key);
659                if (mFirstEventHandler != null && mFirstEventHandler.first == key) {
660                    mFirstEventHandler = null;
661                }
662            }
663        }
664    }
665
666    public void deregisterAllEventHandlers() {
667        synchronized (this) {
668            if (mDispatchInProgressCounter > 0) {
669                // To avoid ConcurrencyException, stash away the event handler for now.
670                mToBeRemovedEventHandlers.addAll(eventHandlers.keySet());
671            } else {
672                eventHandlers.clear();
673                mFirstEventHandler = null;
674            }
675        }
676    }
677
678    // FRAG_TODO doesn't work yet
679    public void filterBroadcasts(Object sender, long eventTypes) {
680        filters.put(sender, eventTypes);
681    }
682
683    /**
684     * @return the time that this controller is currently pointed at
685     */
686    public long getTime() {
687        return mTime.toMillis(false);
688    }
689
690    /**
691     * @return the last set of date flags sent with
692     *         {@link EventType#UPDATE_TITLE}
693     */
694    public long getDateFlags() {
695        return mDateFlags;
696    }
697
698    /**
699     * Set the time this controller is currently pointed at
700     *
701     * @param millisTime Time since epoch in millis
702     */
703    public void setTime(long millisTime) {
704        mTime.set(millisTime);
705    }
706
707    /**
708     * @return the last event ID the edit view was launched with
709     */
710    public long getEventId() {
711        return mEventId;
712    }
713
714    public int getViewType() {
715        return mViewType;
716    }
717
718    public int getPreviousViewType() {
719        return mPreviousViewType;
720    }
721
722    private void launchSelectVisibleCalendars() {
723        Intent intent = new Intent(Intent.ACTION_VIEW);
724        intent.setClass(mContext, SelectVisibleCalendarsActivity.class);
725        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
726        mContext.startActivity(intent);
727    }
728
729    private void launchSettings() {
730        Intent intent = new Intent(Intent.ACTION_VIEW);
731        intent.setClass(mContext, CalendarSettingsActivity.class);
732        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
733        mContext.startActivity(intent);
734    }
735
736    private void launchCreateEvent(long startMillis, long endMillis, boolean allDayEvent,
737            String title, long calendarId) {
738        Intent intent = generateCreateEventIntent(startMillis, endMillis, allDayEvent, title,
739            calendarId);
740        mEventId = -1;
741        mContext.startActivity(intent);
742    }
743
744    public Intent generateCreateEventIntent(long startMillis, long endMillis,
745        boolean allDayEvent, String title, long calendarId) {
746        Intent intent = new Intent(Intent.ACTION_VIEW);
747        intent.setClass(mContext, EditEventActivity.class);
748        intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis);
749        intent.putExtra(EXTRA_EVENT_END_TIME, endMillis);
750        intent.putExtra(EXTRA_EVENT_ALL_DAY, allDayEvent);
751        intent.putExtra(Events.CALENDAR_ID, calendarId);
752        intent.putExtra(Events.TITLE, title);
753        return intent;
754    }
755
756    public void launchViewEvent(long eventId, long startMillis, long endMillis, int response) {
757        Intent intent = new Intent(Intent.ACTION_VIEW);
758        Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
759        intent.setData(eventUri);
760        intent.setClass(mContext, AllInOneActivity.class);
761        intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis);
762        intent.putExtra(EXTRA_EVENT_END_TIME, endMillis);
763        intent.putExtra(ATTENDEE_STATUS, response);
764        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
765        mContext.startActivity(intent);
766    }
767
768    private void launchEditEvent(long eventId, long startMillis, long endMillis, boolean edit) {
769        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
770        Intent intent = new Intent(Intent.ACTION_EDIT, uri);
771        intent.putExtra(EXTRA_EVENT_BEGIN_TIME, startMillis);
772        intent.putExtra(EXTRA_EVENT_END_TIME, endMillis);
773        intent.setClass(mContext, EditEventActivity.class);
774        intent.putExtra(EVENT_EDIT_ON_LAUNCH, edit);
775        mEventId = eventId;
776        mContext.startActivity(intent);
777    }
778
779//    private void launchAlerts() {
780//        Intent intent = new Intent();
781//        intent.setClass(mContext, AlertActivity.class);
782//        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
783//        mContext.startActivity(intent);
784//    }
785
786    private void launchDeleteEvent(long eventId, long startMillis, long endMillis) {
787        launchDeleteEventAndFinish(null, eventId, startMillis, endMillis, -1);
788    }
789
790    private void launchDeleteEventAndFinish(Activity parentActivity, long eventId, long startMillis,
791            long endMillis, int deleteWhich) {
792        DeleteEventHelper deleteEventHelper = new DeleteEventHelper(mContext, parentActivity,
793                parentActivity != null /* exit when done */);
794        deleteEventHelper.delete(startMillis, endMillis, eventId, deleteWhich);
795    }
796
797    private void launchSearch(long eventId, String query, ComponentName componentName) {
798        final SearchManager searchManager =
799                (SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE);
800        final SearchableInfo searchableInfo = searchManager.getSearchableInfo(componentName);
801        final Intent intent = new Intent(Intent.ACTION_SEARCH);
802        intent.putExtra(SearchManager.QUERY, query);
803        intent.setComponent(searchableInfo.getSearchActivity());
804        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
805        mContext.startActivity(intent);
806    }
807
808    /**
809     * Performs a manual refresh of calendars in all known accounts.
810     */
811    public void refreshCalendars() {
812        Account[] accounts = AccountManager.get(mContext).getAccounts();
813        Log.d(TAG, "Refreshing " + accounts.length + " accounts");
814
815        String authority = Calendars.CONTENT_URI.getAuthority();
816        for (int i = 0; i < accounts.length; i++) {
817            if (Log.isLoggable(TAG, Log.DEBUG)) {
818                Log.d(TAG, "Refreshing calendars for: " + accounts[i]);
819            }
820            Bundle extras = new Bundle();
821            extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
822            ContentResolver.requestSync(accounts[i], authority, extras);
823        }
824    }
825
826    // Forces the viewType. Should only be used for initialization.
827    public void setViewType(int viewType) {
828        mViewType = viewType;
829    }
830
831    // Sets the eventId. Should only be used for initialization.
832    public void setEventId(long eventId) {
833        mEventId = eventId;
834    }
835
836    private String eventInfoToString(EventInfo eventInfo) {
837        String tmp = "Unknown";
838
839        StringBuilder builder = new StringBuilder();
840        if ((eventInfo.eventType & EventType.GO_TO) != 0) {
841            tmp = "Go to time/event";
842        } else if ((eventInfo.eventType & EventType.CREATE_EVENT) != 0) {
843            tmp = "New event";
844        } else if ((eventInfo.eventType & EventType.VIEW_EVENT) != 0) {
845            tmp = "View event";
846        } else if ((eventInfo.eventType & EventType.VIEW_EVENT_DETAILS) != 0) {
847            tmp = "View details";
848        } else if ((eventInfo.eventType & EventType.EDIT_EVENT) != 0) {
849            tmp = "Edit event";
850        } else if ((eventInfo.eventType & EventType.DELETE_EVENT) != 0) {
851            tmp = "Delete event";
852        } else if ((eventInfo.eventType & EventType.LAUNCH_SELECT_VISIBLE_CALENDARS) != 0) {
853            tmp = "Launch select visible calendars";
854        } else if ((eventInfo.eventType & EventType.LAUNCH_SETTINGS) != 0) {
855            tmp = "Launch settings";
856        } else if ((eventInfo.eventType & EventType.EVENTS_CHANGED) != 0) {
857            tmp = "Refresh events";
858        } else if ((eventInfo.eventType & EventType.SEARCH) != 0) {
859            tmp = "Search";
860        } else if ((eventInfo.eventType & EventType.USER_HOME) != 0) {
861            tmp = "Gone home";
862        } else if ((eventInfo.eventType & EventType.UPDATE_TITLE) != 0) {
863            tmp = "Update title";
864        }
865        builder.append(tmp);
866        builder.append(": id=");
867        builder.append(eventInfo.id);
868        builder.append(", selected=");
869        builder.append(eventInfo.selectedTime);
870        builder.append(", start=");
871        builder.append(eventInfo.startTime);
872        builder.append(", end=");
873        builder.append(eventInfo.endTime);
874        builder.append(", viewType=");
875        builder.append(eventInfo.viewType);
876        builder.append(", x=");
877        builder.append(eventInfo.x);
878        builder.append(", y=");
879        builder.append(eventInfo.y);
880        return builder.toString();
881    }
882}
883