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