auto import from //branches/cupcake_rel/...@138607
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e64b0e3..700096a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -64,5 +64,6 @@
                 <action android:name="android.intent.action.DATE_CHANGED" />
             </intent-filter>
         </receiver>
+        <service android:name=".CalendarAppWidgetService" />
     </application>
 </manifest>
diff --git a/src/com/android/providers/calendar/AppWidgetShared.java b/src/com/android/providers/calendar/AppWidgetShared.java
new file mode 100644
index 0000000..cb155b2
--- /dev/null
+++ b/src/com/android/providers/calendar/AppWidgetShared.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.calendar;
+
+import android.app.Service;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Set of static variables that are shared between
+ * {@link CalendarAppWidgetProvider} and {@link CalendarAppWidgetService} and
+ * guarded by {@link #sLock}.
+ */
+public class AppWidgetShared {
+    static final String TAG = "AppWidgetShared";
+    // STOPSHIP: remove this debugging flag
+    static final boolean LOGD = true;
+    
+    static Object sLock = new Object();
+    static WakeLock sWakeLock;
+    static boolean sUpdateRequested = false;
+    static boolean sUpdateRunning = false;
+    
+    /**
+     * {@link System#currentTimeMillis()} at last update request. 
+     */
+    static long sLastRequest = -1;
+
+    private static HashSet<Integer> sAppWidgetIds = new HashSet<Integer>();
+    private static HashSet<Long> sChangedEventIds = new HashSet<Long>();
+    
+    /**
+     * Merge a set of filtering appWidgetIds with those from other pending
+     * requests. If null, then reset the filter to match all.
+     * <p>
+     * Only call this while holding a {@link #sLock} lock.
+     */
+    static void mergeAppWidgetIdsLocked(int[] appWidgetIds) {
+        if (appWidgetIds != null) {
+            for (int appWidgetId : appWidgetIds) {
+                sAppWidgetIds.add(appWidgetId);
+            }
+        } else {
+            sAppWidgetIds.clear();
+        }
+    }
+    
+    /**
+     * Merge a set of filtering changedEventIds with those from other pending
+     * requests. If null, then reset the filter to match all.
+     * <p>
+     * Only call this while holding a {@link #sLock} lock.
+     */
+    static void mergeChangedEventIdsLocked(long[] changedEventIds) {
+        if (changedEventIds != null) {
+            for (long changedEventId : changedEventIds) {
+                sChangedEventIds.add(changedEventId);
+            }
+        } else {
+            sChangedEventIds.clear();
+        }
+    }
+
+    /**
+     * Collect all currently requested appWidgetId filters, returning as single
+     * list. This call also clears the internal list.
+     * <p>
+     * Only call this while holding a {@link #sLock} lock.
+     */
+    static int[] collectAppWidgetIdsLocked() {
+        final int size = sAppWidgetIds.size();
+        int[] array = new int[size];
+        Iterator<Integer> iterator = sAppWidgetIds.iterator();
+        for (int i = 0; i < size; i++) {
+            array[i] = iterator.next();
+        }
+        sAppWidgetIds.clear();
+        return array;
+    }
+
+    /**
+     * Collect all currently requested changedEventId filters, returning as
+     * single list. This call also clears the internal list.
+     * <p>
+     * Only call this while holding a {@link #sLock} lock.
+     */
+    static Set<Long> collectChangedEventIdsLocked() {
+        Set<Long> set = new HashSet<Long>();
+        for (Long value : sChangedEventIds) {
+            set.add(value);
+        }
+        sChangedEventIds.clear();
+        return set;
+    }
+
+    /**
+     * Call this at any point to release any {@link WakeLock} and reset to
+     * default state. Usually called before {@link Service#stopSelf()}.
+     * <p>
+     * Only call this while holding a {@link #sLock} lock.
+     */
+    static void clearLocked() {
+        if (sWakeLock != null && sWakeLock.isHeld()) {
+            if (LOGD) Log.d(TAG, "found held wakelock, so releasing");
+            sWakeLock.release();
+        }
+        sWakeLock = null;
+        
+        sUpdateRequested = false;
+        sUpdateRunning = false;
+        
+        sAppWidgetIds.clear();
+        sChangedEventIds.clear();
+    }
+}
diff --git a/src/com/android/providers/calendar/CalendarAppWidgetProvider.java b/src/com/android/providers/calendar/CalendarAppWidgetProvider.java
index 02e83ec..7050060 100644
--- a/src/com/android/providers/calendar/CalendarAppWidgetProvider.java
+++ b/src/com/android/providers/calendar/CalendarAppWidgetProvider.java
@@ -21,77 +21,25 @@
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProvider;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.Typeface;
-import android.graphics.Paint.FontMetrics;
-import android.net.Uri;
-import android.provider.Calendar.Attendees;
-import android.provider.Calendar.Calendars;
-import android.provider.Calendar.Instances;
-import android.text.format.DateFormat;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
 import android.text.format.DateUtils;
-import android.text.format.Time;
 import android.util.Log;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import java.util.TimeZone;
 
 /**
  * Simple widget to show next upcoming calendar event.
  */
 public class CalendarAppWidgetProvider extends AppWidgetProvider {
     static final String TAG = "CalendarAppWidgetProvider";
+    // STOPSHIP: remove this debugging flag
     static final boolean LOGD = true;
-    
-    static final String EVENT_SORT_ORDER = "startDay ASC, allDay DESC, begin ASC";
 
-    static final String[] EVENT_PROJECTION = new String[] {
-        Instances.ALL_DAY,
-        Instances.BEGIN,
-        Instances.END,
-        Instances.COLOR,
-        Instances.TITLE,
-        Instances.RRULE,
-        Instances.HAS_ALARM,
-        Instances.EVENT_LOCATION,
-        Instances.CALENDAR_ID,
-        Instances.EVENT_ID,
-    };
-
-    static final int INDEX_ALL_DAY = 0;
-    static final int INDEX_BEGIN = 1;
-    static final int INDEX_END = 2;
-    static final int INDEX_COLOR = 3;
-    static final int INDEX_TITLE = 4;
-    static final int INDEX_RRULE = 5;
-    static final int INDEX_HAS_ALARM = 6;
-    static final int INDEX_EVENT_LOCATION = 7;
-    static final int INDEX_CALENDAR_ID = 8;
-    static final int INDEX_EVENT_ID = 9;
+    static final String ACTION_CALENDAR_APPWIDGET_UPDATE =
+            "com.android.providers.calendar.APPWIDGET_UPDATE";
     
-    static final long SEARCH_DURATION = DateUtils.WEEK_IN_MILLIS;
-    
-    static final long UPDATE_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
-
-    static final String ACTION_CALENDAR_APPWIDGET_UPDATE = "com.android.calendar.APPWIDGET_UPDATE";
-    
-    static final String PACKAGE_DETAIL = "com.android.calendar";
-    static final String CLASS_DETAIL = "com.android.calendar.AgendaActivity";
-    
-    static final ComponentName THIS_APPWIDGET =
-        new ComponentName("com.android.providers.calendar",
-                "com.android.providers.calendar.CalendarAppWidgetProvider");
-
     /**
      * Threshold to check against when building widget updates. If system clock
      * has changed less than this amount, we consider ignoring the request.
@@ -99,10 +47,10 @@
     static final long UPDATE_THRESHOLD = DateUtils.MINUTE_IN_MILLIS;
 
     /**
-     * Last widget update, as provided by {@link System#currentTimeMillis()}
+     * Maximum time to hold {@link WakeLock} when performing widget updates.
      */
-    private static long sLastUpdate = -1;
-    
+    static final long WAKE_LOCK_TIMEOUT = DateUtils.MINUTE_IN_MILLIS;
+
     private static CalendarAppWidgetProvider sInstance;
     
     static synchronized CalendarAppWidgetProvider getInstance() {
@@ -122,7 +70,7 @@
         final String action = intent.getAction();
         if (ACTION_CALENDAR_APPWIDGET_UPDATE.equals(action)) {
             performUpdate(context, null /* all widgets */,
-                    -1 /* no eventId */, false /* don't ignore */);
+                    null /* no eventIds */, false /* don't ignore */);
         } else {
             super.onReceive(context, intent);
         }
@@ -164,7 +112,7 @@
      */
     @Override
     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
-        performUpdate(context, appWidgetIds, -1 /* no eventId */, false /* don't ignore */);
+        performUpdate(context, appWidgetIds, null /* no eventIds */, false /* force */);
     }
     
     /**
@@ -172,9 +120,18 @@
      */
     private boolean hasInstances(Context context) {
         AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
-        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(THIS_APPWIDGET);
+        ComponentName thisAppWidget = getComponentName(context);
+        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
         return (appWidgetIds.length > 0);
     }
+    
+    /**
+     * Build {@link ComponentName} describing this specific
+     * {@link AppWidgetProvider}
+     */
+    static ComponentName getComponentName(Context context) {
+        return new ComponentName(context, CalendarAppWidgetProvider.class);
+    }
 
     /**
      * The {@link CalendarProvider} has been updated, which means we should push
@@ -187,7 +144,7 @@
     void providerUpdated(Context context, long changedEventId) {
         if (hasInstances(context)) {
             performUpdate(context, null /* all widgets */,
-                    changedEventId, false /* don't ignore */);
+                    new long[] { changedEventId }, false /* force */);
         }
     }
 
@@ -195,537 +152,89 @@
      * {@link TimeChangeReceiver} has triggered that the time changed.
      * 
      * @param context Context to use when creating widget.
-     * @param considerIgnore If true, compare {@link #sLastUpdate} against
+     * @param considerIgnore If true, compare
+     *            {@link AppWidgetShared#sLastRequest} against
      *            {@link #UPDATE_THRESHOLD} to consider ignoring this update
      *            request.
      */
     void timeUpdated(Context context, boolean considerIgnore) {
         if (hasInstances(context)) {
-            performUpdate(context, null /* all widgets */,
-                    -1 /* no eventId */, considerIgnore);
+            performUpdate(context, null /* all widgets */, null /* no events */, considerIgnore);
         }
     }
-
+    
     /**
-     * Process and push out an update for the given appWidgetIds.
+     * Process and push out an update for the given appWidgetIds. This call
+     * actually fires an intent to start {@link CalendarAppWidgetService} as a
+     * background service which handles the actual update, to prevent ANR'ing
+     * during database queries.
+     * <p>
+     * This call will acquire a single {@link WakeLock} and set a flag that an
+     * update has been requested.
      * 
-     * @param context Context to use when creating widget.
-     * @param appWidgetIds List of specific appWidgetIds to update, or null for all.
-     * @param changedEventId Specific event known to be changed, otherwise -1.
-     *            If present, we use it to decide if an update is necessary.
-     * @param considerIgnore If true, compare {@link #sLastUpdate} against
+     * @param context Context to use when acquiring {@link WakeLock} and
+     *            starting {@link CalendarAppWidgetService}.
+     * @param appWidgetIds List of specific appWidgetIds to update, or null for
+     *            all.
+     * @param changedEventIds Specific events known to be changed. If present,
+     *            we use it to decide if an update is necessary.
+     * @param considerIgnore If true, compare
+     *            {@link AppWidgetShared#sLastRequest} against
      *            {@link #UPDATE_THRESHOLD} to consider ignoring this update
      *            request.
      */
     private void performUpdate(Context context, int[] appWidgetIds,
-            long changedEventId, boolean considerIgnore) {
-        ContentResolver resolver = context.getContentResolver();
-        
-        long now = System.currentTimeMillis();
-        
-        // Check against delta if we have a last-updated value
-        if (considerIgnore && sLastUpdate != -1) {
-            long delta = Math.abs(now - sLastUpdate);
-            if (delta < UPDATE_THRESHOLD) {
-                if (LOGD) Log.d(TAG, "Ignoring update request because delta=" + delta);
-                return;
-            }
-        }
-        sLastUpdate = now;
-        
-        Cursor cursor = null;
-        RemoteViews views = null;
-        long triggerTime = -1;
-
-        try {
-            cursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now);
-            if (cursor != null) {
-                MarkedEvents events = buildMarkedEvents(cursor, changedEventId, now);
-                
-                boolean shouldUpdate = true;
-                if (changedEventId != -1) {
-                    shouldUpdate = events.watchFound;
+            long[] changedEventIds, boolean considerIgnore) {
+        synchronized (AppWidgetShared.sLock) {
+            // Consider ignoring this update request if inside threshold. This
+            // check is inside the lock because we depend on this "now" time.
+            long now = System.currentTimeMillis();
+            if (considerIgnore && AppWidgetShared.sLastRequest != -1) {
+                long delta = Math.abs(now - AppWidgetShared.sLastRequest);
+                if (delta < UPDATE_THRESHOLD) {
+                    if (LOGD) Log.d(TAG, "Ignoring update request because delta=" + delta);
+                    return;
                 }
-                
-                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();
+            
+            // We need to update, so make sure we have a valid, held wakelock
+            if (AppWidgetShared.sWakeLock == null ||
+                    !AppWidgetShared.sWakeLock.isHeld()) {
+                if (LOGD) Log.d(TAG, "no held wakelock found, so acquiring new one");
+                PowerManager powerManager = (PowerManager)
+                        context.getSystemService(Context.POWER_SERVICE);
+                AppWidgetShared.sWakeLock =
+                        powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+                AppWidgetShared.sWakeLock.setReferenceCounted(false);
+                AppWidgetShared.sWakeLock.acquire(WAKE_LOCK_TIMEOUT);
             }
+            
+            if (LOGD) Log.d(TAG, "setting request now=" + now);
+            AppWidgetShared.sLastRequest = now;
+            AppWidgetShared.sUpdateRequested = true;
+            
+            // Apply filters that would limit the scope of this update, or clear
+            // any pending filters if all requested.
+            AppWidgetShared.mergeAppWidgetIdsLocked(appWidgetIds);
+            AppWidgetShared.mergeChangedEventIdsLocked(changedEventIds);
+            
+            // Launch over to service so it can perform update
+            final Intent updateIntent = new Intent(context, CalendarAppWidgetService.class);
+            context.startService(updateIntent);
         }
-        
-        // Bail out early if no update built
-        if (views == null) {
-            if (LOGD) Log.d(TAG, "Didn't build update, possibly because changedEventId=" + changedEventId);
-            return;
-        }
-        
-        AppWidgetManager gm = AppWidgetManager.getInstance(context);
-        if (appWidgetIds != null) {
-            gm.updateAppWidget(appWidgetIds, views);
-        } else {
-            gm.updateAppWidget(THIS_APPWIDGET, 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 = getUpdateIntent(context);
-        
-        am.cancel(pendingUpdate);
-        am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
-
-        if (LOGD) Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now));
     }
     
     /**
      * Build the {@link PendingIntent} used to trigger an update of all calendar
-     * widgets. Uses {@link ACTION_CALENDAR_APPWIDGET_UPDATE} to directly target
+     * widgets. Uses {@link #ACTION_CALENDAR_APPWIDGET_UPDATE} to directly target
      * all widgets instead of using {@link AppWidgetManager#EXTRA_APPWIDGET_IDS}.
      * 
      * @param context Context to use when building broadcast.
      */
-    private PendingIntent getUpdateIntent(Context context) {
+    static PendingIntent getUpdateIntent(Context context) {
         Intent updateIntent = new Intent(ACTION_CALENDAR_APPWIDGET_UPDATE);
         updateIntent.setComponent(new ComponentName(context, CalendarAppWidgetProvider.class));
         return PendingIntent.getBroadcast(context, 0 /* no requestCode */,
                 updateIntent, 0 /* no flags */);
     }
-
-    /**
-     * 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.
-     */
-    private String formatDebugTime(long unixTime, long now) {
-        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);
-        }
-    }
-    
-    /**
-     * Convert given UTC time into current local time.
-     * 
-     * @param recycle Time object to recycle, otherwise null.
-     * @param utcTime Time to convert, in UTC.
-     */
-    private long convertUtcToLocal(Time recycle, long utcTime) {
-        if (recycle == null) {
-            recycle = new Time();
-        }
-        recycle.timezone = Time.TIMEZONE_UTC;
-        recycle.set(utcTime);
-        recycle.timezone = TimeZone.getDefault().getID();
-        return recycle.normalize(true);
-    }
-    
-    /**
-     * Figure out the next time we should push widget updates. This is based on
-     * the time calculated by {@link #getEventFlip(Cursor, int)}.
-     * 
-     * @param cursor Valid cursor on {@link Instances#CONTENT_URI}
-     * @param events {@link MarkedEvents} parsed from the cursor
-     */
-    private long calculateUpdateTime(Cursor cursor, MarkedEvents events) {
-        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;
-    }
-    
-    /**
-     * 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.
-     */
-    private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) {
-        if (allDay) {
-            return start;
-        } else {
-            return (start + end) / 2;
-        }
-    }
-    
-    /**
-     * 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
-     */
-    private RemoteViews getAppWidgetUpdate(Context context, Cursor cursor, MarkedEvents events) {
-        Resources res = context.getResources();
-        ContentResolver resolver = context.getContentResolver();
-        
-        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
-        
-        // Clicking on widget launches the agenda view in Calendar
-        Intent agendaIntent = new Intent();
-        agendaIntent.setComponent(new ComponentName(PACKAGE_DETAIL, CLASS_DETAIL));
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
-                agendaIntent, 0 /* no flags */);
-        
-        views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
-        
-        Time time = new Time();
-        time.setToNow();
-        int yearDay = time.yearDay;
-        int dateNumber = time.monthDay;
-
-        // Set calendar icon with actual date
-        views.setImageViewBitmap(R.id.icon, getDateOverlay(res, dateNumber));
-        views.setViewVisibility(R.id.icon, View.VISIBLE);
-        views.setViewVisibility(R.id.no_events, View.GONE);
-
-        // Fill primary event details
-        if (events.primaryRow != -1) {
-            views.setViewVisibility(R.id.primary_card, View.VISIBLE);
-            cursor.moveToPosition(events.primaryRow);
-            
-            // Color stripe
-            int colorFilter = cursor.getInt(INDEX_COLOR);
-            views.setDrawableParameters(R.id.when, true, -1, colorFilter,
-                    PorterDuff.Mode.SRC_IN, -1);
-            views.setTextColor(R.id.title, colorFilter);
-            views.setTextColor(R.id.where, colorFilter);
-            views.setDrawableParameters(R.id.divider, true, -1, colorFilter,
-                    PorterDuff.Mode.SRC_IN, -1);
-            views.setTextColor(R.id.title2, 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);
-            
-            // Where
-            String whereString = cursor.getString(INDEX_EVENT_LOCATION);
-            if (whereString != null && whereString.length() > 0) {
-                views.setViewVisibility(R.id.where, View.VISIBLE);
-                views.setViewVisibility(R.id.stub_where, View.INVISIBLE);
-                views.setTextViewText(R.id.where, whereString);
-            } else {
-                views.setViewVisibility(R.id.where, View.GONE);
-                views.setViewVisibility(R.id.stub_where, View.GONE);
-            }
-        }
-        
-        // Fill other primary events, if present
-        if (events.primaryConflictRow != -1) {
-            views.setViewVisibility(R.id.divider, View.VISIBLE);
-            views.setViewVisibility(R.id.title2, View.VISIBLE);
-
-            if (events.primaryCount > 2) {
-                // If more than two primary conflicts, format multiple message
-                int count = events.primaryCount - 1;
-                String titleString = String.format(res.getQuantityString(
-                        R.plurals.gadget_more_events, count), count);
-                views.setTextViewText(R.id.title2, titleString);
-            } else {
-                cursor.moveToPosition(events.primaryConflictRow);
-
-                // 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.title2, titleString);
-            }
-        } else {
-            views.setViewVisibility(R.id.divider, View.GONE);
-            views.setViewVisibility(R.id.title2, View.GONE);
-        }
-        
-        // Fill secondary event
-        if (events.secondaryRow != -1) {
-            views.setViewVisibility(R.id.secondary_card, View.VISIBLE);
-            views.setViewVisibility(R.id.secondary_when, View.VISIBLE);
-            views.setViewVisibility(R.id.secondary_title, View.VISIBLE);
-            
-            cursor.moveToPosition(events.secondaryRow);
-            
-            // Color stripe
-            int colorFilter = cursor.getInt(INDEX_COLOR);
-            views.setDrawableParameters(R.id.secondary_when, true, -1, colorFilter,
-                    PorterDuff.Mode.SRC_IN, -1);
-            views.setTextColor(R.id.secondary_title, 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.secondary_when, whenString);
-            
-            if (events.secondaryCount > 1) {
-                // If more than two secondary conflicts, format multiple message
-                int count = events.secondaryCount;
-                String titleString = String.format(res.getQuantityString(
-                        R.plurals.gadget_more_events, count), count);
-                views.setTextViewText(R.id.secondary_title, titleString);
-            } else {
-                // 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.secondary_title, titleString);
-            }
-        } else {
-            views.setViewVisibility(R.id.secondary_when, View.GONE);
-            views.setViewVisibility(R.id.secondary_title, View.GONE);
-        }
-        
-        return views;
-    }
-
-    /**
-     * Build date overlay bitmap for positioning on widget. This is the textual
-     * number that has been corrected for font leading issues.
-     * 
-     * @param res {@link Resources} to use for paint color.
-     * @param dateNumber Numerical date to display.
-     */
-    public Bitmap getDateOverlay(Resources res, int dateNumber) {
-        String dateString = Integer.toString(dateNumber);
-        
-        Paint paint = new Paint();
-        paint.setTypeface(Typeface.DEFAULT_BOLD);
-        paint.setTextSize(18);
-        paint.setAntiAlias(true);
-        paint.setSubpixelText(true);
-        paint.setColor(res.getColor(R.color.appwidget_date));
-        
-        // Calculate exact size of text
-        FontMetrics metrics = paint.getFontMetrics();
-        int width = (int) Math.ceil(paint.measureText(dateString));
-        int height = (int) Math.ceil(-metrics.top + metrics.descent);
-        
-        // Add padding to left edge based on various obscure rules
-        // to make font center correctly
-        char firstChar = dateString.charAt(0);
-        char lastChar = dateString.charAt(dateString.length() - 1);
-        int leftPadding = (dateString.length() == 1 || firstChar == '2' ||
-                lastChar == '1' || lastChar == '0') ? 1 : 0;
-        
-        Bitmap overlay = Bitmap.createBitmap(width + leftPadding,
-                height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(overlay);
-        canvas.drawText(dateString, leftPadding, -metrics.top, paint);
-        
-        return overlay;
-    }
-    
-    /**
-     * Build a set of {@link RemoteViews} that describes an error state.
-     */
-    private RemoteViews getAppWidgetNoEvents(Context context) {
-        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
-
-        views.setViewVisibility(R.id.no_events, View.VISIBLE);
-        
-        views.setViewVisibility(R.id.icon, View.GONE);
-        views.setViewVisibility(R.id.primary_card, View.GONE);
-        views.setViewVisibility(R.id.secondary_card, View.GONE);
-        
-        // Clicking on widget launches the agenda view in Calendar
-        Intent agendaIntent = new Intent();
-        agendaIntent.setComponent(new ComponentName(PACKAGE_DETAIL, CLASS_DETAIL));
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
-                agendaIntent, 0 /* no flags */);
-        
-        views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
-
-        return views;
-    }
-    
-    private static class MarkedEvents {
-        long primaryTime = -1;
-        int primaryRow = -1;
-        int primaryConflictRow = -1;
-        int primaryCount = 0;
-        long secondaryTime = -1;
-        int secondaryRow = -1;
-        int secondaryCount = 0;
-        boolean watchFound = false;
-    }
-
-    /**
-     * 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 watchEventId Specific event 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()}
-     */
-    private MarkedEvents buildMarkedEvents(Cursor cursor, long watchEventId, long now) {
-        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 (eventId == watchEventId) {
-                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;
-    }
-
-    /**
-     * 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()}.
-     */
-    private Cursor getUpcomingInstancesCursor(ContentResolver resolver,
-            long searchDuration, long now) {
-        // 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);
-    }
-    
 }
diff --git a/src/com/android/providers/calendar/CalendarAppWidgetService.java b/src/com/android/providers/calendar/CalendarAppWidgetService.java
new file mode 100644
index 0000000..b88d2e4
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarAppWidgetService.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.calendar;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.graphics.Paint.FontMetrics;
+import android.net.Uri;
+import android.os.IBinder;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Instances;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import java.util.Set;
+import java.util.TimeZone;
+
+
+public class CalendarAppWidgetService extends Service implements Runnable {
+    static final String TAG = "CalendarAppWidgetService";
+    // STOPSHIP: remove this debugging flag
+    static final boolean LOGD = true;
+    
+    static final String EVENT_SORT_ORDER = "startDay ASC, allDay DESC, begin ASC";
+
+    static final String[] EVENT_PROJECTION = new String[] {
+        Instances.ALL_DAY,
+        Instances.BEGIN,
+        Instances.END,
+        Instances.COLOR,
+        Instances.TITLE,
+        Instances.RRULE,
+        Instances.HAS_ALARM,
+        Instances.EVENT_LOCATION,
+        Instances.CALENDAR_ID,
+        Instances.EVENT_ID,
+    };
+
+    static final int INDEX_ALL_DAY = 0;
+    static final int INDEX_BEGIN = 1;
+    static final int INDEX_END = 2;
+    static final int INDEX_COLOR = 3;
+    static final int INDEX_TITLE = 4;
+    static final int INDEX_RRULE = 5;
+    static final int INDEX_HAS_ALARM = 6;
+    static final int INDEX_EVENT_LOCATION = 7;
+    static final int INDEX_CALENDAR_ID = 8;
+    static final int INDEX_EVENT_ID = 9;
+    
+    static final long SEARCH_DURATION = DateUtils.WEEK_IN_MILLIS;
+    
+    static final long UPDATE_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
+    
+    static final String ACTION_PACKAGE = "com.android.calendar";
+    static final String ACTION_CLASS = "com.android.calendar.AgendaActivity";
+    
+    @Override
+    public void onStart(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();
+            }
+        }
+    }
+    
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+    
+    /**
+     * Thread loop to handle 
+     */
+    public void run() {
+        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);
+        }
+    }
+
+    /**
+     * 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.
+     */
+    private void performUpdate(Context context, int[] appWidgetIds,
+            Set<Long> changedEventIds, long now) {
+        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) {
+            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));
+    }
+
+    /**
+     * 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.
+     */
+    private String formatDebugTime(long unixTime, long now) {
+        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);
+        }
+    }
+    
+    /**
+     * Convert given UTC time into current local time.
+     * 
+     * @param recycle Time object to recycle, otherwise null.
+     * @param utcTime Time to convert, in UTC.
+     */
+    private long convertUtcToLocal(Time recycle, long utcTime) {
+        if (recycle == null) {
+            recycle = new Time();
+        }
+        recycle.timezone = Time.TIMEZONE_UTC;
+        recycle.set(utcTime);
+        recycle.timezone = TimeZone.getDefault().getID();
+        return recycle.normalize(true);
+    }
+
+    /**
+     * 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
+     */
+    private long calculateUpdateTime(Cursor cursor, MarkedEvents events) {
+        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;
+    }
+    
+    /**
+     * 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.
+     */
+    private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) {
+        if (allDay) {
+            return start;
+        } else {
+            return (start + end) / 2;
+        }
+    }
+    
+    /**
+     * 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
+     */
+    private RemoteViews getAppWidgetUpdate(Context context, Cursor cursor, MarkedEvents events) {
+        Resources res = context.getResources();
+        ContentResolver resolver = context.getContentResolver();
+        
+        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
+        
+        // Clicking on widget launches the agenda view in Calendar
+        Intent agendaIntent = new Intent();
+        agendaIntent.setComponent(new ComponentName(ACTION_PACKAGE, ACTION_CLASS));
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
+                agendaIntent, 0 /* no flags */);
+        
+        views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
+        
+        Time time = new Time();
+        time.setToNow();
+        int yearDay = time.yearDay;
+        int dateNumber = time.monthDay;
+
+        // Set calendar icon with actual date
+        views.setImageViewBitmap(R.id.icon, getDateOverlay(res, dateNumber));
+        views.setViewVisibility(R.id.icon, View.VISIBLE);
+        views.setViewVisibility(R.id.no_events, View.GONE);
+
+        // Fill primary event details
+        if (events.primaryRow != -1) {
+            views.setViewVisibility(R.id.primary_card, View.VISIBLE);
+            cursor.moveToPosition(events.primaryRow);
+            
+            // Color stripe
+            int colorFilter = cursor.getInt(INDEX_COLOR);
+            views.setDrawableParameters(R.id.when, true, -1, colorFilter,
+                    PorterDuff.Mode.SRC_IN, -1);
+            views.setTextColor(R.id.title, colorFilter);
+            views.setTextColor(R.id.where, colorFilter);
+            views.setDrawableParameters(R.id.divider, true, -1, colorFilter,
+                    PorterDuff.Mode.SRC_IN, -1);
+            views.setTextColor(R.id.title2, 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);
+            
+            // Where
+            String whereString = cursor.getString(INDEX_EVENT_LOCATION);
+            if (whereString != null && whereString.length() > 0) {
+                views.setViewVisibility(R.id.where, View.VISIBLE);
+                views.setViewVisibility(R.id.stub_where, View.INVISIBLE);
+                views.setTextViewText(R.id.where, whereString);
+            } else {
+                views.setViewVisibility(R.id.where, View.GONE);
+                views.setViewVisibility(R.id.stub_where, View.GONE);
+            }
+        }
+        
+        // Fill other primary events, if present
+        if (events.primaryConflictRow != -1) {
+            views.setViewVisibility(R.id.divider, View.VISIBLE);
+            views.setViewVisibility(R.id.title2, View.VISIBLE);
+
+            if (events.primaryCount > 2) {
+                // If more than two primary conflicts, format multiple message
+                int count = events.primaryCount - 1;
+                String titleString = String.format(res.getQuantityString(
+                        R.plurals.gadget_more_events, count), count);
+                views.setTextViewText(R.id.title2, titleString);
+            } else {
+                cursor.moveToPosition(events.primaryConflictRow);
+
+                // 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.title2, titleString);
+            }
+        } else {
+            views.setViewVisibility(R.id.divider, View.GONE);
+            views.setViewVisibility(R.id.title2, View.GONE);
+        }
+        
+        // Fill secondary event
+        if (events.secondaryRow != -1) {
+            views.setViewVisibility(R.id.secondary_card, View.VISIBLE);
+            views.setViewVisibility(R.id.secondary_when, View.VISIBLE);
+            views.setViewVisibility(R.id.secondary_title, View.VISIBLE);
+            
+            cursor.moveToPosition(events.secondaryRow);
+            
+            // Color stripe
+            int colorFilter = cursor.getInt(INDEX_COLOR);
+            views.setDrawableParameters(R.id.secondary_when, true, -1, colorFilter,
+                    PorterDuff.Mode.SRC_IN, -1);
+            views.setTextColor(R.id.secondary_title, 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.secondary_when, whenString);
+            
+            if (events.secondaryCount > 1) {
+                // If more than two secondary conflicts, format multiple message
+                int count = events.secondaryCount;
+                String titleString = String.format(res.getQuantityString(
+                        R.plurals.gadget_more_events, count), count);
+                views.setTextViewText(R.id.secondary_title, titleString);
+            } else {
+                // 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.secondary_title, titleString);
+            }
+        } else {
+            views.setViewVisibility(R.id.secondary_when, View.GONE);
+            views.setViewVisibility(R.id.secondary_title, View.GONE);
+        }
+        
+        return views;
+    }
+
+    /**
+     * Build date overlay bitmap for positioning on widget. This is the textual
+     * number that has been corrected for font leading issues.
+     * 
+     * @param res {@link Resources} to use for paint color.
+     * @param dateNumber Numerical date to display.
+     */
+    public Bitmap getDateOverlay(Resources res, int dateNumber) {
+        String dateString = Integer.toString(dateNumber);
+        
+        Paint paint = new Paint();
+        paint.setTypeface(Typeface.DEFAULT_BOLD);
+        paint.setTextSize(18);
+        paint.setAntiAlias(true);
+        paint.setSubpixelText(true);
+        paint.setColor(res.getColor(R.color.appwidget_date));
+        
+        // Calculate exact size of text
+        FontMetrics metrics = paint.getFontMetrics();
+        int width = (int) Math.ceil(paint.measureText(dateString));
+        int height = (int) Math.ceil(-metrics.top + metrics.descent);
+        
+        // Add padding to left edge based on various obscure rules
+        // to make font center correctly
+        char firstChar = dateString.charAt(0);
+        char lastChar = dateString.charAt(dateString.length() - 1);
+        int leftPadding = (dateString.length() == 1 || firstChar == '2' ||
+                lastChar == '1' || lastChar == '0') ? 1 : 0;
+        
+        Bitmap overlay = Bitmap.createBitmap(width + leftPadding,
+                height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(overlay);
+        canvas.drawText(dateString, leftPadding, -metrics.top, paint);
+        
+        return overlay;
+    }
+    
+    /**
+     * Build a set of {@link RemoteViews} that describes an error state.
+     */
+    private RemoteViews getAppWidgetNoEvents(Context context) {
+        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget);
+
+        views.setViewVisibility(R.id.no_events, View.VISIBLE);
+        
+        views.setViewVisibility(R.id.icon, View.GONE);
+        views.setViewVisibility(R.id.primary_card, View.GONE);
+        views.setViewVisibility(R.id.secondary_card, View.GONE);
+        
+        // Clicking on widget launches the agenda view in Calendar
+        Intent agendaIntent = new Intent();
+        agendaIntent.setComponent(new ComponentName(ACTION_PACKAGE, ACTION_CLASS));
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
+                agendaIntent, 0 /* no flags */);
+        
+        views.setOnClickPendingIntent(R.id.agenda_appwidget, pendingIntent);
+
+        return views;
+    }
+    
+    private class MarkedEvents {
+        long primaryTime = -1;
+        int primaryRow = -1;
+        int primaryConflictRow = -1;
+        int primaryCount = 0;
+        long secondaryTime = -1;
+        int secondaryRow = -1;
+        int secondaryCount = 0;
+        boolean watchFound = false;
+    }
+
+    /**
+     * 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()}
+     */
+    private MarkedEvents buildMarkedEvents(Cursor cursor, Set<Long> watchEventIds, long now) {
+        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;
+    }
+
+    /**
+     * 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()}.
+     */
+    private Cursor getUpcomingInstancesCursor(ContentResolver resolver,
+            long searchDuration, long now) {
+        // 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);
+    }
+}