CalendarController.java 20.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.calendar;

19 20 21
import static android.provider.Calendar.EVENT_BEGIN_TIME;
import static android.provider.Calendar.EVENT_END_TIME;

Erik's avatar
Erik committed
22
import android.accounts.Account;
Michael Chan's avatar
Michael Chan committed
23
import android.app.Activity;
Erik's avatar
Erik committed
24
import android.content.ContentResolver;
25
import android.content.ContentUris;
26
import android.content.Context;
27
import android.content.Intent;
Erik's avatar
Erik committed
28
import android.database.Cursor;
29
import android.net.Uri;
Erik's avatar
Erik committed
30
import android.os.AsyncTask;
Erik's avatar
Erik committed
31 32
import android.os.Bundle;
import android.provider.Calendar.Calendars;
33
import android.provider.Calendar.Events;
Erik's avatar
Erik committed
34
import android.text.TextUtils;
35
import android.text.format.Time;
Michael Chan's avatar
Michael Chan committed
36 37
import android.util.Log;

38 39
import java.util.Iterator;
import java.util.LinkedHashMap;
40
import java.util.LinkedList;
41
import java.util.Map.Entry;
Michael Chan's avatar
Michael Chan committed
42 43
import java.util.WeakHashMap;

Erik's avatar
Erik committed
44
public class CalendarController {
45
    private static final boolean DEBUG = true;
Michael Chan's avatar
Michael Chan committed
46
    private static final String TAG = "CalendarController";
Erik's avatar
Erik committed
47 48 49
    private static final String REFRESH_SELECTION = Calendars.SYNC_EVENTS + "=?";
    private static final String[] REFRESH_ARGS = new String[] { "1" };
    private static final String REFRESH_ORDER = Calendars._SYNC_ACCOUNT + ","
Erik's avatar
Erik committed
50
            + Calendars._SYNC_ACCOUNT_TYPE;
Michael Chan's avatar
Michael Chan committed
51

52 53
    private Context mContext;

54 55 56 57 58 59
    // This uses a LinkedHashMap so that we can replace fragments based on the
    // view id they are being expanded into since we can't guarantee a reference
    // to the handler will be findable
    private LinkedHashMap<Integer,EventHandler> eventHandlers =
            new LinkedHashMap<Integer,EventHandler>(5);
    private LinkedList<Integer> mToBeRemovedEventHandlers = new LinkedList<Integer>();
60
    private boolean mDispatchInProgress;
Michael Chan's avatar
Michael Chan committed
61

62 63 64
    private static WeakHashMap<Context, CalendarController> instances =
        new WeakHashMap<Context, CalendarController>();

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 71
    private Time mTime = new Time();

Erik's avatar
Erik committed
72 73
    private AsyncQueryService mService;

74
    /**
Michael Chan's avatar
Michael Chan committed
75
     * One of the event types that are sent to or from the controller
76
     */
Erik's avatar
Erik committed
77
    public interface EventType {
78
        final long CREATE_EVENT = 1L;
Michael Chan's avatar
Michael Chan committed
79 80 81 82
        final long VIEW_EVENT = 1L << 1;
        final long EDIT_EVENT = 1L << 2;
        final long DELETE_EVENT = 1L << 3;

83
        final long GO_TO = 1L << 4;
Michael Chan's avatar
Michael Chan committed
84

85 86
        final long LAUNCH_MANAGE_CALENDARS = 1L << 5;
        final long LAUNCH_SETTINGS = 1L << 6;
Erik's avatar
Erik committed
87 88

        final long EVENTS_CHANGED = 1L << 7;
89 90

        final long SEARCH = 1L << 8;
Michael Chan's avatar
Michael Chan committed
91
    }
92 93

    /**
Michael Chan's avatar
Michael Chan committed
94
     * One of the Agenda/Day/Week/Month view types
95
     */
Erik's avatar
Erik committed
96
    public interface ViewType {
97
        final int DETAIL = -1;
98 99 100 101 102
        final int CURRENT = 0;
        final int AGENDA = 1;
        final int DAY = 2;
        final int WEEK = 3;
        final int MONTH = 4;
103
        final int EDIT = 5;
Michael Chan's avatar
Michael Chan committed
104 105
    }

Erik's avatar
Erik committed
106
    public static class EventInfo {
107 108 109 110 111 112 113 114 115
        public long eventType; // one of the EventType
        public int viewType; // one of the ViewType
        public long id; // event id
        public Time selectedTime; // the selected time in focus
        public Time startTime; // start of a range of time.
        public Time endTime; // end of a range of time.
        public int x; // x coordinate in the activity space
        public int y; // y coordinate in the activity space
        public String query; // query for a user search
Michael Chan's avatar
Michael Chan committed
116 117
    }

118
    // FRAG_TODO remove unneeded api's
Erik's avatar
Erik committed
119
    public interface EventHandler {
Michael Chan's avatar
Michael Chan committed
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
        long getSupportedEventTypes();
        void handleEvent(EventInfo event);

        /**
         * Returns the time in millis of the selected event in this view.
         * @return the selected time in UTC milliseconds.
         */
        long getSelectedTime();

        /**
         * Changes the view to include the given time.
         * @param time the desired time to view.
         * @animate enable animation
         */
        void goTo(Time time, boolean animate);

        /**
         * Changes the view to include today's date.
         */
        void goToToday();

        /**
         * This is called when the user wants to create a new event and returns
         * true if the new event should default to an all-day event.
         * @return true if the new event should be an all-day event.
         */
        boolean getAllDay();

        /**
Erik's avatar
Erik committed
149 150
         * This notifies the handler that the database has changed and it should
         * update its view.
Michael Chan's avatar
Michael Chan committed
151 152 153 154
         */
        void eventsChanged();
    }

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    /**
     * Creates and/or returns an instance of CalendarController associated with
     * the supplied context. It is best to pass in the current Activity.
     *
     * @param context The activity if at all possible.
     */
    public static CalendarController getInstance(Context context) {
        synchronized (instances) {
            CalendarController controller = instances.get(context);
            if (controller == null) {
                controller = new CalendarController(context);
                instances.put(context, controller);
            }
            return controller;
        }
    }

Erik's avatar
Erik committed
172 173 174 175 176 177 178 179 180 181
    /**
     * Removes an instance when it is no longer needed. This should be called in
     * an activity's onDestroy method.
     *
     * @param context The activity used to create the controller
     */
    public static void removeInstance(Context context) {
        instances.remove(context);
    }

182
    private CalendarController(Context context) {
183
        mContext = context;
184
        mTime.setToNow();
185
        mDetailViewType = Utils.getSharedPreference(mContext,
186 187
                GeneralPreferences.KEY_DETAILED_VIEW,
                GeneralPreferences.DEFAULT_DETAILED_VIEW);
Erik's avatar
Erik committed
188 189 190
        mService = new AsyncQueryService(context) {
            @Override
            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
Erik's avatar
Erik committed
191
                new RefreshInBackground().execute(cursor);
Erik's avatar
Erik committed
192 193
            }
        };
Michael Chan's avatar
Michael Chan committed
194
    }
195 196

    /**
Michael Chan's avatar
Michael Chan committed
197 198 199 200 201
     * Helper for sending New/View/Edit/Delete events
     *
     * @param sender object of the caller
     * @param eventType one of {@link EventType}
     * @param eventId event id
202 203
     * @param startMillis start time
     * @param endMillis end time
Michael Chan's avatar
Michael Chan committed
204 205
     * @param x x coordinate in the activity space
     * @param y y coordinate in the activity space
206
     */
207 208
    public void sendEventRelatedEvent(Object sender, long eventType, long eventId, long startMillis,
            long endMillis, int x, int y) {
Michael Chan's avatar
Michael Chan committed
209 210
        EventInfo info = new EventInfo();
        info.eventType = eventType;
211 212 213
        if (eventType == EventType.EDIT_EVENT) {
            info.viewType = ViewType.EDIT;
        }
Michael Chan's avatar
Michael Chan committed
214
        info.id = eventId;
215 216 217 218
        info.startTime = new Time();
        info.startTime.set(startMillis);
        info.endTime = new Time();
        info.endTime.set(endMillis);
Michael Chan's avatar
Michael Chan committed
219 220 221 222
        info.x = x;
        info.y = y;
        this.sendEvent(sender, info);
    }
223 224

    /**
Michael Chan's avatar
Michael Chan committed
225 226 227 228 229 230
     * Helper for sending non-calendar-event events
     *
     * @param sender object of the caller
     * @param eventType one of {@link EventType}
     * @param start start time
     * @param end end time
231
     * @param eventId event id
Michael Chan's avatar
Michael Chan committed
232
     * @param viewType {@link ViewType}
233
     */
Erik's avatar
Erik committed
234
    public void sendEvent(Object sender, long eventType, Time start, Time end, long eventId,
235
            int viewType) {
Michael Chan's avatar
Michael Chan committed
236 237 238 239 240 241 242 243 244
        EventInfo info = new EventInfo();
        info.eventType = eventType;
        info.startTime = start;
        info.endTime = end;
        info.id = eventId;
        info.viewType = viewType;
        this.sendEvent(sender, info);
    }

Erik's avatar
Erik committed
245
    public void sendEvent(Object sender, final EventInfo event) {
Michael Chan's avatar
Michael Chan committed
246 247
        // TODO Throw exception on invalid events

248 249 250
        if (DEBUG) {
            Log.d(TAG, eventInfoToString(event));
        }
Michael Chan's avatar
Michael Chan committed
251 252 253 254

        Long filteredTypes = filters.get(sender);
        if (filteredTypes != null && (filteredTypes.longValue() & event.eventType) != 0) {
            // Suppress event per filter
255 256 257
            if (DEBUG) {
                Log.d(TAG, "Event suppressed");
            }
Michael Chan's avatar
Michael Chan committed
258 259 260
            return;
        }

261
        mPreviousViewType = mViewType;
Michael Chan's avatar
Michael Chan committed
262

263
        // Fix up view if not specified
264
        if (event.viewType == ViewType.DETAIL) {
265 266
            event.viewType = mDetailViewType;
            mViewType = mDetailViewType;
267
        } else if (event.viewType == ViewType.CURRENT) {
268
            event.viewType = mViewType;
269 270
        } else {
            mViewType = event.viewType;
271 272 273 274

            if (event.viewType == ViewType.AGENDA || event.viewType == ViewType.DAY) {
                mDetailViewType = mViewType;
            }
275 276
        }

277 278 279 280 281 282 283
        // Fix up start time if not specified
        if (event.startTime != null && event.startTime.toMillis(false) != 0) {
            mTime.set(event.startTime);
        }
        event.startTime = mTime;

        boolean handled = false;
284 285 286
        synchronized (this) {
            mDispatchInProgress = true;

287 288 289
            if (DEBUG) {
                Log.d(TAG, "sendEvent: Dispatching to " + eventHandlers.size() + " handlers");
            }
290
            // Dispatch to event handler(s)
291 292 293 294 295
            for (Iterator<Entry<Integer, EventHandler>> handlers =
                    eventHandlers.entrySet().iterator(); handlers.hasNext();) {
                Entry<Integer, EventHandler> entry = handlers.next();
                int key = entry.getKey();
                EventHandler eventHandler = entry.getValue();
296 297
                if (eventHandler != null
                        && (eventHandler.getSupportedEventTypes() & event.eventType) != 0) {
298
                    if (mToBeRemovedEventHandlers.contains(key)) {
299 300 301
                        continue;
                    }
                    eventHandler.handleEvent(event);
302
                    handled = true;
303 304
                }
            }
305

306 307
            // Deregister removed handlers
            if (mToBeRemovedEventHandlers.size() > 0) {
308
                for (Integer zombie : mToBeRemovedEventHandlers) {
309
                    eventHandlers.remove(zombie);
Michael Chan's avatar
Michael Chan committed
310
                }
311
                mToBeRemovedEventHandlers.clear();
Michael Chan's avatar
Michael Chan committed
312
            }
313
            mDispatchInProgress = false;
Michael Chan's avatar
Michael Chan committed
314
        }
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341

        if (!handled) {
            // Launch Calendars, and Settings
            if (event.eventType == EventType.LAUNCH_MANAGE_CALENDARS) {
                launchManageCalendars();
                return;
            } else if (event.eventType == EventType.LAUNCH_SETTINGS) {
                launchSettings();
                return;
            }

            // Create/View/Edit/Delete Event
            long endTime = (event.endTime == null) ? -1 : event.endTime.toMillis(false);
            if (event.eventType == EventType.CREATE_EVENT) {
                launchCreateEvent(event.startTime.toMillis(false), endTime);
                return;
            } else if (event.eventType == EventType.VIEW_EVENT) {
                launchViewEvent(event.id, event.startTime.toMillis(false), endTime);
                return;
            } else if (event.eventType == EventType.EDIT_EVENT) {
                launchEditEvent(event.id, event.startTime.toMillis(false), endTime);
                return;
            } else if (event.eventType == EventType.DELETE_EVENT) {
                launchDeleteEvent(event.id, event.startTime.toMillis(false), endTime);
                return;
            }
        }
Michael Chan's avatar
Michael Chan committed
342 343
    }

344 345 346 347 348 349 350 351
    /**
     * Adds or updates an event handler. This uses a LinkedHashMap so that we can
     * replace fragments based on the view id they are being expanded into.
     *
     * @param key The view id or placeholder for this handler
     * @param eventHandler Typically a fragment or activity in the calendar app
     */
    public void registerEventHandler(int key, EventHandler eventHandler) {
352
        synchronized (this) {
353
            eventHandlers.put(key, eventHandler);
354
        }
Michael Chan's avatar
Michael Chan committed
355 356
    }

357
    public void deregisterEventHandler(Integer key) {
358 359 360
        synchronized (this) {
            if (mDispatchInProgress) {
                // To avoid ConcurrencyException, stash away the event handler for now.
361
                mToBeRemovedEventHandlers.add(key);
362
            } else {
363
                eventHandlers.remove(key);
364 365
            }
        }
Michael Chan's avatar
Michael Chan committed
366 367
    }

368
    // FRAG_TODO doesn't work yet
Erik's avatar
Erik committed
369
    public void filterBroadcasts(Object sender, long eventTypes) {
Michael Chan's avatar
Michael Chan committed
370 371 372
        filters.put(sender, eventTypes);
    }

373 374 375 376 377 378 379
    /**
     * @return the time that this controller is currently pointed at
     */
    public long getTime() {
        return mTime.toMillis(false);
    }

380 381 382 383 384 385 386
    public int getViewType() {
        return mViewType;
    }

    public int getPreviousViewType() {
        return mPreviousViewType;
    }
387

388 389
    private void launchManageCalendars() {
        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 396
    }

    private void launchSettings() {
        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 403
    }

    private void launchCreateEvent(long startMillis, long endMillis) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
404
        intent.setClassName(mContext, EditEventActivity.class.getName());
405 406
        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
        intent.putExtra(EVENT_END_TIME, endMillis);
407
        mContext.startActivity(intent);
408 409 410 411 412 413
    }

    private void launchViewEvent(long eventId, long startMillis, long endMillis) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
        intent.setData(eventUri);
414
        intent.setClassName(mContext, EventInfoActivity.class.getName());
415 416
        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
        intent.putExtra(EVENT_END_TIME, endMillis);
417
        mContext.startActivity(intent);
418 419 420 421 422 423 424
    }

    private void launchEditEvent(long eventId, long startMillis, long endMillis) {
        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
        Intent intent = new Intent(Intent.ACTION_EDIT, uri);
        intent.putExtra(EVENT_BEGIN_TIME, startMillis);
        intent.putExtra(EVENT_END_TIME, endMillis);
425 426
        intent.setClass(mContext, EditEventActivity.class);
        mContext.startActivity(intent);
427 428 429 430 431 432 433 434
    }

    private void launchDeleteEvent(long eventId, long startMillis, long endMillis) {
        launchDeleteEventAndFinish(null, eventId, startMillis, endMillis, -1);
    }

    private void launchDeleteEventAndFinish(Activity parentActivity, long eventId, long startMillis,
            long endMillis, int deleteWhich) {
435
        DeleteEventHelper deleteEventHelper = new DeleteEventHelper(mContext, parentActivity,
436 437 438
                parentActivity != null /* exit when done */);
        deleteEventHelper.delete(startMillis, endMillis, eventId, deleteWhich);
    }
439

Erik's avatar
Erik committed
440 441 442 443 444 445 446 447 448 449 450
    public void refreshCalendars() {
        Log.d(TAG, "RefreshCalendars starting");
        // get the account, url, and current sync state
        mService.startQuery(mService.getNextToken(), null, Calendars.CONTENT_URI,
                new String[] {Calendars._ID, // 0
                        Calendars._SYNC_ACCOUNT, // 1
                        Calendars._SYNC_ACCOUNT_TYPE, // 2
                        },
                REFRESH_SELECTION, REFRESH_ARGS, REFRESH_ORDER);
    }

451 452 453 454 455
    // Forces the viewType. Should only be used for initialization.
    public void setViewType(int viewType) {
        mViewType = viewType;
    }

Erik's avatar
Erik committed
456 457 458 459 460 461 462 463 464 465 466 467 468
    private class RefreshInBackground extends AsyncTask<Cursor, Integer, Integer> {
        /* (non-Javadoc)
         * @see android.os.AsyncTask#doInBackground(Params[])
         */
        @Override
        protected Integer doInBackground(Cursor... params) {
            if (params.length != 1) {
                return null;
            }
            Cursor cursor = params[0];
            if (cursor == null) {
                return null;
            }
Erik's avatar
Erik committed
469

Erik's avatar
Erik committed
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
            String previousAccount = null;
            String previousType = null;
            Log.d(TAG, "Refreshing " + cursor.getCount() + " calendars");
            try {
                while (cursor.moveToNext()) {
                    Account account = null;
                    String accountName = cursor.getString(1);
                    String accountType = cursor.getString(2);
                    // Only need to schedule one sync per account and they're
                    // ordered by account,type
                    if (TextUtils.equals(accountName, previousAccount) &&
                            TextUtils.equals(accountType, previousType)) {
                        continue;
                    }
                    previousAccount = accountName;
                    previousType = accountType;
                    account = new Account(accountName, accountType);
                    scheduleSync(account, false /* two-way sync */, null);
Erik's avatar
Erik committed
488
                }
Erik's avatar
Erik committed
489 490
            } finally {
                cursor.close();
Erik's avatar
Erik committed
491
            }
Erik's avatar
Erik committed
492
            return null;
Erik's avatar
Erik committed
493 494
        }

Erik's avatar
Erik committed
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
        /**
         * Schedule a calendar sync for the account.
         * @param account the account for which to schedule a sync
         * @param uploadChangesOnly if set, specify that the sync should only send
         *   up local changes.  This is typically used for a local sync, a user override of
         *   too many deletions, or a sync after a calendar is unselected.
         * @param url the url feed for the calendar to sync (may be null, in which case a poll of
         *   all feeds is done.)
         */
        void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
            Bundle extras = new Bundle();
            if (uploadChangesOnly) {
                extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
            }
            if (url != null) {
                extras.putString("feed", url);
                extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
            }
            ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
Erik's avatar
Erik committed
514 515 516
        }
    }

517 518 519 520
    private String eventInfoToString(EventInfo eventInfo) {
        String tmp = "Unknown";

        StringBuilder builder = new StringBuilder();
521
        if ((eventInfo.eventType & EventType.GO_TO) != 0) {
522 523 524 525 526 527 528 529 530 531 532 533 534
            tmp = "Go to time/event";
        } else if ((eventInfo.eventType & EventType.CREATE_EVENT) != 0) {
            tmp = "New event";
        } else if ((eventInfo.eventType & EventType.VIEW_EVENT) != 0) {
            tmp = "View event";
        } else if ((eventInfo.eventType & EventType.EDIT_EVENT) != 0) {
            tmp = "Edit event";
        } else if ((eventInfo.eventType & EventType.DELETE_EVENT) != 0) {
            tmp = "Delete event";
        } else if ((eventInfo.eventType & EventType.LAUNCH_MANAGE_CALENDARS) != 0) {
            tmp = "Launch select calendar";
        } else if ((eventInfo.eventType & EventType.LAUNCH_SETTINGS) != 0) {
            tmp = "Launch settings";
535 536
        } else if ((eventInfo.eventType & EventType.EVENTS_CHANGED) != 0) {
            tmp = "Refresh events";
537 538
        } else if ((eventInfo.eventType & EventType.SEARCH) != 0) {
            tmp = "Search";
539 540 541 542
        }
        builder.append(tmp);
        builder.append(": id=");
        builder.append(eventInfo.id);
543 544 545
        builder.append(", selected=");
        builder.append(eventInfo.selectedTime);
        builder.append(", start=");
546
        builder.append(eventInfo.startTime);
547
        builder.append(", end=");
548 549 550 551 552 553 554 555 556
        builder.append(eventInfo.endTime);
        builder.append(", viewType=");
        builder.append(eventInfo.viewType);
        builder.append(", x=");
        builder.append(eventInfo.x);
        builder.append(", y=");
        builder.append(eventInfo.y);
        return builder.toString();
    }
557
}