2 * Copyright (C) 2009 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.calendar;
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.ComponentName;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.Resources;
27 import android.database.Cursor;
28 import android.gadget.GadgetManager;
29 import android.gadget.GadgetProvider;
30 import android.graphics.Bitmap;
31 import android.graphics.BitmapFactory;
32 import android.graphics.Canvas;
33 import android.graphics.Paint;
34 import android.graphics.PorterDuff;
35 import android.graphics.drawable.Drawable;
36 import android.net.Uri;
37 import android.provider.Calendar;
38 import android.provider.Calendar.Attendees;
39 import android.provider.Calendar.Calendars;
40 import android.provider.Calendar.EventsColumns;
41 import android.provider.Calendar.Instances;
42 import android.provider.Calendar.Reminders;
43 import android.text.format.DateFormat;
44 import android.text.format.DateUtils;
45 import android.text.format.Time;
46 import android.util.Config;
47 import android.util.Log;
48 import android.view.View;
49 import android.widget.RemoteViews;
51 import java.util.Arrays;
52 import java.util.Date;
53 import java.util.GregorianCalendar;
56 * Simple gadget to show next upcoming calendar event.
58 public class CalendarGadgetProvider extends GadgetProvider {
59 static final String TAG = "CalendarGadgetProvider";
60 static final boolean LOGD = false;
62 // TODO: listen for timezone and system time changes to update date icon
64 static final String EVENT_SORT_ORDER = "startDay ASC, allDay ASC, begin ASC";
66 static final String[] EVENT_PROJECTION = new String[] {
74 Instances.EVENT_LOCATION,
75 Instances.CALENDAR_ID,
79 static final int INDEX_ALL_DAY = 0;
80 static final int INDEX_BEGIN = 1;
81 static final int INDEX_END = 2;
82 static final int INDEX_COLOR = 3;
83 static final int INDEX_TITLE = 4;
84 static final int INDEX_RRULE = 5;
85 static final int INDEX_HAS_ALARM = 6;
86 static final int INDEX_EVENT_LOCATION = 7;
87 static final int INDEX_CALENDAR_ID = 8;
88 static final int INDEX_EVENT_ID = 9;
90 static final long SEARCH_DURATION = DateUtils.WEEK_IN_MILLIS;
92 static final long UPDATE_DELAY_TRIGGER_DURATION = DateUtils.MINUTE_IN_MILLIS * 30;
93 static final long UPDATE_DELAY_DURATION = DateUtils.MINUTE_IN_MILLIS * 5;
95 static final long UPDATE_NO_EVENTS = DateUtils.DAY_IN_MILLIS;
97 private static final int[] DATE_ICONS = new int[] {
98 R.drawable.ic_date_01, R.drawable.ic_date_02, R.drawable.ic_date_03,
99 R.drawable.ic_date_04, R.drawable.ic_date_05, R.drawable.ic_date_06,
100 R.drawable.ic_date_07, R.drawable.ic_date_08, R.drawable.ic_date_09,
101 R.drawable.ic_date_10, R.drawable.ic_date_11, R.drawable.ic_date_12,
102 R.drawable.ic_date_13, R.drawable.ic_date_14, R.drawable.ic_date_15,
103 R.drawable.ic_date_16, R.drawable.ic_date_17, R.drawable.ic_date_18,
104 R.drawable.ic_date_19, R.drawable.ic_date_20, R.drawable.ic_date_21,
105 R.drawable.ic_date_22, R.drawable.ic_date_23, R.drawable.ic_date_24,
106 R.drawable.ic_date_25, R.drawable.ic_date_26, R.drawable.ic_date_27,
107 R.drawable.ic_date_28, R.drawable.ic_date_29, R.drawable.ic_date_30,
108 R.drawable.ic_date_31,
112 public void onDisabled(Context context) {
113 // Unsubscribe from all AlarmManager updates
114 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
115 PendingIntent pendingUpdate = getUpdateIntent(context);
116 am.cancel(pendingUpdate);
120 public void onUpdate(Context context, GadgetManager gadgetManager, int[] gadgetIds) {
121 performUpdate(context, gadgetIds);
124 static void performUpdate(Context context, int[] gadgetIds) {
125 performUpdate(context, gadgetIds, Long.MIN_VALUE, Long.MAX_VALUE);
129 * Process and push out an update for the given gadgetIds.
131 static void performUpdate(Context context, int[] gadgetIds,
132 long changedStart, long changedEnd) {
133 ContentResolver resolver = context.getContentResolver();
135 Cursor cursor = null;
136 RemoteViews views = null;
137 long triggerTime = -1;
140 cursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION);
141 if (cursor != null) {
142 MarkedEvents events = buildMarkedEvents(cursor);
143 if (events.primaryCount == 0) {
144 views = getGadgetNoEvents(context);
145 } else if (causesUpdate(events, changedStart, changedEnd)) {
146 views = getGadgetUpdate(context, cursor, events);
147 triggerTime = calculateUpdateTime(context, cursor, events);
150 views = getGadgetNoEvents(context);
153 if (cursor != null) {
158 // Bail out early if no update built
163 GadgetManager gm = GadgetManager.getInstance(context);
164 if (gadgetIds != null) {
165 gm.updateGadget(gadgetIds, views);
167 ComponentName thisGadget = new ComponentName(context, CalendarGadgetProvider.class);
168 gm.updateGadget(thisGadget, views);
171 // Schedule an alarm to wake ourselves up for the next update. We also cancel
172 // all existing wake-ups because PendingIntents don't match against extras.
174 // If no next-update calculated, schedule update about a day from now
175 long now = System.currentTimeMillis();
176 if (triggerTime == -1) {
177 triggerTime = now + UPDATE_NO_EVENTS;
180 // If requested update in past then bail out. This means we lose future
181 // updates, but it's better than possibly looping to death.
182 if (triggerTime <= now) {
183 Log.w(TAG, String.format(
184 "Encountered a bad triggerTime=%d, so bailing on future updates", triggerTime));
187 // Force early update at midnight to change date, if needed
188 long nextMidnight = getNextMidnight();
189 if (triggerTime > nextMidnight) {
190 triggerTime = nextMidnight;
193 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
194 PendingIntent pendingUpdate = getUpdateIntent(context);
196 am.cancel(pendingUpdate);
197 am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
200 long seconds = (triggerTime - System.currentTimeMillis()) /
201 DateUtils.SECOND_IN_MILLIS;
202 Log.d(TAG, String.format("Scheduled next update at %d (%d seconds from now)",
203 triggerTime, seconds));
207 static PendingIntent getUpdateIntent(Context context) {
208 Intent updateIntent = new Intent(GadgetManager.ACTION_GADGET_UPDATE);
209 return PendingIntent.getBroadcast(context, 0 /* no requestCode */,
210 updateIntent, 0 /* no flags */);
214 * Figure out the best time to push gadget updates. If the event is longer
215 * than 30 minutes, we should wait until 5 minutes after it starts to
216 * replace it with next event. Otherwise we replace at start time.
218 * Absolute worst case is that we don't have an upcoming event in the next
219 * week, so we should wait an entire day before the next push.
221 static long calculateUpdateTime(Context context, Cursor cursor, MarkedEvents events) {
222 ContentResolver resolver = context.getContentResolver();
223 long result = System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS;
225 if (events.primaryRow != -1) {
226 cursor.moveToPosition(events.primaryRow);
227 long start = cursor.getLong(INDEX_BEGIN);
228 long end = cursor.getLong(INDEX_END);
230 // If event is longer than our trigger, avoid pushing an update
231 // for next event until a few minutes after it starts. (Otherwise
232 // just push the update right as the event starts.)
233 long length = end - start;
234 if (length >= UPDATE_DELAY_TRIGGER_DURATION) {
235 result = start + UPDATE_DELAY_DURATION;
244 * Return next midnight in current timezone.
246 static long getNextMidnight() {
247 Time time = new Time();
248 time.set(System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS);
252 return time.toMillis(true /* ignore DST */);
256 * Build a set of {@link RemoteViews} that describes how to update any
257 * gadget for a specific event instance.
259 * @param cursor Valid cursor on {@link Instances#CONTENT_URI}
260 * @param events {@link MarkedEvents} parsed from the cursor
262 static RemoteViews getGadgetUpdate(Context context, Cursor cursor, MarkedEvents events) {
263 Resources res = context.getResources();
264 ContentResolver resolver = context.getContentResolver();
266 RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
268 // Clicking on gadget launches the agenda view in Calendar
269 // TODO: launch to specific primaryEventTime (bug 1648608)
270 Intent agendaIntent = new Intent(context, AgendaActivity.class);
271 PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
272 agendaIntent, 0 /* no flags */);
274 views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
276 // Build calendar icon with actual date
277 Bitmap dateIcon = buildDateIcon(context);
278 views.setImageViewBitmap(R.id.icon, dateIcon);
279 views.setViewVisibility(R.id.icon, View.VISIBLE);
280 views.setViewVisibility(R.id.no_events, View.GONE);
282 long nextMidnight = getNextMidnight();
284 // Fill primary event details
285 if (events.primaryRow != -1) {
286 views.setViewVisibility(R.id.primary_card, View.VISIBLE);
287 cursor.moveToPosition(events.primaryRow);
290 int colorFilter = cursor.getInt(INDEX_COLOR);
291 views.setDrawableParameters(R.id.when, true, -1, colorFilter,
292 PorterDuff.Mode.SRC_IN, -1);
293 views.setTextColor(R.id.title, colorFilter);
294 views.setTextColor(R.id.where, colorFilter);
295 views.setDrawableParameters(R.id.divider, true, -1, colorFilter,
296 PorterDuff.Mode.SRC_IN, -1);
297 views.setTextColor(R.id.title2, colorFilter);
300 long start = cursor.getLong(INDEX_BEGIN);
301 boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
306 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_UTC
307 | DateUtils.FORMAT_SHOW_DATE;
309 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_TIME;
310 // Show date if starts beyond next midnight
311 if (start > nextMidnight) {
312 flags = flags | DateUtils.FORMAT_SHOW_DATE;
315 if (DateFormat.is24HourFormat(context)) {
316 flags |= DateUtils.FORMAT_24HOUR;
318 whenString = DateUtils.formatDateRange(context, start, start, flags);
319 views.setTextViewText(R.id.when, whenString);
322 String titleString = cursor.getString(INDEX_TITLE);
323 if (titleString == null || titleString.length() == 0) {
324 titleString = context.getString(R.string.no_title_label);
326 views.setTextViewText(R.id.title, titleString);
329 String whereString = cursor.getString(INDEX_EVENT_LOCATION);
330 if (whereString != null && whereString.length() > 0) {
331 views.setViewVisibility(R.id.where, View.VISIBLE);
332 views.setViewVisibility(R.id.stub_where, View.INVISIBLE);
333 views.setTextViewText(R.id.where, whereString);
335 views.setViewVisibility(R.id.where, View.GONE);
336 views.setViewVisibility(R.id.stub_where, View.GONE);
340 // Fill other primary events, if present
341 if (events.primaryConflictRow != -1) {
342 views.setViewVisibility(R.id.divider, View.VISIBLE);
343 views.setViewVisibility(R.id.title2, View.VISIBLE);
345 if (events.primaryCount > 2) {
346 // If more than two primary conflicts, format multiple message
347 int count = events.primaryCount - 1;
348 String titleString = String.format(res.getQuantityString(
349 R.plurals.gadget_more_events, count), count);
350 views.setTextViewText(R.id.title2, titleString);
352 cursor.moveToPosition(events.primaryConflictRow);
355 String titleString = cursor.getString(INDEX_TITLE);
356 if (titleString == null || titleString.length() == 0) {
357 titleString = context.getString(R.string.no_title_label);
359 views.setTextViewText(R.id.title2, titleString);
362 views.setViewVisibility(R.id.divider, View.GONE);
363 views.setViewVisibility(R.id.title2, View.GONE);
366 // Fill secondary event
367 if (events.secondaryRow != -1) {
368 views.setViewVisibility(R.id.secondary_card, View.VISIBLE);
369 views.setViewVisibility(R.id.secondary_when, View.VISIBLE);
370 views.setViewVisibility(R.id.secondary_title, View.VISIBLE);
372 cursor.moveToPosition(events.secondaryRow);
375 int colorFilter = cursor.getInt(INDEX_COLOR);
376 views.setDrawableParameters(R.id.secondary_when, true, -1, colorFilter,
377 PorterDuff.Mode.SRC_IN, -1);
378 views.setTextColor(R.id.secondary_title, colorFilter);
381 long start = cursor.getLong(INDEX_BEGIN);
382 boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
387 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_UTC
388 | DateUtils.FORMAT_SHOW_DATE;
390 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_TIME;
391 // Show date if starts beyond next midnight
392 if (start > nextMidnight) {
393 flags = flags | DateUtils.FORMAT_SHOW_DATE;
396 if (DateFormat.is24HourFormat(context)) {
397 flags |= DateUtils.FORMAT_24HOUR;
399 whenString = DateUtils.formatDateRange(context, start, start, flags);
400 views.setTextViewText(R.id.secondary_when, whenString);
402 if (events.secondaryCount > 1) {
403 // If more than two secondary conflicts, format multiple message
404 int count = events.secondaryCount;
405 String titleString = String.format(res.getQuantityString(
406 R.plurals.gadget_more_events, count), count);
407 views.setTextViewText(R.id.secondary_title, titleString);
410 String titleString = cursor.getString(INDEX_TITLE);
411 if (titleString == null || titleString.length() == 0) {
412 titleString = context.getString(R.string.no_title_label);
414 views.setTextViewText(R.id.secondary_title, titleString);
417 views.setViewVisibility(R.id.secondary_when, View.GONE);
418 views.setViewVisibility(R.id.secondary_title, View.GONE);
425 * Build a set of {@link RemoteViews} that describes an error state.
427 static RemoteViews getGadgetNoEvents(Context context) {
428 RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
430 views.setViewVisibility(R.id.icon, View.GONE);
431 views.setViewVisibility(R.id.no_events, View.VISIBLE);
433 views.setViewVisibility(R.id.primary_card, View.GONE);
434 views.setViewVisibility(R.id.secondary_card, View.GONE);
436 // Clicking on gadget launches the agenda view in Calendar
437 Intent agendaIntent = new Intent(context, AgendaActivity.class);
438 PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
439 agendaIntent, 0 /* no flags */);
441 views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
447 * Build super-awesome calendar icon with actual date overlay. Uses current
448 * system date to generate.
450 static Bitmap buildDateIcon(Context context) {
451 Time time = new Time();
453 int dateNumber = time.monthDay;
455 Resources res = context.getResources();
456 Bitmap blankIcon = BitmapFactory.decodeResource(res, R.drawable.app_icon_blank);
457 Bitmap overlay = BitmapFactory.decodeResource(res, DATE_ICONS[dateNumber - 1]);
459 Bitmap result = Bitmap.createBitmap(blankIcon.getWidth(),
460 blankIcon.getHeight(), blankIcon.getConfig());
462 Canvas canvas = new Canvas(result);
463 Paint paint = new Paint();
465 canvas.drawBitmap(blankIcon, 0f, 0f, paint);
466 canvas.drawBitmap(overlay, 0f, 0f, paint);
471 static class MarkedEvents {
472 long primaryTime = -1;
474 int primaryConflictRow = -1;
475 int primaryCount = 0;
476 long secondaryTime = -1;
477 int secondaryRow = -1;
478 int secondaryCount = 0;
482 * Check if the given {@link MarkedEvents} should cause an update based on a
483 * time span, usually coming from a calendar changed event.
485 static boolean causesUpdate(MarkedEvents events, long changedStart, long changedEnd) {
486 boolean primaryTouched =
487 (events.primaryTime >= changedStart && events.primaryTime <= changedEnd);
488 boolean secondaryTouched =
489 (events.secondaryTime >= changedStart && events.secondaryTime <= changedEnd);
490 return (primaryTouched || secondaryTouched);
494 * Walk the given instances cursor and build a list of marked events to be
495 * used when updating the gadget. This structure is also used to check if
496 * updates are needed. Assumes the incoming cursor is valid.
498 static MarkedEvents buildMarkedEvents(Cursor cursor) {
499 MarkedEvents events = new MarkedEvents();
500 long now = System.currentTimeMillis();
502 cursor.moveToPosition(-1);
503 while (cursor.moveToNext()) {
504 int row = cursor.getPosition();
505 long begin = cursor.getLong(INDEX_BEGIN);
506 boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
508 // Skip all-day events that have already started
509 if (allDay && begin < now) {
513 if (events.primaryRow == -1) {
515 events.primaryRow = row;
516 events.primaryTime = begin;
517 events.primaryCount = 1;
518 } else if (events.primaryTime == begin) {
519 // Found conflicting primary event
520 if (events.primaryConflictRow == -1) {
521 events.primaryConflictRow = row;
523 events.primaryCount += 1;
524 } else if (events.secondaryRow == -1) {
525 // Found second event
526 events.secondaryRow = row;
527 events.secondaryTime = begin;
528 events.secondaryCount = 1;
529 } else if (events.secondaryTime == begin) {
530 // Found conflicting secondary event
531 events.secondaryCount += 1;
533 // Nothing interesting about this event, so bail out
540 * Query across all calendars for upcoming event instances from now until
541 * some time in the future.
543 * @param searchDuration Distance into the future to look for event
544 * instances, in milliseconds.
546 static Cursor getUpcomingInstancesCursor(ContentResolver resolver, long searchDuration) {
547 // Search for events from now until some time in the future
548 long start = System.currentTimeMillis();
549 long end = start + searchDuration;
551 Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
552 String.format("%d/%d", start, end));
554 // Make sure we only look at events *starting* after now
555 String selection = String.format("%s=1 AND %s!=%d AND %s>=%d",
556 Calendars.SELECTED, Instances.SELF_ATTENDEE_STATUS,
557 Attendees.ATTENDEE_STATUS_DECLINED, Instances.BEGIN, start);
559 return resolver.query(uri, EVENT_PROJECTION, selection, null,