CalendarAppWidgetServicepublic class CalendarAppWidgetService extends android.app.Service implements Runnable
Fields Summary |
---|
static final String | TAG | static final boolean | LOGD | static final String | EVENT_SORT_ORDER | static final String[] | EVENT_PROJECTION | static final int | INDEX_ALL_DAY | static final int | INDEX_BEGIN | static final int | INDEX_END | static final int | INDEX_COLOR | static final int | INDEX_TITLE | static final int | INDEX_RRULE | static final int | INDEX_HAS_ALARM | static final int | INDEX_EVENT_LOCATION | static final int | INDEX_CALENDAR_ID | static final int | INDEX_EVENT_ID | static final long | SEARCH_DURATION | static final long | UPDATE_NO_EVENTS | static final String | ACTION_PACKAGE | static final String | ACTION_CLASS |
Methods Summary |
---|
private com.android.providers.calendar.CalendarAppWidgetService$MarkedEvents | buildMarkedEvents(android.database.Cursor cursor, java.util.Set watchEventIds, long now)Walk the given instances cursor and build a list of marked events to be
used when updating the widget. This structure is also used to check if
updates are needed.
MarkedEvents events = new MarkedEvents();
final Time recycle = new Time();
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
int row = cursor.getPosition();
long eventId = cursor.getLong(INDEX_EVENT_ID);
long start = cursor.getLong(INDEX_BEGIN);
long end = cursor.getLong(INDEX_END);
boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
// Adjust all-day times into local timezone
if (allDay) {
start = convertUtcToLocal(recycle, start);
end = convertUtcToLocal(recycle, end);
}
// Skip events that have already passed their flip times
long eventFlip = getEventFlip(cursor, start, end, allDay);
if (LOGD) Log.d(TAG, "Calculated flip time " + formatDebugTime(eventFlip, now));
if (eventFlip < now) {
continue;
}
// Mark if we've encountered the watched event
if (watchEventIds.contains(eventId)) {
events.watchFound = true;
}
if (events.primaryRow == -1) {
// Found first event
events.primaryRow = row;
events.primaryTime = start;
events.primaryCount = 1;
} else if (events.primaryTime == start) {
// Found conflicting primary event
if (events.primaryConflictRow == -1) {
events.primaryConflictRow = row;
}
events.primaryCount += 1;
} else if (events.secondaryRow == -1) {
// Found second event
events.secondaryRow = row;
events.secondaryTime = start;
events.secondaryCount = 1;
} else if (events.secondaryTime == start) {
// Found conflicting secondary event
events.secondaryCount += 1;
} else {
// Nothing interesting about this event, so bail out
break;
}
}
return events;
| private long | calculateUpdateTime(android.database.Cursor cursor, com.android.providers.calendar.CalendarAppWidgetService$MarkedEvents events)Figure out the next time we should push widget updates, usually the time
calculated by {@link #getEventFlip(Cursor, long, long, boolean)}.
long result = -1;
if (events.primaryRow != -1) {
cursor.moveToPosition(events.primaryRow);
long start = cursor.getLong(INDEX_BEGIN);
long end = cursor.getLong(INDEX_END);
boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
// Adjust all-day times into local timezone
if (allDay) {
final Time recycle = new Time();
start = convertUtcToLocal(recycle, start);
end = convertUtcToLocal(recycle, end);
}
result = getEventFlip(cursor, start, end, allDay);
}
return result;
| private long | convertUtcToLocal(android.text.format.Time recycle, long utcTime)Convert given UTC time into current local time.
if (recycle == null) {
recycle = new Time();
}
recycle.timezone = Time.TIMEZONE_UTC;
recycle.set(utcTime);
recycle.timezone = TimeZone.getDefault().getID();
return recycle.normalize(true);
| private java.lang.String | formatDebugTime(long unixTime, long now)Format given time for debugging output.
Time time = new Time();
time.set(unixTime);
long delta = unixTime - now;
if (delta > DateUtils.MINUTE_IN_MILLIS) {
delta /= DateUtils.MINUTE_IN_MILLIS;
return String.format("[%d] %s (%+d mins)", unixTime, time.format("%H:%M:%S"), delta);
} else {
delta /= DateUtils.SECOND_IN_MILLIS;
return String.format("[%d] %s (%+d secs)", unixTime, time.format("%H:%M:%S"), delta);
}
| private android.widget.RemoteViews | getAppWidgetNoEvents(android.content.Context context)Build a set of {@link RemoteViews} that describes an error state.
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
setNoEventsVisible(views, true);
// Clicking on widget launches the agenda view in Calendar
PendingIntent pendingIntent = getLaunchPendingIntent(context);
views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
return views;
| private android.widget.RemoteViews | getAppWidgetUpdate(android.content.Context context, android.database.Cursor cursor, com.android.providers.calendar.CalendarAppWidgetService$MarkedEvents events)Build a set of {@link RemoteViews} that describes how to update any
widget for a specific event instance.
Resources res = context.getResources();
ContentResolver resolver = context.getContentResolver();
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
setNoEventsVisible(views, false);
// Clicking on widget launches the agenda view in Calendar
PendingIntent pendingIntent = getLaunchPendingIntent(context);
views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
Time time = new Time();
time.setToNow();
int yearDay = time.yearDay;
int dateNumber = time.monthDay;
// Calendar header
String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1,
DateUtils.LENGTH_MEDIUM).toUpperCase();
views.setTextViewText(R.id.day_of_week, dayOfWeek);
views.setTextViewText(R.id.day_of_month, Integer.toString(time.monthDay));
// Fill primary event details
cursor.moveToPosition(events.primaryRow);
// Color stripe
int colorFilter = cursor.getInt(INDEX_COLOR);
views.setTextColor(R.id.when, colorFilter);
views.setTextColor(R.id.title, colorFilter);
views.setTextColor(R.id.where, colorFilter);
// When
long start = cursor.getLong(INDEX_BEGIN);
boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
int flags;
String whenString;
if (allDay) {
flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_UTC
| DateUtils.FORMAT_SHOW_DATE;
} else {
flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_TIME;
// Show date if different from today
time.set(start);
if (yearDay != time.yearDay) {
flags = flags | DateUtils.FORMAT_SHOW_DATE;
}
}
if (DateFormat.is24HourFormat(context)) {
flags |= DateUtils.FORMAT_24HOUR;
}
whenString = DateUtils.formatDateRange(context, start, start, flags);
views.setTextViewText(R.id.when, whenString);
// What
String titleString = cursor.getString(INDEX_TITLE);
if (titleString == null || titleString.length() == 0) {
titleString = context.getString(R.string.no_title_label);
}
views.setTextViewText(R.id.title, titleString);
// Conflicts
int titleLines = 4;
if (events.primaryCount > 1) {
int count = events.primaryCount - 1;
String conflictString = String.format(res.getQuantityString(
R.plurals.gadget_more_events, count), count);
views.setTextViewText(R.id.conflict, conflictString);
views.setViewVisibility(R.id.conflict, View.VISIBLE);
titleLines -= 1;
} else {
views.setViewVisibility(R.id.conflict, View.GONE);
}
// Where
String whereString = cursor.getString(INDEX_EVENT_LOCATION);
if (whereString != null && whereString.length() > 0) {
views.setViewVisibility(R.id.where, View.VISIBLE);
views.setTextViewText(R.id.where, whereString);
titleLines -= 1;
} else {
views.setViewVisibility(R.id.where, View.GONE);
}
// Trim title lines based on details shown. In landscape we're using
// singleLine which means this value is ignored.
views.setInt(R.id.title, "setMaxLines", titleLines);
return views;
| private long | getEventFlip(android.database.Cursor cursor, long start, long end, boolean allDay)Calculate flipping point for the given event; when we should hide this
event and show the next one. This is usually half-way through the event.
if (allDay) {
return start;
} else {
return (start + end) / 2;
}
| private android.app.PendingIntent | getLaunchPendingIntent(android.content.Context context)Build a {@link PendingIntent} to launch the Calendar app. This correctly
sets action, category, and flags so that we don't duplicate tasks when
Calendar was also launched from a normal desktop icon.
Intent launchIntent = new Intent();
launchIntent.setComponent(new ComponentName(ACTION_PACKAGE, ACTION_CLASS));
launchIntent.setAction(Intent.ACTION_MAIN);
launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
return PendingIntent.getActivity(context, 0 /* no requestCode */,
launchIntent, 0 /* no flags */);
| private android.database.Cursor | getUpcomingInstancesCursor(android.content.ContentResolver resolver, long searchDuration, long now)Query across all calendars for upcoming event instances from now until
some time in the future.
// Search for events from now until some time in the future
long end = now + searchDuration;
Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
String.format("%d/%d", now, end));
String selection = String.format("%s=1 AND %s!=%d",
Calendars.SELECTED, Instances.SELF_ATTENDEE_STATUS,
Attendees.ATTENDEE_STATUS_DECLINED);
return resolver.query(uri, EVENT_PROJECTION, selection, null,
EVENT_SORT_ORDER);
| public android.os.IBinder | onBind(android.content.Intent intent)
return null;
| public void | onStart(android.content.Intent intent, int startId)
super.onStart(intent, startId);
// Only start processing thread if not already running
synchronized (AppWidgetShared.sLock) {
if (!AppWidgetShared.sUpdateRunning) {
if (LOGD) Log.d(TAG, "no thread running, so starting new one");
AppWidgetShared.sUpdateRunning = true;
new Thread(this).start();
}
}
| private void | performUpdate(android.content.Context context, int[] appWidgetIds, java.util.Set changedEventIds, long now)Process and push out an update for the given appWidgetIds.
ContentResolver resolver = context.getContentResolver();
Cursor cursor = null;
RemoteViews views = null;
long triggerTime = -1;
try {
cursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now);
if (cursor != null) {
MarkedEvents events = buildMarkedEvents(cursor, changedEventIds, now);
boolean shouldUpdate = true;
if (changedEventIds.size() > 0) {
shouldUpdate = events.watchFound;
}
if (events.primaryCount == 0) {
views = getAppWidgetNoEvents(context);
} else if (shouldUpdate) {
views = getAppWidgetUpdate(context, cursor, events);
triggerTime = calculateUpdateTime(cursor, events);
}
} else {
views = getAppWidgetNoEvents(context);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// Bail out early if no update built
if (views == null) {
if (LOGD) Log.d(TAG, "Didn't build update, possibly because changedEventIds=" +
changedEventIds.toString());
return;
}
AppWidgetManager gm = AppWidgetManager.getInstance(context);
if (appWidgetIds != null && appWidgetIds.length > 0) {
gm.updateAppWidget(appWidgetIds, views);
} else {
ComponentName thisWidget = CalendarAppWidgetProvider.getComponentName(context);
gm.updateAppWidget(thisWidget, views);
}
// Schedule an alarm to wake ourselves up for the next update. We also cancel
// all existing wake-ups because PendingIntents don't match against extras.
// If no next-update calculated, or bad trigger time in past, schedule
// update about six hours from now.
if (triggerTime == -1 || triggerTime < now) {
if (LOGD) Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now));
triggerTime = now + UPDATE_NO_EVENTS;
}
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(context);
am.cancel(pendingUpdate);
am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
if (LOGD) Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now));
| public void | run()Thread loop to handle
while (true) {
long now = -1;
int[] appWidgetIds;
Set<Long> changedEventIds;
synchronized (AppWidgetShared.sLock) {
// Bail out if no remaining updates
if (!AppWidgetShared.sUpdateRequested) {
// Clear current shared state, release wakelock, and stop service
if (LOGD) Log.d(TAG, "no requested update or expired wakelock, bailing");
AppWidgetShared.clearLocked();
stopSelf();
return;
}
// Clear requested flag and collect latest parameters
AppWidgetShared.sUpdateRequested = false;
now = AppWidgetShared.sLastRequest;
appWidgetIds = AppWidgetShared.collectAppWidgetIdsLocked();
changedEventIds = AppWidgetShared.collectChangedEventIdsLocked();
}
// Process this update
if (LOGD) Log.d(TAG, "processing requested update now=" + now);
performUpdate(this, appWidgetIds, changedEventIds, now);
}
| private void | setNoEventsVisible(android.widget.RemoteViews views, boolean noEvents)Set visibility of various widget components if there are events, or if no
events were found.
views.setViewVisibility(R.id.no_events, noEvents ? View.VISIBLE : View.GONE);
int otherViews = noEvents ? View.GONE : View.VISIBLE;
views.setViewVisibility(R.id.day_of_month, otherViews);
views.setViewVisibility(R.id.day_of_week, otherViews);
views.setViewVisibility(R.id.divider, otherViews);
views.setViewVisibility(R.id.when, otherViews);
views.setViewVisibility(R.id.title, otherViews);
// Don't force-show views that are handled elsewhere
if (noEvents) {
views.setViewVisibility(R.id.conflict, otherViews);
views.setViewVisibility(R.id.where, otherViews);
}
|
|