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