auto import from //branches/cupcake/...@131421
[android/platform/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.graphics.PorterDuff;
30 import android.net.Uri;
31 import android.provider.Calendar;
32 import android.provider.Calendar.Attendees;
33 import android.provider.Calendar.Calendars;
34 import android.provider.Calendar.EventsColumns;
35 import android.provider.Calendar.Instances;
36 import android.provider.Calendar.Reminders;
37 import android.text.format.DateFormat;
38 import android.text.format.DateUtils;
39 import android.util.Config;
40 import android.util.Log;
41 import android.view.View;
42 import android.widget.RemoteViews;
43
44 import java.util.Arrays;
45
46 /**
47  * Simple gadget to show next upcoming calendar event.
48  */
49 public class CalendarGadgetProvider extends BroadcastReceiver {
50     static final String TAG = "CalendarGadgetProvider";
51     // TODO: turn off this debugging
52     static final boolean LOGD = Config.LOGD || true;
53
54     static final String[] UPDATE_PROJECTION = new String[] {
55         Instances.ALL_DAY,
56         Instances.BEGIN,
57         Instances.END
58     };
59
60     static final String EVENT_SORT_ORDER = "begin ASC, title ASC";
61
62     static final String[] EVENT_PROJECTION = new String[] {
63         Instances.ALL_DAY,
64         Instances.BEGIN,
65         Instances.END,
66         Instances.COLOR,
67         Instances.TITLE,
68         Instances.RRULE,
69         Instances.HAS_ALARM,
70         Instances.EVENT_LOCATION,
71         Instances.CALENDAR_ID,
72         Instances.EVENT_ID,
73     };
74
75     static final int INDEX_ALL_DAY = 0;
76     static final int INDEX_BEGIN = 1;
77     static final int INDEX_END = 2;
78     static final int INDEX_COLOR = 3;
79     static final int INDEX_TITLE = 4;
80     static final int INDEX_RRULE = 5;
81     static final int INDEX_HAS_ALARM = 6;
82     static final int INDEX_EVENT_LOCATION = 7;
83     static final int INDEX_CALENDAR_ID = 8;
84     static final int INDEX_EVENT_ID = 9;
85     
86     static final long SHORT_DURATION = DateUtils.DAY_IN_MILLIS;
87     static final long LONG_DURATION = DateUtils.WEEK_IN_MILLIS;
88     
89     static final long UPDATE_DELAY_TRIGGER_DURATION = DateUtils.MINUTE_IN_MILLIS * 30;
90     static final long UPDATE_DELAY_DURATION = DateUtils.MINUTE_IN_MILLIS * 5;
91
92     public void onReceive(Context context, Intent intent) {
93         String action = intent.getAction();
94         
95         if (GadgetManager.ACTION_GADGET_ENABLED.equals(action)) {
96             if (LOGD) Log.d(TAG, "ENABLED");
97         } else if (GadgetManager.ACTION_GADGET_DISABLED.equals(action)) {
98             if (LOGD) Log.d(TAG, "DISABLED");
99             // TODO: remove all alarmmanager subscriptions?
100         } else if (GadgetManager.ACTION_GADGET_UPDATE.equals(action)) {
101             if (LOGD) Log.d(TAG, "UPDATE");
102
103             // Update specific gadgets
104             int[] gadgetIds = intent.getIntArrayExtra(GadgetManager.EXTRA_GADGET_IDS);
105             performUpdate(context, gadgetIds);
106             
107 //        } else if (Calendar.ACTION_EVENTS_CHANGED.equals(action)) {
108 //            if (LOGD) Log.d(TAG, "ACTION_EVENTS_CHANGED");
109 //            
110 //            // Force update of all gadgets when a calendar changes
111 //            performUpdate(context, null);
112         }
113         
114         // TODO: handle configuration step for picking calendars from the user?
115         // TODO: backend database to store selected calendars?
116         
117     }
118     
119     /**
120      * Process and push out an update for the given gadgetIds.
121      */
122     static void performUpdate(Context context, int[] gadgetIds) {
123         // TODO: get list of all alive gadgetids to make sure we update all active
124         // TODO: lookup calendarQuery in our backend database
125         
126         ContentResolver resolver = context.getContentResolver();
127         
128         // We're interested in selected calendars that have un-declined events
129         String calendarQuery = String.format("%s=1 AND %s!=%d", Calendars.SELECTED,
130                 Instances.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
131         
132         Cursor cursor = null;
133         RemoteViews views = null;
134
135         try {
136             // Try searching for events in next day, if nothing found then expand
137             // search to upcoming week.
138             cursor = getUpcomingInstancesCursor(resolver, SHORT_DURATION, calendarQuery);
139             
140             if (cursor == null || cursor.getCount() == 0) {
141                 if (cursor != null) {
142                     cursor.close();
143                 }
144                 if (LOGD) Log.d(TAG, "having to look into LONG_DURATION");
145                 cursor = getUpcomingInstancesCursor(resolver, LONG_DURATION, calendarQuery);
146             }
147             
148             // TODO: iterate across several events if showing more than one event in gadget
149             if (cursor != null && cursor.moveToFirst()) {
150                 views = getGadgetUpdate(context, cursor);
151             } else {
152                 views = getGadgetUpdateError(context);
153             }
154         } finally {
155             // Close the cursor we used, if still valid
156             if (cursor != null) {
157                 cursor.close();
158             }
159         }
160         
161         GadgetManager gm = GadgetManager.getInstance(context);
162         if (gadgetIds != null) {
163             gm.updateGadget(gadgetIds, views);
164         } else {
165             ComponentName thisGadget = new ComponentName(context, CalendarGadgetProvider.class);
166             gm.updateGadget(thisGadget, views);
167         }
168
169         // Schedule an alarm to wake ourselves up for the next update.  We also cancel
170         // all existing wake-ups because PendingIntents don't match against extras.
171         
172         Intent updateIntent = new Intent(GadgetManager.ACTION_GADGET_UPDATE);
173         PendingIntent pendingUpdate = PendingIntent.getBroadcast(context,
174                 0 /* no requestCode */, updateIntent, 0 /* no flags */);
175
176         // Figure out next time we need to update, and force to at least one minute
177         long triggerTime = calculateUpdateTime(context, calendarQuery);
178         long worstCase = System.currentTimeMillis() + DateUtils.MINUTE_IN_MILLIS;
179         if (triggerTime < worstCase) {
180             triggerTime = worstCase;
181         }
182         
183         AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
184         am.cancel(pendingUpdate);
185         am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
186
187         if (LOGD) {
188             long seconds = (triggerTime - System.currentTimeMillis()) /
189                     DateUtils.SECOND_IN_MILLIS;
190             Log.d(TAG, String.format("Scheduled next update at %d (%d seconds from now)",
191                     triggerTime, seconds));
192         }
193         
194     }
195     
196     /**
197      * Figure out the best time to push gadget updates. If the event is longer
198      * than 30 minutes, we should wait until 5 minutes after it starts to
199      * replace it with next event. Otherwise we replace at start time.
200      * <p>
201      * Absolute worst case is that we don't have an upcoming event in the next
202      * week, so we should wait an entire day before the next push.
203      */
204     static long calculateUpdateTime(Context context, String calendarQuery) {
205         ContentResolver resolver = context.getContentResolver();
206         long result = System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS;
207
208         Cursor cursor = null;
209         try {
210             long start = System.currentTimeMillis();
211             long end = start + LONG_DURATION;
212             
213             Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
214                     String.format("%d/%d", start, end));
215
216             // Make sure we only look at events *starting* after now
217             String selection = String.format("(%s) AND %s > %d",
218                     calendarQuery, Instances.BEGIN, start);
219             
220             cursor = resolver.query(uri, UPDATE_PROJECTION, selection, null,
221                     EVENT_SORT_ORDER);
222             
223             if (cursor != null && cursor.moveToFirst()) {
224                 boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
225                 start = cursor.getLong(INDEX_BEGIN);
226                 end = cursor.getLong(INDEX_END);
227                 
228                 // If event is longer than our trigger, avoid pushing an update
229                 // for next event until a few minutes after it starts.  (Otherwise
230                 // just push the update right as the event starts.)
231                 long length = end - start;
232                 if (length >= UPDATE_DELAY_TRIGGER_DURATION) {
233                     result = start + UPDATE_DELAY_DURATION;
234                 } else {
235                     result = start;
236                 }
237             }
238         } finally {
239             if (cursor != null) {
240                 cursor.close();
241             }
242         }
243         
244         return result;
245     }
246     
247     /**
248      * Build a set of {@link RemoteViews} that describes how to update any
249      * gadget for a specific event instance. This assumes the incoming cursor on
250      * a valid row from {@link Instances#CONTENT_URI}.
251      */
252     static RemoteViews getGadgetUpdate(Context context, Cursor cursor) {
253         ContentResolver resolver = context.getContentResolver();
254         
255         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
256         
257         // Clicking on gadget launches the agenda view in Calendar
258         Intent agendaIntent = new Intent(context, AgendaActivity.class);
259         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
260                 agendaIntent, 0 /* no flags */);
261         
262         views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
263         
264         views.setViewVisibility(R.id.vertical_stripe, View.VISIBLE);
265         views.setViewVisibility(R.id.divider, View.VISIBLE);
266         
267         // Color stripe
268         int colorFilter = cursor.getInt(INDEX_COLOR);
269         views.setDrawableParameters(R.id.vertical_stripe, true, -1, colorFilter,
270                 PorterDuff.Mode.SRC_IN, -1);
271         views.setTextColor(R.id.title, colorFilter);
272         views.setDrawableParameters(R.id.repeat, true, -1, colorFilter,
273                 PorterDuff.Mode.SRC_IN, -1);
274         views.setDrawableParameters(R.id.divider, true, -1, colorFilter,
275                 PorterDuff.Mode.SRC_IN, -1);
276
277         // What
278         String titleString = cursor.getString(INDEX_TITLE);
279         if (titleString == null || titleString.length() == 0) {
280             titleString = context.getString(R.string.no_title_label);
281         }
282         views.setTextViewText(R.id.title, titleString);
283         
284         // When
285         long start = cursor.getLong(INDEX_BEGIN);
286         long end = cursor.getLong(INDEX_END);
287         boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
288         
289         if (LOGD) {
290             long offset = start - System.currentTimeMillis();
291             Log.d(TAG, "found event offset=" + offset);
292         }
293         
294         int flags;
295         String whenString;
296         if (allDay) {
297             flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY |
298                     DateUtils.FORMAT_SHOW_DATE;
299         } else {
300             flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
301         }
302         if (DateFormat.is24HourFormat(context)) {
303             flags |= DateUtils.FORMAT_24HOUR;
304         }
305         whenString = DateUtils.formatDateRange(context, start, end, flags);
306         whenString = context.getString(R.string.gadget_next_event, whenString);
307         views.setTextViewText(R.id.when, whenString);
308
309         // Repeating info
310         String rrule = cursor.getString(INDEX_RRULE);
311         if (rrule != null) {
312             views.setViewVisibility(R.id.repeat, View.VISIBLE);
313         } else {
314             views.setViewVisibility(R.id.repeat, View.GONE);
315         }
316         
317         // Reminder
318         boolean hasAlarm = cursor.getInt(INDEX_HAS_ALARM) != 0;
319         if (hasAlarm) {
320             long eventId = cursor.getLong(INDEX_EVENT_ID);
321             int alarmMinutes = getAlarmMinutes(resolver, eventId);
322             
323             if (alarmMinutes != -1) {
324                 views.setViewVisibility(R.id.reminder, View.VISIBLE);
325                 views.setTextViewText(R.id.reminder, String.valueOf(alarmMinutes));
326             } else {
327                 views.setViewVisibility(R.id.reminder, View.GONE);
328             }
329         } else {
330             views.setViewVisibility(R.id.reminder, View.GONE);
331         }
332         
333         // Where
334         String whereString = cursor.getString(INDEX_EVENT_LOCATION);
335         if (whereString != null && whereString.length() > 0) {
336             views.setViewVisibility(R.id.where, View.VISIBLE);
337             views.setTextViewText(R.id.where, whereString);
338         } else {
339             views.setViewVisibility(R.id.where, View.GONE);
340         }
341         
342         // Calendar
343         long calendarId = cursor.getLong(INDEX_CALENDAR_ID);
344         String displayName = getCalendarDisplayName(resolver, calendarId);
345         if (displayName != null && displayName.length() > 0) {
346             views.setViewVisibility(R.id.calendar_container, View.VISIBLE);
347             views.setTextViewText(R.id.calendar, displayName);
348         } else {
349             views.setViewVisibility(R.id.calendar_container, View.GONE);
350         }
351         
352         return views;
353     }
354     
355     /**
356      * Build a set of {@link RemoteViews} that describes an error state.
357      */
358     static RemoteViews getGadgetUpdateError(Context context) {
359         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget_item);
360
361         Resources res = context.getResources();
362         views.setTextViewText(R.id.title, res.getText(R.string.gadget_no_events));
363         views.setTextColor(R.id.title, res.getColor(R.color.gadget_no_events));
364         
365         views.setViewVisibility(R.id.vertical_stripe, View.GONE);
366         views.setViewVisibility(R.id.repeat, View.GONE);
367         views.setViewVisibility(R.id.divider, View.GONE);
368         views.setViewVisibility(R.id.where, View.GONE);
369         views.setViewVisibility(R.id.calendar_container, View.GONE);
370         
371         // Clicking on gadget launches the agenda view in Calendar
372         Intent agendaIntent = new Intent(context, AgendaActivity.class);
373         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0 /* no requestCode */,
374                 agendaIntent, 0 /* no flags */);
375         
376         views.setOnClickPendingIntent(R.id.gadget, pendingIntent);
377
378         return views;
379     }
380     
381     /**
382      * Query across all calendars for upcoming event instances from now until
383      * some time in the future.
384      * 
385      * @param searchDuration Distance into the future to look for event
386      *            instances in milliseconds.
387      * @param calendarQuery SQL string to apply against the event selection
388      *            clause so we can filter a specific subset of calendars. A good
389      *            field for filtering is _sync_id in the Calendar table, if
390      *            present.
391      */
392     static Cursor getUpcomingInstancesCursor(ContentResolver resolver, long searchDuration,
393             String calendarQuery) {
394         // Search for events from now until some time in the future
395         long start = System.currentTimeMillis();
396         long end = start + searchDuration;
397         
398         Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
399                 String.format("%d/%d", start, end));
400
401         // Make sure we only look at events *starting* after now
402         String selection = String.format("(%s) AND %s > %d",
403                 calendarQuery, Instances.BEGIN, start);
404
405         return resolver.query(uri, EVENT_PROJECTION, selection, null,
406                 EVENT_SORT_ORDER);
407     }
408     
409     /**
410      * Pull the display name of a specific {@link EventsColumns#CALENDAR_ID}.
411      */
412     static String getCalendarDisplayName(ContentResolver resolver, long calendarId) {
413         Cursor cursor = null;
414         String result = null;
415         
416         try {
417             cursor = resolver.query(Calendars.CONTENT_URI,
418                     EventInfoActivity.CALENDARS_PROJECTION,
419                     String.format(EventInfoActivity.CALENDARS_WHERE, calendarId),
420                     null, null);
421
422             if (cursor != null && cursor.moveToFirst()) {
423                 result = cursor.getString(EventInfoActivity.CALENDARS_INDEX_DISPLAY_NAME);
424             }
425         } finally {
426             if (cursor != null) {
427                 cursor.close();
428             }
429         }
430         
431         return result;
432     }
433     
434     /**
435      * Pull the alarm reminder, in minutes, for a specific event.
436      */
437     static int getAlarmMinutes(ContentResolver resolver, long eventId) {
438         Cursor cursor = null;
439         int result = -1;
440         
441         try {
442             cursor = resolver.query(Reminders.CONTENT_URI,
443                     AgendaAdapter.REMINDERS_PROJECTION,
444                     String.format(AgendaAdapter.REMINDERS_WHERE, eventId),
445                     null, null);
446             
447             if (cursor != null && cursor.moveToFirst()) {
448                 result = cursor.getInt(AgendaAdapter.REMINDERS_INDEX_MINUTES);
449             }
450         } finally {
451             if (cursor != null) {
452                 cursor.close();
453             }
454         }
455         
456         return result;
457     }
458     
459 }
460