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