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