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