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