CalendarController.java revision eaafa2b48be7194a61754604ae37b3d62e9118d8
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.Calendar.EVENT_BEGIN_TIME;
20import static android.provider.Calendar.EVENT_END_TIME;
21
22import com.android.calendar.event.EditEventActivity;
23
24import android.accounts.Account;
25import android.app.Activity;
26import android.app.SearchManager;
27import android.app.SearchableInfo;
28import android.content.ComponentName;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.Context;
32import android.content.Intent;
33import android.database.Cursor;
34import android.net.Uri;
35import android.os.AsyncTask;
36import android.os.Bundle;
37import android.provider.Calendar.Calendars;
38import android.provider.Calendar.Events;
39import android.text.TextUtils;
40import android.text.format.Time;
41import android.util.Log;
42
43import java.util.Iterator;
44import java.util.LinkedHashMap;
45import java.util.LinkedList;
46import java.util.Map.Entry;
47import java.util.WeakHashMap;
48
49public class CalendarController {
50    private static final boolean DEBUG = true;
51    private static final String TAG = "CalendarController";
52    private static final String REFRESH_SELECTION = Calendars.SYNC_EVENTS + "=?";
53    private static final String[] REFRESH_ARGS = new String[] { "1" };
54    private static final String REFRESH_ORDER = Calendars._SYNC_ACCOUNT + ","
55            + Calendars._SYNC_ACCOUNT_TYPE;
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    public static final int ATTENDEE_NO_RESPONSE = -1;
65
66    private Context mContext;
67
68    // This uses a LinkedHashMap so that we can replace fragments based on the
69    // view id they are being expanded into since we can't guarantee a reference
70    // to the handler will be findable
71    private LinkedHashMap<Integer,EventHandler> eventHandlers =
72            new LinkedHashMap<Integer,EventHandler>(5);
73    private LinkedList<Integer> mToBeRemovedEventHandlers = new LinkedList<Integer>();
74    private LinkedHashMap<Integer, EventHandler> mToBeAddedEventHandlers = new LinkedHashMap<
75            Integer, EventHandler>();
76    private boolean mDispatchInProgress;
77
78    private static WeakHashMap<Context, CalendarController> instances =
79        new WeakHashMap<Context, CalendarController>();
80
81    private 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 Time mTime = new Time();
88
89    private AsyncQueryService mService;
90
91    /**
92     * One of the event types that are sent to or from the controller
93     */
94    public interface EventType {
95        final long CREATE_EVENT = 1L;
96
97        // Simple view of an event
98        final long VIEW_EVENT = 1L << 1;
99
100        // Full detail view in read only mode
101        final long VIEW_EVENT_DETAILS = 1L << 2;
102
103        // full detail view in edit mode
104        final long EDIT_EVENT = 1L << 3;
105
106        final long DELETE_EVENT = 1L << 4;
107
108        final long GO_TO = 1L << 5;
109
110        final long LAUNCH_SETTINGS = 1L << 6;
111
112        final long EVENTS_CHANGED = 1L << 7;
113
114        final long SEARCH = 1L << 8;
115
116        // User has pressed the home key
117        final long USER_HOME = 1L << 9;
118
119        // date range has changed, update the title
120        final long UPDATE_TITLE = 1L << 10;
121    }
122
123    /**
124     * One of the Agenda/Day/Week/Month view types
125     */
126    public interface ViewType {
127        final int DETAIL = -1;
128        final int CURRENT = 0;
129        final int AGENDA = 1;
130        final int DAY = 2;
131        final int WEEK = 3;
132        final int MONTH = 4;
133        final int EDIT = 5;
134    }
135
136    public static class EventInfo {
137        public long eventType; // one of the EventType
138        public int viewType; // one of the ViewType
139        public long id; // event id
140        public Time selectedTime; // the selected time in focus
141        public Time startTime; // start of a range of time.
142        public Time endTime; // end of a range of time.
143        public int x; // x coordinate in the activity space
144        public int y; // y coordinate in the activity space
145        public String query; // query for a user search
146        public ComponentName componentName;  // used in combination with query
147
148        /**
149         * For EventType.VIEW_EVENT:
150         * It is the default attendee response.
151         * Set to {@link #ATTENDEE_NO_RESPONSE}, Calendar.ATTENDEE_STATUS_ACCEPTED,
152         * Calendar.ATTENDEE_STATUS_DECLINED, or Calendar.ATTENDEE_STATUS_TENTATIVE.
153         * <p>
154         * For EventType.GO_TO:
155         * Set to {@link #EXTRA_GOTO_TIME} to go to the specified date/time.
156         * Set to {@link #EXTRA_GOTO_DATE} to consider the date but ignore the time.
157         */
158        public long extraLong;
159    }
160
161    /**
162     * Pass to the ExtraLong parameter for EventType.GO_TO to signal the time
163     * can be ignored
164     */
165    public static final long EXTRA_GOTO_DATE = 1;
166    public static final long EXTRA_GOTO_TIME = -1;
167
168    public interface EventHandler {
169        long getSupportedEventTypes();
170        void handleEvent(EventInfo event);
171
172        /**
173         * This notifies the handler that the database has changed and it should
174         * update its view.
175         */
176        void eventsChanged();
177    }
178
179    /**
180     * Creates and/or returns an instance of CalendarController associated with
181     * the supplied context. It is best to pass in the current Activity.
182     *
183     * @param context The activity if at all possible.
184     */
185    public static CalendarController getInstance(Context context) {
186        synchronized (instances) {
187            CalendarController controller = instances.get(context);
188            if (controller == null) {
189                controller = new CalendarController(context);
190                instances.put(context, controller);
191            }
192            return controller;
193        }
194    }
195
196    /**
197     * Removes an instance when it is no longer needed. This should be called in
198     * an activity's onDestroy method.
199     *
200     * @param context The activity used to create the controller
201     */
202    public static void removeInstance(Context context) {
203        instances.remove(context);
204    }
205
206    private CalendarController(Context context) {
207        mContext = context;
208        mTime.setToNow();
209        mDetailViewType = Utils.getSharedPreference(mContext,
210                GeneralPreferences.KEY_DETAILED_VIEW,
211                GeneralPreferences.DEFAULT_DETAILED_VIEW);
212        mService = new AsyncQueryService(context) {
213            @Override
214            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
215                new RefreshInBackground().execute(cursor);
216            }
217        };
218    }
219
220    public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis,
221            long endMillis, int x, int y) {
222        sendEventRelatedEvent( sender,  eventType,  eventId,  startMillis,
223                 endMillis,  x,  y, CalendarController.ATTENDEE_NO_RESPONSE);
224    }
225
226    /**
227     * Helper for sending New/View/Edit/Delete events
228     *
229     * @param sender object of the caller
230     * @param eventType one of {@link EventType}
231     * @param eventId event id
232     * @param startMillis start time
233     * @param endMillis end time
234     * @param x x coordinate in the activity space
235     * @param y y coordinate in the activity space
236     * @param extraLong default response value for the "simple event view". Use
237     *            CalendarController.ATTENDEE_NO_RESPONSE for no response.
238     */
239    public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis,
240            long endMillis, int x, int y, long extraLong) {
241        EventInfo info = new EventInfo();
242        info.eventType = eventType;
243        if (eventType == EventType.EDIT_EVENT || eventType == EventType.VIEW_EVENT_DETAILS) {
244            info.viewType = ViewType.CURRENT;
245        }
246        info.id = eventId;
247        info.startTime = new Time();
248        info.startTime.set(startMillis);
249        info.selectedTime = info.startTime;
250        info.endTime = new Time();
251        info.endTime.set(endMillis);
252        info.x = x;
253        info.y = y;
254        info.extraLong = extraLong;
255        this.sendEvent(sender, info);
256    }
257
258    /**
259     * Helper for sending non-calendar-event events
260     *
261     * @param sender object of the caller
262     * @param eventType one of {@link EventType}
263     * @param start start time
264     * @param end end time
265     * @param eventId event id
266     * @param viewType {@link ViewType}
267     */
268    public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId,
269            int viewType) {
270        sendEvent(sender, eventType, start, end, eventId, viewType, EXTRA_GOTO_TIME, null, null);
271    }
272
273    /**
274     * sendEvent() variant with extraLong, search query, and search component name.
275     */
276    public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId,
277            int viewType, long extraLong, String query, ComponentName componentName) {
278        EventInfo info = new EventInfo();
279        info.eventType = eventType;
280        info.startTime = start;
281        info.selectedTime = start;
282        info.endTime = end;
283        info.id = eventId;
284        info.viewType = viewType;
285        info.query = query;
286        info.componentName = componentName;
287        info.extraLong = extraLong;
288        this.sendEvent(sender, info);
289    }
290
291    public void sendEvent(Object sender, final EventInfo event) {
292        // TODO Throw exception on invalid events
293
294        if (DEBUG) {
295            Log.d(TAG, eventInfoToString(event));
296        }
297
298        Long filteredTypes = filters.get(sender);
299        if (filteredTypes != null && (filteredTypes.longValue() & event.eventType) != 0) {
300            // Suppress event per filter
301            if (DEBUG) {
302                Log.d(TAG, "Event suppressed");
303            }
304            return;
305        }
306
307        mPreviousViewType = mViewType;
308
309        // Fix up view if not specified
310        if (event.viewType == ViewType.DETAIL) {
311            event.viewType = mDetailViewType;
312            mViewType = mDetailViewType;
313        } else if (event.viewType == ViewType.CURRENT) {
314            event.viewType = mViewType;
315        } else if (event.viewType != ViewType.EDIT){
316            mViewType = event.viewType;
317
318            if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY) {
319                mDetailViewType = mViewType;
320            }
321        }
322
323        // Fix up start time if not specified
324        if (event.startTime != null && event.startTime.toMillis(false) != 0) {
325            mTime.set(event.startTime);
326        }
327        event.startTime = mTime;
328
329        // Store the eventId if we're entering edit event
330        if ((event.eventType
331                & (EventType.CREATE_EVENT | EventType.EDIT_EVENT | EventType.VIEW_EVENT_DETAILS))
332                != 0) {
333            if (event.id > 0) {
334                mEventId = event.id;
335            } else {
336                mEventId = -1;
337            }
338        }
339
340        boolean handled = false;
341        synchronized (this) {
342            mDispatchInProgress = true;
343
344            if (DEBUG) {
345                Log.d(TAG, "sendEvent: Dispatching to " + eventHandlers.size() + " handlers");
346            }
347            // Dispatch to event handler(s)
348            for (Iterator<Entry<Integer, EventHandler>> handlers =
349                    eventHandlers.entrySet().iterator(); handlers.hasNext();) {
350                Entry<Integer, EventHandler> entry = handlers.next();
351                int key = entry.getKey();
352                EventHandler eventHandler = entry.getValue();
353                if (eventHandler != null
354                        && (eventHandler.getSupportedEventTypes() & event.eventType) != 0) {
355                    if (mToBeRemovedEventHandlers.contains(key)) {
356                        continue;
357                    }
358                    eventHandler.handleEvent(event);
359                    handled = true;
360                }
361            }
362
363            mDispatchInProgress = false;
364
365            // Deregister removed handlers
366            if (mToBeRemovedEventHandlers.size() > 0) {
367                for (Integer zombie : mToBeRemovedEventHandlers) {
368                    eventHandlers.remove(zombie);
369                }
370                mToBeRemovedEventHandlers.clear();
371            }
372            // Add new handlers
373            if (mToBeAddedEventHandlers.size() > 0) {
374                for (Entry<Integer, EventHandler> food : mToBeAddedEventHandlers.entrySet()) {
375                    eventHandlers.put(food.getKey(), food.getValue());
376                }
377            }
378        }
379
380        if (!handled) {
381            // Launch Settings
382            if (event.eventType == EventType.LAUNCH_SETTINGS) {
383                launchSettings();
384                return;
385            }
386
387            // Create/View/Edit/Delete Event
388            long endTime = (event.endTime == null) ? -1 : event.endTime.toMillis(false);
389            if (event.eventType == EventType.CREATE_EVENT) {
390                launchCreateEvent(event.startTime.toMillis(false), endTime);
391                return;
392            } else if (event.eventType == EventType.VIEW_EVENT) {
393                launchViewEvent(event.id, event.startTime.toMillis(false), endTime);
394                return;
395            } else if (event.eventType == EventType.EDIT_EVENT) {
396                launchEditEvent(event.id, event.startTime.toMillis(false), endTime, true);
397                return;
398            } else if (event.eventType == EventType.VIEW_EVENT_DETAILS) {
399                launchEditEvent(event.id, event.startTime.toMillis(false), endTime, false);
400                return;
401            } else if (event.eventType == EventType.DELETE_EVENT) {
402                launchDeleteEvent(event.id, event.startTime.toMillis(false), endTime);
403                return;
404            } else if (event.eventType == EventType.SEARCH) {
405                launchSearch(event.id, event.query, event.componentName);
406                return;
407            }
408        }
409    }
410
411    /**
412     * Adds or updates an event handler. This uses a LinkedHashMap so that we can
413     * replace fragments based on the view id they are being expanded into.
414     *
415     * @param key The view id or placeholder for this handler
416     * @param eventHandler Typically a fragment or activity in the calendar app
417     */
418    public void registerEventHandler(int key, EventHandler eventHandler) {
419        synchronized (this) {
420            if (mDispatchInProgress) {
421                mToBeAddedEventHandlers.put(key, eventHandler);
422            } else {
423                eventHandlers.put(key, eventHandler);
424            }
425        }
426    }
427
428    public void deregisterEventHandler(Integer key) {
429        synchronized (this) {
430            if (mDispatchInProgress) {
431                // To avoid ConcurrencyException, stash away the event handler for now.
432                mToBeRemovedEventHandlers.add(key);
433            } else {
434                eventHandlers.remove(key);
435            }
436        }
437    }
438
439    // FRAG_TODO doesn't work yet
440    public void filterBroadcasts(Object sender, long eventTypes) {
441        filters.put(sender, eventTypes);
442    }
443
444    /**
445     * @return the time that this controller is currently pointed at
446     */
447    public long getTime() {
448        return mTime.toMillis(false);
449    }
450
451    /**
452     * Set the time this controller is currently pointed at
453     *
454     * @param millisTime Time since epoch in millis
455     */
456    public void setTime(long millisTime) {
457        mTime.set(millisTime);
458    }
459
460    /**
461     * @return the last event ID the edit view was launched with
462     */
463    public long getEventId() {
464        return mEventId;
465    }
466
467    public int getViewType() {
468        return mViewType;
469    }
470
471    public int getPreviousViewType() {
472        return mPreviousViewType;
473    }
474
475    private void launchSettings() {
476        Intent intent = new Intent(Intent.ACTION_VIEW);
477        intent.setClassName(mContext, CalendarSettingsActivity.class.getName());
478        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
479        mContext.startActivity(intent);
480    }
481
482    private void launchCreateEvent(long startMillis, long endMillis) {
483        Intent intent = new Intent(Intent.ACTION_VIEW);
484        intent.setClassName(mContext, EditEventActivity.class.getName());
485        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
486        intent.putExtra(EVENT_END_TIME, endMillis);
487        mEventId = -1;
488        mContext.startActivity(intent);
489    }
490
491    private void launchViewEvent(long eventId, long startMillis, long endMillis) {
492        Intent intent = new Intent(Intent.ACTION_VIEW);
493        Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
494        intent.setData(eventUri);
495//        intent.setClassName(mContext, EventInfoActivity.class.getName());
496        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
497        intent.putExtra(EVENT_END_TIME, endMillis);
498        mContext.startActivity(intent);
499    }
500
501    private void launchEditEvent(long eventId, long startMillis, long endMillis, boolean edit) {
502        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
503        Intent intent = new Intent(Intent.ACTION_EDIT, uri);
504        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
505        intent.putExtra(EVENT_END_TIME, endMillis);
506        intent.setClass(mContext, EditEventActivity.class);
507        intent.putExtra(EVENT_EDIT_ON_LAUNCH, edit);
508        mEventId = eventId;
509        mContext.startActivity(intent);
510    }
511
512    private void launchDeleteEvent(long eventId, long startMillis, long endMillis) {
513        launchDeleteEventAndFinish(null, eventId, startMillis, endMillis, -1);
514    }
515
516    private void launchDeleteEventAndFinish(Activity parentActivity, long eventId, long startMillis,
517            long endMillis, int deleteWhich) {
518        DeleteEventHelper deleteEventHelper = new DeleteEventHelper(mContext, parentActivity,
519                parentActivity != null /* exit when done */);
520        deleteEventHelper.delete(startMillis, endMillis, eventId, deleteWhich);
521    }
522
523    private void launchSearch(long eventId, String query, ComponentName componentName) {
524        final SearchManager searchManager =
525                (SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE);
526        final SearchableInfo searchableInfo = searchManager.getSearchableInfo(componentName);
527        final Intent intent = new Intent(Intent.ACTION_SEARCH);
528        intent.putExtra(SearchManager.QUERY, query);
529        intent.setComponent(searchableInfo.getSearchActivity());
530        mContext.startActivity(intent);
531    }
532
533    public void refreshCalendars() {
534        Log.d(TAG, "RefreshCalendars starting");
535        // get the account, url, and current sync state
536        mService.startQuery(mService.getNextToken(), null, Calendars.CONTENT_URI,
537                new String[] {Calendars._ID, // 0
538                        Calendars._SYNC_ACCOUNT, // 1
539                        Calendars._SYNC_ACCOUNT_TYPE, // 2
540                        },
541                REFRESH_SELECTION, REFRESH_ARGS, REFRESH_ORDER);
542    }
543
544    // Forces the viewType. Should only be used for initialization.
545    public void setViewType(int viewType) {
546        mViewType = viewType;
547    }
548
549    // Sets the eventId. Should only be used for initialization.
550    public void setEventId(long eventId) {
551        mEventId = eventId;
552    }
553
554    private class RefreshInBackground extends AsyncTask<Cursor, Integer, Integer> {
555        /* (non-Javadoc)
556         * @see android.os.AsyncTask#doInBackground(Params[])
557         */
558        @Override
559        protected Integer doInBackground(Cursor... params) {
560            if (params.length != 1) {
561                return null;
562            }
563            Cursor cursor = params[0];
564            if (cursor == null) {
565                return null;
566            }
567
568            String previousAccount = null;
569            String previousType = null;
570            Log.d(TAG, "Refreshing " + cursor.getCount() + " calendars");
571            try {
572                while (cursor.moveToNext()) {
573                    Account account = null;
574                    String accountName = cursor.getString(1);
575                    String accountType = cursor.getString(2);
576                    // Only need to schedule one sync per account and they're
577                    // ordered by account,type
578                    if (TextUtils.equals(accountName, previousAccount) &&
579                            TextUtils.equals(accountType, previousType)) {
580                        continue;
581                    }
582                    previousAccount = accountName;
583                    previousType = accountType;
584                    account = new Account(accountName, accountType);
585                    scheduleSync(account, false /* two-way sync */, null);
586                }
587            } finally {
588                cursor.close();
589            }
590            return null;
591        }
592
593        /**
594         * Schedule a calendar sync for the account.
595         * @param account the account for which to schedule a sync
596         * @param uploadChangesOnly if set, specify that the sync should only send
597         *   up local changes.  This is typically used for a local sync, a user override of
598         *   too many deletions, or a sync after a calendar is unselected.
599         * @param url the url feed for the calendar to sync (may be null, in which case a poll of
600         *   all feeds is done.)
601         */
602        void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
603            Bundle extras = new Bundle();
604            if (uploadChangesOnly) {
605                extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
606            }
607            if (url != null) {
608                extras.putString("feed", url);
609                extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
610            }
611            ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
612        }
613    }
614
615    private String eventInfoToString(EventInfo eventInfo) {
616        String tmp = "Unknown";
617
618        StringBuilder builder = new StringBuilder();
619        if ((eventInfo.eventType & EventType.GO_TO) != 0) {
620            tmp = "Go to time/event";
621        } else if ((eventInfo.eventType & EventType.CREATE_EVENT) != 0) {
622            tmp = "New event";
623        } else if ((eventInfo.eventType & EventType.VIEW_EVENT) != 0) {
624            tmp = "View event";
625        } else if ((eventInfo.eventType & EventType.EDIT_EVENT) != 0) {
626            tmp = "Edit event";
627        } else if ((eventInfo.eventType & EventType.DELETE_EVENT) != 0) {
628            tmp = "Delete event";
629        } else if ((eventInfo.eventType & EventType.LAUNCH_SETTINGS) != 0) {
630            tmp = "Launch settings";
631        } else if ((eventInfo.eventType & EventType.EVENTS_CHANGED) != 0) {
632            tmp = "Refresh events";
633        } else if ((eventInfo.eventType & EventType.SEARCH) != 0) {
634            tmp = "Search";
635        } else if ((eventInfo.eventType & EventType.VIEW_EVENT_DETAILS) != 0) {
636            tmp = "View details";
637        }
638        builder.append(tmp);
639        builder.append(": id=");
640        builder.append(eventInfo.id);
641        builder.append(", selected=");
642        builder.append(eventInfo.selectedTime);
643        builder.append(", start=");
644        builder.append(eventInfo.startTime);
645        builder.append(", end=");
646        builder.append(eventInfo.endTime);
647        builder.append(", viewType=");
648        builder.append(eventInfo.viewType);
649        builder.append(", x=");
650        builder.append(eventInfo.x);
651        builder.append(", y=");
652        builder.append(eventInfo.y);
653        return builder.toString();
654    }
655}
656