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