FileDocCategorySizeDatePackage
CalendarAppWidgetService.javaAPI DocAndroid 1.5 API21330Wed May 06 22:42:48 BST 2009com.android.providers.calendar

CalendarAppWidgetService

public 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
Constructors Summary
Methods Summary
private com.android.providers.calendar.CalendarAppWidgetService$MarkedEventsbuildMarkedEvents(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.

param
cursor Valid cursor across {@link Instances#CONTENT_URI}.
param
watchEventIds Specific events to watch for, setting {@link MarkedEvents#watchFound} if found during marking.
param
now Current system time to use for this update, possibly from {@link System#currentTimeMillis()}

    

                                                                                                 
            
        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 longcalculateUpdateTime(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)}.

param
cursor Valid cursor on {@link Instances#CONTENT_URI}
param
events {@link MarkedEvents} parsed from the cursor

        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 longconvertUtcToLocal(android.text.format.Time recycle, long utcTime)
Convert given UTC time into current local time.

param
recycle Time object to recycle, otherwise null.
param
utcTime Time to convert, in UTC.

        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.StringformatDebugTime(long unixTime, long now)
Format given time for debugging output.

param
unixTime Target time to report.
param
now Current system time from {@link System#currentTimeMillis()} for calculating time difference.

        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.RemoteViewsgetAppWidgetNoEvents(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.RemoteViewsgetAppWidgetUpdate(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.

param
cursor Valid cursor on {@link Instances#CONTENT_URI}
param
events {@link MarkedEvents} parsed from the cursor

        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 longgetEventFlip(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.

param
start Event start time in local timezone.
param
end Event end time in local timezone.

        if (allDay) {
            return start;
        } else {
            return (start + end) / 2;
        }
    
private android.app.PendingIntentgetLaunchPendingIntent(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.CursorgetUpcomingInstancesCursor(android.content.ContentResolver resolver, long searchDuration, long now)
Query across all calendars for upcoming event instances from now until some time in the future.

param
resolver {@link ContentResolver} to use when querying {@link Instances#CONTENT_URI}.
param
searchDuration Distance into the future to look for event instances, in milliseconds.
param
now Current system time to use for this update, possibly from {@link System#currentTimeMillis()}.

        // 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.IBinderonBind(android.content.Intent intent)

        return null;
    
public voidonStart(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 voidperformUpdate(android.content.Context context, int[] appWidgetIds, java.util.Set changedEventIds, long now)
Process and push out an update for the given appWidgetIds.

param
context Context to use when updating widget.
param
appWidgetIds List of appWidgetIds to update, or null for all.
param
changedEventIds Specific events known to be changed, otherwise null. If present, we use to decide if an update is necessary.
param
now System clock time to use during this update.

        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 voidrun()
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 voidsetNoEventsVisible(android.widget.RemoteViews views, boolean noEvents)
Set visibility of various widget components if there are events, or if no events were found.

param
views Set of {@link RemoteViews} to apply visibility.
param
noEvents True if no events found, otherwise false.

        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);
        }