CalendarAppWidgetService.java revision 309c34fcce4912a9c6f1c0a39c090cebf61296be
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.google.common.annotations.VisibleForTesting; 20 21import com.android.calendar.R; 22import com.android.calendar.Utils; 23import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo; 24import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo; 25import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo; 26 27import android.app.AlarmManager; 28import android.app.PendingIntent; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.Intent; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.database.MatrixCursor; 35import android.net.Uri; 36import android.provider.Calendar.Attendees; 37import android.provider.Calendar.CalendarCache; 38import android.provider.Calendar.Calendars; 39import android.provider.Calendar.Instances; 40import android.text.TextUtils; 41import android.text.format.DateUtils; 42import android.text.format.Time; 43import android.util.Log; 44import android.view.View; 45import android.widget.RemoteViews; 46import android.widget.RemoteViewsService; 47 48 49public class CalendarAppWidgetService extends RemoteViewsService { 50 private static final String TAG = "CalendarWidget"; 51 52 static final int EVENT_MIN_COUNT = 20; 53 static final int EVENT_MAX_COUNT = 503; 54 55 private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, " 56 + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " 57 + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT; 58 59 // TODO can't use parameter here because provider is dropping them 60 private static final String EVENT_SELECTION = Calendars.SELECTED + "=1 AND " 61 + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED; 62 63 static final String[] EVENT_PROJECTION = new String[] { 64 Instances.ALL_DAY, 65 Instances.BEGIN, 66 Instances.END, 67 Instances.TITLE, 68 Instances.EVENT_LOCATION, 69 Instances.EVENT_ID, 70 Instances.START_DAY, 71 Instances.END_DAY, 72 Instances.COLOR 73 }; 74 75 static final int INDEX_ALL_DAY = 0; 76 static final int INDEX_BEGIN = 1; 77 static final int INDEX_END = 2; 78 static final int INDEX_TITLE = 3; 79 static final int INDEX_EVENT_LOCATION = 4; 80 static final int INDEX_EVENT_ID = 5; 81 static final int INDEX_START_DAY = 6; 82 static final int INDEX_END_DAY = 7; 83 static final int INDEX_COLOR = 8; 84 85 static final int MAX_DAYS = 7; 86 87 private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS; 88 89 /** 90 * Update interval used when no next-update calculated, or bad trigger time in past. 91 * Unit: milliseconds. 92 */ 93 private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6; 94 95 @Override 96 public RemoteViewsFactory onGetViewFactory(Intent intent) { 97 return new CalendarFactory(getApplicationContext(), intent); 98 } 99 100 protected static class CalendarFactory implements RemoteViewsService.RemoteViewsFactory { 101 private static final boolean LOGD = false; 102 103 // Suppress unnecessary logging about update time. Need to be static as this object is 104 // re-instanciated frequently. 105 // TODO: It seems loadData() is called via onCreate() four times, which should mean 106 // unnecessary CalendarFactory object is created and dropped. It is not efficient. 107 private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS; 108 109 private Context mContext; 110 private Resources mResources; 111 private CalendarAppWidgetModel mModel; 112 private Cursor mCursor; 113 114 protected CalendarFactory(Context context, Intent intent) { 115 mContext = context; 116 mResources = context.getResources(); 117 } 118 119 @Override 120 public void onCreate() { 121 loadData(); 122 } 123 124 @Override 125 public void onDataSetChanged() { 126 loadData(); 127 } 128 129 @Override 130 public void onDestroy() { 131 mCursor.close(); 132 } 133 134 @Override 135 public RemoteViews getLoadingView() { 136 RemoteViews views = new RemoteViews(mContext.getPackageName(), 137 R.layout.appwidget_loading); 138 return views; 139 } 140 141 @Override 142 public RemoteViews getViewAt(int position) { 143 // we use getCount here so that it doesn't return null when empty 144 if (position < 0 || position >= getCount()) { 145 return null; 146 } 147 148 if (mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) { 149 RemoteViews views = new RemoteViews(mContext.getPackageName(), 150 R.layout.appwidget_no_events); 151 final Intent intent = CalendarAppWidgetProvider.getLaunchFillInIntent(0); 152 views.setOnClickFillInIntent(R.id.appwidget_no_events, intent); 153 return views; 154 } 155 156 RowInfo rowInfo = mModel.mRowInfos.get(position); 157 if (rowInfo.mType == RowInfo.TYPE_DAY) { 158 RemoteViews views = new RemoteViews(mContext.getPackageName(), 159 R.layout.appwidget_day); 160 DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex); 161 updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel); 162 return views; 163 } else { 164 final RemoteViews views = new RemoteViews(mContext.getPackageName(), 165 R.layout.appwidget_row); 166 final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex); 167 168 final long now = System.currentTimeMillis(); 169 if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) { 170 views.setInt(R.id.appwidget_row, "setBackgroundColor", 171 mResources.getColor(R.color.appwidget_row_in_progress)); 172 } else { 173 views.setInt(R.id.appwidget_row, "setBackgroundResource", 174 R.drawable.bg_event_cal_widget_holo); 175 } 176 177 updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when); 178 updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where); 179 updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title); 180 181 views.setViewVisibility(R.id.color, View.VISIBLE); 182 views.setInt(R.id.color, "setBackgroundColor", eventInfo.color); 183 184 // An element in ListView. 185 final Intent fillInIntent = 186 CalendarAppWidgetProvider.getLaunchFillInIntent(eventInfo.start); 187 views.setOnClickFillInIntent(R.id.appwidget_row, fillInIntent); 188 return views; 189 } 190 } 191 192 @Override 193 public int getViewTypeCount() { 194 return 4; 195 } 196 197 @Override 198 public int getCount() { 199 // if there are no events, we still return 1 to represent the "no 200 // events" view 201 return Math.max(1, mModel.mRowInfos.size()); 202 } 203 204 @Override 205 public long getItemId(int position) { 206 return position; 207 } 208 209 @Override 210 public boolean hasStableIds() { 211 return true; 212 } 213 214 private void loadData() { 215 final long now = System.currentTimeMillis(); 216 if (LOGD) Log.d(TAG, "Querying for widget events..."); 217 if (mCursor != null) { 218 mCursor.close(); 219 } 220 221 final ContentResolver resolver = mContext.getContentResolver(); 222 mCursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now); 223 String tz = getTimeZoneFromDB(resolver); 224 mModel = buildAppWidgetModel(mContext, mCursor, tz); 225 226 // Schedule an alarm to wake ourselves up for the next update. We also cancel 227 // all existing wake-ups because PendingIntents don't match against extras. 228 long triggerTime = calculateUpdateTime(mModel, now); 229 230 // If no next-update calculated, or bad trigger time in past, schedule 231 // update about six hours from now. 232 if (triggerTime < now) { 233 Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now)); 234 triggerTime = now + UPDATE_TIME_NO_EVENTS; 235 } 236 237 final AlarmManager alertManager = 238 (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); 239 final PendingIntent pendingUpdate = 240 CalendarAppWidgetProvider.getUpdateIntent(mContext); 241 242 alertManager.cancel(pendingUpdate); 243 alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate); 244 if (triggerTime != sLastUpdateTime) { 245 Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now)); 246 sLastUpdateTime = triggerTime; 247 } 248 } 249 250 /** 251 * Query across all calendars for upcoming event instances from now until 252 * some time in the future. 253 * 254 * Widen the time range that we query by one day on each end so that we can 255 * catch all-day events. All-day events are stored starting at midnight in 256 * UTC but should be included in the list of events starting at midnight 257 * local time. This may fetch more events than we actually want, so we 258 * filter them out later. 259 * 260 * @param resolver {@link ContentResolver} to use when querying 261 * {@link Instances#CONTENT_URI}. 262 * @param searchDuration Distance into the future to look for event 263 * instances, in milliseconds. 264 * @param now Current system time to use for this update, possibly from 265 * {@link System#currentTimeMillis()}. 266 */ 267 private Cursor getUpcomingInstancesCursor(ContentResolver resolver, 268 long searchDuration, long now) { 269 // Search for events from now until some time in the future 270 271 // Add a day on either side to catch all-day events 272 long begin = now - DateUtils.DAY_IN_MILLIS; 273 long end = now + searchDuration + DateUtils.DAY_IN_MILLIS; 274 275 Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, Long.toString(begin) + "/" + end); 276 277 Cursor cursor = resolver.query(uri, EVENT_PROJECTION, 278 EVENT_SELECTION, null, EVENT_SORT_ORDER); 279 280 // Start managing the cursor ourselves 281 MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor); 282 cursor.close(); 283 284 return matrixCursor; 285 } 286 287 private String getTimeZoneFromDB(ContentResolver resolver) { 288 String tz = null; 289 Cursor tzCursor = null; 290 try { 291 tzCursor = resolver.query( 292 CalendarCache.URI, CalendarCache.POJECTION, null, null, null); 293 if (tzCursor != null) { 294 int keyColumn = tzCursor.getColumnIndexOrThrow(CalendarCache.KEY); 295 int valueColumn = tzCursor.getColumnIndexOrThrow(CalendarCache.VALUE); 296 while (tzCursor.moveToNext()) { 297 if (TextUtils.equals(tzCursor.getString(keyColumn), 298 CalendarCache.TIMEZONE_KEY_INSTANCES)) { 299 tz = tzCursor.getString(valueColumn); 300 } 301 } 302 } 303 if (tz == null) { 304 tz = Time.getCurrentTimezone(); 305 } 306 } finally { 307 if (tzCursor != null) { 308 tzCursor.close(); 309 } 310 } 311 return tz; 312 } 313 314 @VisibleForTesting 315 protected static CalendarAppWidgetModel buildAppWidgetModel( 316 Context context, Cursor cursor, String timeZone) { 317 CalendarAppWidgetModel model = new CalendarAppWidgetModel(context); 318 model.buildFromCursor(cursor, timeZone); 319 return model; 320 } 321 322 /** 323 * Calculates and returns the next time we should push widget updates. 324 */ 325 private long calculateUpdateTime(CalendarAppWidgetModel model, long now) { 326 // Make sure an update happens at midnight or earlier 327 long minUpdateTime = getNextMidnightTimeMillis(); 328 for (EventInfo event : model.mEventInfos) { 329 final boolean allDay = event.allDay; 330 final long start; 331 final long end; 332 if (allDay) { 333 // Adjust all-day times into local timezone 334 final Time recycle = new Time(); 335 start = Utils.convertUtcToLocal(recycle, event.start); 336 end = Utils.convertUtcToLocal(recycle, event.end); 337 } else { 338 start = event.start; 339 end = event.end; 340 } 341 342 // We want to update widget when we enter/exit time range of an event. 343 if (now < start) { 344 minUpdateTime = Math.min(minUpdateTime, start); 345 } else if (now < end) { 346 minUpdateTime = Math.min(minUpdateTime, end); 347 } 348 } 349 return minUpdateTime; 350 } 351 352 private static long getNextMidnightTimeMillis() { 353 Time time = new Time(); 354 time.setToNow(); 355 time.monthDay++; 356 time.hour = 0; 357 time.minute = 0; 358 time.second = 0; 359 long midnight = time.normalize(true); 360 return midnight; 361 } 362 363 static void updateTextView(RemoteViews views, int id, int visibility, String string) { 364 views.setViewVisibility(id, visibility); 365 if (visibility == View.VISIBLE) { 366 views.setTextViewText(id, string); 367 } 368 } 369 } 370 371 /** 372 * Format given time for debugging output. 373 * 374 * @param unixTime Target time to report. 375 * @param now Current system time from {@link System#currentTimeMillis()} 376 * for calculating time difference. 377 */ 378 static String formatDebugTime(long unixTime, long now) { 379 Time time = new Time(); 380 time.set(unixTime); 381 382 long delta = unixTime - now; 383 if (delta > DateUtils.MINUTE_IN_MILLIS) { 384 delta /= DateUtils.MINUTE_IN_MILLIS; 385 return String.format("[%d] %s (%+d mins)", unixTime, 386 time.format("%H:%M:%S"), delta); 387 } else { 388 delta /= DateUtils.SECOND_IN_MILLIS; 389 return String.format("[%d] %s (%+d secs)", unixTime, 390 time.format("%H:%M:%S"), delta); 391 } 392 } 393} 394