OSDN Git Service

auto import from //depot/cupcake/@132589
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / CalendarGadgetProvider.java
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.calendar;
18
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;
50
51 import java.util.Arrays;
52 import java.util.Date;
53 import java.util.GregorianCalendar;
54
55 /**
56  * Simple gadget to show next upcoming calendar event.
57  */
58 public class CalendarGadgetProvider extends GadgetProvider {
59     static final String TAG = "CalendarGadgetProvider";
60     static final boolean LOGD = false;
61     
62     // TODO: listen for timezone and system time changes to update date icon
63
64     static final String EVENT_SORT_ORDER = "startDay ASC, allDay ASC, begin ASC";
65
66     static final String[] EVENT_PROJECTION = new String[] {
67         Instances.ALL_DAY,
68         Instances.BEGIN,
69         Instances.END,
70         Instances.COLOR,
71         Instances.TITLE,
72         Instances.RRULE,
73         Instances.HAS_ALARM,
74         Instances.EVENT_LOCATION,
75         Instances.CALENDAR_ID,
76         Instances.EVENT_ID,
77     };
78
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;
89     
90     static final long SEARCH_DURATION = DateUtils.WEEK_IN_MILLIS;
91     
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;
94     
95     static final long UPDATE_NO_EVENTS = DateUtils.DAY_IN_MILLIS;
96
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,
109     };
110     
111     @Override
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);
117     }
118
119     @Override
120     public void onUpdate(Context context, GadgetManager gadgetManager, int[] gadgetIds) {
121         performUpdate(context, gadgetIds);
122     }
123     
124     static void performUpdate(Context context, int[] gadgetIds) {
125         performUpdate(context, gadgetIds, Long.MIN_VALUE, Long.MAX_VALUE);
126     }
127     
128     /**
129      * Process and push out an update for the given gadgetIds.
130      */
131     static void performUpdate(Context context, int[] gadgetIds,
132             long changedStart, long changedEnd) {
133         ContentResolver resolver = context.getContentResolver();
134         
135         Cursor cursor = null;
136         RemoteViews views = null;
137         long triggerTime = -1;
138
139         try {
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);
148                 }
149             } else {
150                 views = getGadgetNoEvents(context);
151             }
152         } finally {
153             if (cursor != null) {
154                 cursor.close();
155             }
156         }
157         
158         // Bail out early if no update built
159         if (views == null) {
160             return;
161         }
162         
163         GadgetManager gm = GadgetManager.getInstance(context);
164         if (gadgetIds != null) {
165             gm.updateGadget(gadgetIds, views);
166         } else {
167             ComponentName thisGadget = new ComponentName(context, CalendarGadgetProvider.class);
168             gm.updateGadget(thisGadget, views);
169         }
170
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.
173         
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;
178         }
179         
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));
185         }
186         
187         // Force early update at midnight to change date, if needed
188         long nextMidnight = getNextMidnight();
189         if (triggerTime > nextMidnight) {
190             triggerTime = nextMidnight;
191         }
192         
193         AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
194         PendingIntent pendingUpdate = getUpdateIntent(context);
195         
196         am.cancel(pendingUpdate);
197         am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
198
199         if (LOGD) {
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));
204         }
205     }
206     
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 */);
211     }
212     
213     /**
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.
217      * <p>
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.
220      */
221     static long calculateUpdateTime(Context context, Cursor cursor, MarkedEvents events) {
222         ContentResolver resolver = context.getContentResolver();
223         long result = System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS;
224         
225         if (events.primaryRow != -1) {
226             cursor.moveToPosition(events.primaryRow);
227             long start = cursor.getLong(INDEX_BEGIN);
228             long end = cursor.getLong(INDEX_END);
229             
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;
236             } else {
237                 result = start;
238             }
239         }
240         return result;
241     }
242     
243     /**
244      * Return next midnight in current timezone.
245      */
246     static long getNextMidnight() {
247         Time time = new Time();
248         time.set(System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS);
249         time.hour = 0;
250         time.minute = 0;
251         time.second = 0;
252         return time.toMillis(true /* ignore DST */);
253     }
254     
255     /**
256      * Build a set of {@link RemoteViews} that describes how to update any
257      * gadget for a specific event instance.
258      * 
259      * @param cursor Valid cursor on {@link Instances#CONTENT_URI}
260      * @param events {@link MarkedEvents} parsed from the cursor
261      */
262     static RemoteViews getGadgetUpdate(Context context, Cursor cursor, MarkedEvents events) {
263         Resources res = context.getResources();
264         ContentResolver resolver = context.getContentResolver();
265         
266         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
267         
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 */);
273         
274         views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
275         
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);
281         
282         long nextMidnight = getNextMidnight();
283
284         // Fill primary event details
285         if (events.primaryRow != -1) {
286             views.setViewVisibility(R.id.primary_card, View.VISIBLE);
287             cursor.moveToPosition(events.primaryRow);
288             
289             // Color stripe
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);
298
299             // When
300             long start = cursor.getLong(INDEX_BEGIN);
301             boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
302             
303             int flags;
304             String whenString;
305             if (allDay) {
306                 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_UTC
307                         | DateUtils.FORMAT_SHOW_DATE;
308             } else {
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;
313                 }
314             }
315             if (DateFormat.is24HourFormat(context)) {
316                 flags |= DateUtils.FORMAT_24HOUR;
317             }
318             whenString = DateUtils.formatDateRange(context, start, start, flags);
319             views.setTextViewText(R.id.when, whenString);
320
321             // What
322             String titleString = cursor.getString(INDEX_TITLE);
323             if (titleString == null || titleString.length() == 0) {
324                 titleString = context.getString(R.string.no_title_label);
325             }
326             views.setTextViewText(R.id.title, titleString);
327             
328             // Where
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);
334             } else {
335                 views.setViewVisibility(R.id.where, View.GONE);
336                 views.setViewVisibility(R.id.stub_where, View.GONE);
337             }
338         }
339         
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);
344
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);
351             } else {
352                 cursor.moveToPosition(events.primaryConflictRow);
353
354                 // What
355                 String titleString = cursor.getString(INDEX_TITLE);
356                 if (titleString == null || titleString.length() == 0) {
357                     titleString = context.getString(R.string.no_title_label);
358                 }
359                 views.setTextViewText(R.id.title2, titleString);
360             }
361         } else {
362             views.setViewVisibility(R.id.divider, View.GONE);
363             views.setViewVisibility(R.id.title2, View.GONE);
364         }
365         
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);
371             
372             cursor.moveToPosition(events.secondaryRow);
373             
374             // Color stripe
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);
379             
380             // When
381             long start = cursor.getLong(INDEX_BEGIN);
382             boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
383             
384             int flags;
385             String whenString;
386             if (allDay) {
387                 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_UTC
388                         | DateUtils.FORMAT_SHOW_DATE;
389             } else {
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;
394                 }
395             }
396             if (DateFormat.is24HourFormat(context)) {
397                 flags |= DateUtils.FORMAT_24HOUR;
398             }
399             whenString = DateUtils.formatDateRange(context, start, start, flags);
400             views.setTextViewText(R.id.secondary_when, whenString);
401             
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);
408             } else {
409                 // What
410                 String titleString = cursor.getString(INDEX_TITLE);
411                 if (titleString == null || titleString.length() == 0) {
412                     titleString = context.getString(R.string.no_title_label);
413                 }
414                 views.setTextViewText(R.id.secondary_title, titleString);
415             }
416         } else {
417             views.setViewVisibility(R.id.secondary_when, View.GONE);
418             views.setViewVisibility(R.id.secondary_title, View.GONE);
419         }
420         
421         return views;
422     }
423     
424     /**
425      * Build a set of {@link RemoteViews} that describes an error state.
426      */
427     static RemoteViews getGadgetNoEvents(Context context) {
428         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
429
430         views.setViewVisibility(R.id.icon, View.GONE);
431         views.setViewVisibility(R.id.no_events, View.VISIBLE);
432         
433         views.setViewVisibility(R.id.primary_card, View.GONE);
434         views.setViewVisibility(R.id.secondary_card, View.GONE);
435         
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 */);
440         
441         views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
442
443         return views;
444     }
445     
446     /**
447      * Build super-awesome calendar icon with actual date overlay. Uses current
448      * system date to generate.
449      */
450     static Bitmap buildDateIcon(Context context) {
451         Time time = new Time();
452         time.setToNow();
453         int dateNumber = time.monthDay;
454         
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]);
458         
459         Bitmap result = Bitmap.createBitmap(blankIcon.getWidth(),
460                 blankIcon.getHeight(), blankIcon.getConfig());
461         
462         Canvas canvas = new Canvas(result);
463         Paint paint = new Paint();
464         
465         canvas.drawBitmap(blankIcon, 0f, 0f, paint);
466         canvas.drawBitmap(overlay, 0f, 0f, paint);
467         
468         return result;
469     }
470
471     static class MarkedEvents {
472         long primaryTime = -1;
473         int primaryRow = -1;
474         int primaryConflictRow = -1;
475         int primaryCount = 0;
476         long secondaryTime = -1;
477         int secondaryRow = -1;
478         int secondaryCount = 0;
479     }
480     
481     /**
482      * Check if the given {@link MarkedEvents} should cause an update based on a
483      * time span, usually coming from a calendar changed event.
484      */
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);
491     }
492     
493     /**
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.
497      */
498     static MarkedEvents buildMarkedEvents(Cursor cursor) {
499         MarkedEvents events = new MarkedEvents();
500         long now = System.currentTimeMillis();
501         
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;
507             
508             // Skip all-day events that have already started
509             if (allDay && begin < now) {
510                 continue;
511             }
512             
513             if (events.primaryRow == -1) {
514                 // Found first event
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;
522                 }
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;
532             } else {
533                 // Nothing interesting about this event, so bail out
534             }
535         }
536         return events;
537     }
538     
539     /**
540      * Query across all calendars for upcoming event instances from now until
541      * some time in the future.
542      * 
543      * @param searchDuration Distance into the future to look for event
544      *            instances, in milliseconds.
545      */
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;
550         
551         Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
552                 String.format("%d/%d", start, end));
553
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);
558         
559         return resolver.query(uri, EVENT_PROJECTION, selection, null,
560                 EVENT_SORT_ORDER);
561     }
562     
563 }