CalendarAppWidgetService.java revision 3f888688c0f2644ad3de032d5d1cf623a7b092fd
1/* 2 * Copyright (C) 2009 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.widget; 18 19import com.android.calendar.R; 20import com.android.calendar.Utils; 21import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo; 22import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo; 23import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo; 24 25import android.app.AlarmManager; 26import android.app.PendingIntent; 27import android.appwidget.AppWidgetManager; 28import android.content.BroadcastReceiver; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.CursorLoader; 32import android.content.Intent; 33import android.content.IntentFilter; 34import android.content.IntentFilter.MalformedMimeTypeException; 35import android.content.Loader; 36import android.content.res.Resources; 37import android.database.Cursor; 38import android.database.MatrixCursor; 39import android.net.Uri; 40import android.os.Handler; 41import android.provider.Calendar; 42import android.provider.Calendar.Attendees; 43import android.provider.Calendar.Calendars; 44import android.provider.Calendar.Instances; 45import android.text.format.DateUtils; 46import android.text.format.Time; 47import android.util.Log; 48import android.view.View; 49import android.widget.RemoteViews; 50import android.widget.RemoteViewsService; 51 52 53public class CalendarAppWidgetService extends RemoteViewsService { 54 private static final String TAG = "CalendarWidget"; 55 56 static final int EVENT_MIN_COUNT = 20; 57 static final int EVENT_MAX_COUNT = 503; 58 // Minimum delay between queries on the database for widget updates in ms 59 static final int WIDGET_UPDATE_THROTTLE = 500; 60 61 private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, " 62 + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " 63 + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT; 64 65 // TODO can't use parameter here because provider is dropping them 66 private static final String EVENT_SELECTION = Calendars.VISIBLE + "=1 AND " 67 + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED; 68 69 static final String[] EVENT_PROJECTION = new String[] { 70 Instances.ALL_DAY, 71 Instances.BEGIN, 72 Instances.END, 73 Instances.TITLE, 74 Instances.EVENT_LOCATION, 75 Instances.EVENT_ID, 76 Instances.START_DAY, 77 Instances.END_DAY, 78 Instances.CALENDAR_COLOR 79 }; 80 81 static final int INDEX_ALL_DAY = 0; 82 static final int INDEX_BEGIN = 1; 83 static final int INDEX_END = 2; 84 static final int INDEX_TITLE = 3; 85 static final int INDEX_EVENT_LOCATION = 4; 86 static final int INDEX_EVENT_ID = 5; 87 static final int INDEX_START_DAY = 6; 88 static final int INDEX_END_DAY = 7; 89 static final int INDEX_COLOR = 8; 90 91 static final int MAX_DAYS = 7; 92 93 private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS; 94 95 /** 96 * Update interval used when no next-update calculated, or bad trigger time in past. 97 * Unit: milliseconds. 98 */ 99 private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6; 100 101 @Override 102 public RemoteViewsFactory onGetViewFactory(Intent intent) { 103 return new CalendarFactory(getApplicationContext(), intent); 104 } 105 106 protected static class CalendarFactory extends BroadcastReceiver implements 107 RemoteViewsService.RemoteViewsFactory, Loader.OnLoadCompleteListener<Cursor> { 108 private static final boolean LOGD = false; 109 110 // Suppress unnecessary logging about update time. Need to be static as this object is 111 // re-instanciated frequently. 112 // TODO: It seems loadData() is called via onCreate() four times, which should mean 113 // unnecessary CalendarFactory object is created and dropped. It is not efficient. 114 private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS; 115 116 private Context mContext; 117 private Resources mResources; 118 private CalendarAppWidgetModel mModel; 119 private Cursor mCursor; 120 private CursorLoader mLoader; 121 private Handler mHandler = new Handler(); 122 private int mAppWidgetId; 123 124 private Runnable mTimezoneChanged = new Runnable() { 125 @Override 126 public void run() { 127 if (mLoader != null) { 128 mLoader.forceLoad(); 129 } 130 } 131 }; 132 133 private Runnable mUpdateLoader = new Runnable() { 134 @Override 135 public void run() { 136 if (mLoader != null) { 137 Uri uri = createLoaderUri(); 138 mLoader.setUri(uri); 139 mLoader.forceLoad(); 140 } 141 } 142 }; 143 144 protected CalendarFactory(Context context, Intent intent) { 145 mContext = context; 146 mResources = context.getResources(); 147 mAppWidgetId = intent.getIntExtra( 148 AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); 149 } 150 151 @Override 152 public void onCreate() { 153 initLoader(); 154 } 155 156 @Override 157 public void onDataSetChanged() { 158 } 159 160 @Override 161 public void onDestroy() { 162 if (mCursor != null) { 163 mCursor.close(); 164 } 165 if (mLoader != null) { 166 mLoader.reset(); 167 } 168 mContext.unregisterReceiver(this); 169 } 170 171 @Override 172 public RemoteViews getLoadingView() { 173 RemoteViews views = new RemoteViews(mContext.getPackageName(), 174 R.layout.appwidget_loading); 175 return views; 176 } 177 178 @Override 179 public RemoteViews getViewAt(int position) { 180 // we use getCount here so that it doesn't return null when empty 181 if (position < 0 || position >= getCount()) { 182 return null; 183 } 184 185 if (mModel == null || mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) { 186 RemoteViews views = new RemoteViews(mContext.getPackageName(), 187 R.layout.appwidget_no_events); 188 final Intent intent = CalendarAppWidgetProvider.getLaunchFillInIntent(0, 0, 0); 189 views.setOnClickFillInIntent(R.id.appwidget_no_events, intent); 190 return views; 191 } 192 193 RowInfo rowInfo = mModel.mRowInfos.get(position); 194 if (rowInfo.mType == RowInfo.TYPE_DAY) { 195 RemoteViews views = new RemoteViews(mContext.getPackageName(), 196 R.layout.appwidget_day); 197 DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex); 198 updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel); 199 return views; 200 } else { 201 final RemoteViews views = new RemoteViews(mContext.getPackageName(), 202 R.layout.appwidget_row); 203 final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex); 204 205 final long now = System.currentTimeMillis(); 206 if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) { 207 views.setInt(R.id.appwidget_row, "setBackgroundColor", 208 mResources.getColor(R.color.appwidget_row_in_progress)); 209 } else { 210 views.setInt(R.id.appwidget_row, "setBackgroundResource", 211 R.drawable.bg_event_cal_widget_holo); 212 } 213 214 updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when); 215 updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where); 216 updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title); 217 218 views.setViewVisibility(R.id.color, View.VISIBLE); 219 views.setInt(R.id.color, "setBackgroundColor", eventInfo.color); 220 221 long start = eventInfo.start; 222 long end = eventInfo.end; 223 // An element in ListView. 224 if (eventInfo.allDay) { 225 String tz = Utils.getTimeZone(mContext, null); 226 Time recycle = new Time(); 227 start = Utils.convertAlldayLocalToUTC(recycle, start, tz); 228 end = Utils.convertAlldayLocalToUTC(recycle, end, tz); 229 } 230 final Intent fillInIntent = CalendarAppWidgetProvider.getLaunchFillInIntent( 231 eventInfo.id, start, end); 232 views.setOnClickFillInIntent(R.id.appwidget_row, fillInIntent); 233 return views; 234 } 235 } 236 237 @Override 238 public int getViewTypeCount() { 239 return 4; 240 } 241 242 @Override 243 public int getCount() { 244 // if there are no events, we still return 1 to represent the "no 245 // events" view 246 if (mModel == null) { 247 return 1; 248 } 249 return Math.max(1, mModel.mRowInfos.size()); 250 } 251 252 @Override 253 public long getItemId(int position) { 254 if (mModel == null || mModel.mRowInfos.isEmpty()) { 255 return 0; 256 } 257 RowInfo rowInfo = mModel.mRowInfos.get(position); 258 if (rowInfo.mType == RowInfo.TYPE_DAY) { 259 return rowInfo.mIndex; 260 } 261 EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex); 262 long prime = 31; 263 long result = 1; 264 result = prime * result + (int) (eventInfo.id ^ (eventInfo.id >>> 32)); 265 result = prime * result + (int) (eventInfo.start ^ (eventInfo.start >>> 32)); 266 return result; 267 } 268 269 @Override 270 public boolean hasStableIds() { 271 return true; 272 } 273 274 /** 275 * Query across all calendars for upcoming event instances from now 276 * until some time in the future. Widen the time range that we query by 277 * one day on each end so that we can catch all-day events. All-day 278 * events are stored starting at midnight in UTC but should be included 279 * in the list of events starting at midnight local time. This may fetch 280 * more events than we actually want, so we filter them out later. 281 * 282 * @param resolver {@link ContentResolver} to use when querying 283 * {@link Instances#CONTENT_URI}. 284 * @param searchDuration Distance into the future to look for event 285 * instances, in milliseconds. 286 * @param now Current system time to use for this update, possibly from 287 * {@link System#currentTimeMillis()}. 288 */ 289 public void initLoader() { 290 if (LOGD) 291 Log.d(TAG, "Querying for widget events..."); 292 IntentFilter filter = new IntentFilter(); 293 filter.addAction(CalendarAppWidgetProvider.ACTION_CALENDAR_APPWIDGET_SCHEDULED_UPDATE); 294 filter.addDataScheme(ContentResolver.SCHEME_CONTENT); 295 filter.addDataAuthority(Calendar.AUTHORITY, null); 296 try { 297 filter.addDataType(CalendarAppWidgetProvider.APPWIDGET_DATA_TYPE); 298 } catch (MalformedMimeTypeException e) { 299 Log.e(TAG, e.getMessage()); 300 } 301 mContext.registerReceiver(this, filter); 302 303 filter = new IntentFilter(); 304 filter.addAction(Intent.ACTION_PROVIDER_CHANGED); 305 filter.addDataScheme(ContentResolver.SCHEME_CONTENT); 306 filter.addDataAuthority(Calendar.AUTHORITY, null); 307 mContext.registerReceiver(this, filter); 308 309 filter = new IntentFilter(); 310 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 311 filter.addAction(Intent.ACTION_TIME_CHANGED); 312 filter.addAction(Intent.ACTION_DATE_CHANGED); 313 mContext.registerReceiver(this, filter); 314 315 // Search for events from now until some time in the future 316 Uri uri = createLoaderUri(); 317 318 mLoader = new CursorLoader( 319 mContext, uri, EVENT_PROJECTION, EVENT_SELECTION, null, EVENT_SORT_ORDER); 320 mLoader.setUpdateThrottle(WIDGET_UPDATE_THROTTLE); 321 mLoader.startLoading(); 322 mLoader.registerListener(mAppWidgetId, this); 323 324 } 325 326 /** 327 * @return The uri for the loader 328 */ 329 private Uri createLoaderUri() { 330 long now = System.currentTimeMillis(); 331 // Add a day on either side to catch all-day events 332 long begin = now - DateUtils.DAY_IN_MILLIS; 333 long end = now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS; 334 335 Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, Long.toString(begin) + "/" + end); 336 return uri; 337 } 338 339 /* @VisibleForTesting */ 340 protected static CalendarAppWidgetModel buildAppWidgetModel( 341 Context context, Cursor cursor, String timeZone) { 342 CalendarAppWidgetModel model = new CalendarAppWidgetModel(context, timeZone); 343 model.buildFromCursor(cursor, timeZone); 344 return model; 345 } 346 347 /** 348 * Calculates and returns the next time we should push widget updates. 349 */ 350 private long calculateUpdateTime(CalendarAppWidgetModel model, long now, String timeZone) { 351 // Make sure an update happens at midnight or earlier 352 long minUpdateTime = getNextMidnightTimeMillis(timeZone); 353 for (EventInfo event : model.mEventInfos) { 354 final boolean allDay = event.allDay; 355 final long start; 356 final long end; 357 start = event.start; 358 end = event.end; 359 360 // We want to update widget when we enter/exit time range of an event. 361 if (now < start) { 362 minUpdateTime = Math.min(minUpdateTime, start); 363 } else if (now < end) { 364 minUpdateTime = Math.min(minUpdateTime, end); 365 } 366 } 367 return minUpdateTime; 368 } 369 370 private static long getNextMidnightTimeMillis(String timezone) { 371 Time time = new Time(); 372 time.setToNow(); 373 time.monthDay++; 374 time.hour = 0; 375 time.minute = 0; 376 time.second = 0; 377 long midnightDeviceTz = time.normalize(true); 378 379 time.timezone = timezone; 380 time.setToNow(); 381 time.monthDay++; 382 time.hour = 0; 383 time.minute = 0; 384 time.second = 0; 385 long midnightHomeTz = time.normalize(true); 386 387 return Math.min(midnightDeviceTz, midnightHomeTz); 388 } 389 390 static void updateTextView(RemoteViews views, int id, int visibility, String string) { 391 views.setViewVisibility(id, visibility); 392 if (visibility == View.VISIBLE) { 393 views.setTextViewText(id, string); 394 } 395 } 396 397 /* 398 * (non-Javadoc) 399 * @see 400 * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android 401 * .content.Loader, java.lang.Object) 402 */ 403 @Override 404 public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) { 405 // Copy it to a local static cursor. 406 MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor); 407 cursor.close(); 408 409 final long now = System.currentTimeMillis(); 410 if (mCursor != null) { 411 mCursor.close(); 412 } 413 mCursor = matrixCursor; 414 String tz = Utils.getTimeZone(mContext, mTimezoneChanged); 415 mModel = buildAppWidgetModel(mContext, mCursor, tz); 416 417 // Schedule an alarm to wake ourselves up for the next update. 418 // We also cancel 419 // all existing wake-ups because PendingIntents don't match 420 // against extras. 421 long triggerTime = calculateUpdateTime(mModel, now, tz); 422 423 // If no next-update calculated, or bad trigger time in past, 424 // schedule 425 // update about six hours from now. 426 if (triggerTime < now) { 427 Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now)); 428 triggerTime = now + UPDATE_TIME_NO_EVENTS; 429 } 430 431 432 final AlarmManager alertManager = (AlarmManager) mContext.getSystemService( 433 Context.ALARM_SERVICE); 434 final PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(mContext); 435 436 alertManager.cancel(pendingUpdate); 437 alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate); 438 Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now)); 439 Time time = new Time(Utils.getTimeZone(mContext, null)); 440 time.setToNow(); 441 442 if (time.normalize(true) != sLastUpdateTime) { 443 Time time2 = new Time(Utils.getTimeZone(mContext, null)); 444 time2.set(sLastUpdateTime); 445 time2.normalize(true); 446 if (time.year != time2.year || time.yearDay != time2.yearDay) { 447 final Intent updateIntent = new Intent( 448 CalendarAppWidgetProvider.ACTION_CALENDAR_APPWIDGET_UPDATE); 449 mContext.sendBroadcast(updateIntent); 450 } 451 452 sLastUpdateTime = time.toMillis(true); 453 } 454 455 AppWidgetManager.getInstance(mContext).notifyAppWidgetViewDataChanged( 456 mAppWidgetId, R.id.events_list); 457 } 458 459 @Override 460 public void onReceive(Context context, Intent intent) { 461 mHandler.removeCallbacks(mUpdateLoader); 462 mHandler.post(mUpdateLoader); 463 } 464 } 465 466 /** 467 * Format given time for debugging output. 468 * 469 * @param unixTime Target time to report. 470 * @param now Current system time from {@link System#currentTimeMillis()} 471 * for calculating time difference. 472 */ 473 static String formatDebugTime(long unixTime, long now) { 474 Time time = new Time(); 475 time.set(unixTime); 476 477 long delta = unixTime - now; 478 if (delta > DateUtils.MINUTE_IN_MILLIS) { 479 delta /= DateUtils.MINUTE_IN_MILLIS; 480 return String.format("[%d] %s (%+d mins)", unixTime, 481 time.format("%H:%M:%S"), delta); 482 } else { 483 delta /= DateUtils.SECOND_IN_MILLIS; 484 return String.format("[%d] %s (%+d secs)", unixTime, 485 time.format("%H:%M:%S"), delta); 486 } 487 } 488} 489