AlertReceiver.java revision 3a07a68da6460c36a5dbec5b8828baa4355dbe04
1/*
2 * Copyright (C) 2007 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
17package com.android.calendar.alerts;
18
19import android.app.Notification;
20import android.app.PendingIntent;
21import android.app.Service;
22import android.content.BroadcastReceiver;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.net.Uri;
29import android.os.Handler;
30import android.os.HandlerThread;
31import android.os.PowerManager;
32import android.provider.CalendarContract.Attendees;
33import android.provider.CalendarContract.Calendars;
34import android.provider.CalendarContract.Events;
35import android.text.SpannableStringBuilder;
36import android.text.TextUtils;
37import android.text.style.RelativeSizeSpan;
38import android.text.style.TextAppearanceSpan;
39import android.util.Log;
40import android.view.View;
41import android.widget.RemoteViews;
42
43import com.android.calendar.R;
44import com.android.calendar.Utils;
45import com.android.calendar.alerts.AlertService.NotificationWrapper;
46
47import java.util.ArrayList;
48import java.util.List;
49import java.util.regex.Pattern;
50
51/**
52 * Receives android.intent.action.EVENT_REMINDER intents and handles
53 * event reminders.  The intent URI specifies an alert id in the
54 * CalendarAlerts database table.  This class also receives the
55 * BOOT_COMPLETED intent so that it can add a status bar notification
56 * if there are Calendar event alarms that have not been dismissed.
57 * It also receives the TIME_CHANGED action so that it can fire off
58 * snoozed alarms that have become ready.  The real work is done in
59 * the AlertService class.
60 *
61 * To trigger this code after pushing the apk to device:
62 * adb shell am broadcast -a "android.intent.action.EVENT_REMINDER"
63 *    -n "com.android.calendar/.alerts.AlertReceiver"
64 */
65public class AlertReceiver extends BroadcastReceiver {
66    private static final String TAG = "AlertReceiver";
67
68    private static final String DELETE_ALL_ACTION = "com.android.calendar.DELETEALL";
69    private static final String MAIL_ACTION = "com.android.calendar.MAIL";
70    private static final String EXTRA_EVENT_ID = "eventid";
71
72    // The broadcast for notification refreshes scheduled by the app. This is to
73    // distinguish the EVENT_REMINDER broadcast sent by the provider.
74    public static final String EVENT_REMINDER_APP_ACTION =
75            "com.android.calendar.EVENT_REMINDER_APP";
76
77    static final Object mStartingServiceSync = new Object();
78    static PowerManager.WakeLock mStartingService;
79    private static final Pattern mBlankLinePattern = Pattern.compile("^\\s*$[\n\r]",
80            Pattern.MULTILINE);
81
82    public static final String ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders";
83    private static final int NOTIFICATION_DIGEST_MAX_LENGTH = 3;
84
85    private static Handler sAsyncHandler;
86    static {
87        HandlerThread thr = new HandlerThread("AlertReceiver async");
88        thr.start();
89        sAsyncHandler = new Handler(thr.getLooper());
90    }
91
92    @Override
93    public void onReceive(final Context context, final Intent intent) {
94        if (AlertService.DEBUG) {
95            Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString());
96        }
97        if (DELETE_ALL_ACTION.equals(intent.getAction())) {
98
99            /* The user has clicked the "Clear All Notifications"
100             * buttons so dismiss all Calendar alerts.
101             */
102            // TODO Grab a wake lock here?
103            Intent serviceIntent = new Intent(context, DismissAlarmsService.class);
104            context.startService(serviceIntent);
105        } else if (MAIL_ACTION.equals(intent.getAction())) {
106            // Close the notification shade.
107            Intent closeNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
108            context.sendBroadcast(closeNotificationShadeIntent);
109
110            // Now start the email intent.
111            final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
112            if (eventId != -1) {
113                Intent i = new Intent(context, QuickResponseActivity.class);
114                i.putExtra(QuickResponseActivity.EXTRA_EVENT_ID, eventId);
115                i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
116                context.startActivity(i);
117            }
118        } else {
119            Intent i = new Intent();
120            i.setClass(context, AlertService.class);
121            i.putExtras(intent);
122            i.putExtra("action", intent.getAction());
123            Uri uri = intent.getData();
124
125            // This intent might be a BOOT_COMPLETED so it might not have a Uri.
126            if (uri != null) {
127                i.putExtra("uri", uri.toString());
128            }
129            beginStartingService(context, i);
130        }
131    }
132
133    /**
134     * Start the service to process the current event notifications, acquiring
135     * the wake lock before returning to ensure that the service will run.
136     */
137    public static void beginStartingService(Context context, Intent intent) {
138        synchronized (mStartingServiceSync) {
139            if (mStartingService == null) {
140                PowerManager pm =
141                    (PowerManager)context.getSystemService(Context.POWER_SERVICE);
142                mStartingService = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
143                        "StartingAlertService");
144                mStartingService.setReferenceCounted(false);
145            }
146            mStartingService.acquire();
147            context.startService(intent);
148        }
149    }
150
151    /**
152     * Called back by the service when it has finished processing notifications,
153     * releasing the wake lock if the service is now stopping.
154     */
155    public static void finishStartingService(Service service, int startId) {
156        synchronized (mStartingServiceSync) {
157            if (mStartingService != null) {
158                if (service.stopSelfResult(startId)) {
159                    mStartingService.release();
160                }
161            }
162        }
163    }
164
165    private static PendingIntent createClickEventIntent(Context context, long eventId,
166            long startMillis, long endMillis, int notificationId) {
167        return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId,
168                "com.android.calendar.CLICK", true);
169    }
170
171    private static PendingIntent createDeleteEventIntent(Context context, long eventId,
172            long startMillis, long endMillis, int notificationId) {
173        return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId,
174                "com.android.calendar.DELETE", false);
175    }
176
177    private static PendingIntent createDismissAlarmsIntent(Context context, long eventId,
178            long startMillis, long endMillis, int notificationId, String action,
179            boolean showEvent) {
180        Intent intent = new Intent();
181        intent.setClass(context, DismissAlarmsService.class);
182        intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId);
183        intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis);
184        intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis);
185        intent.putExtra(AlertUtils.SHOW_EVENT_KEY, showEvent);
186        intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId);
187
188        // Must set a field that affects Intent.filterEquals so that the resulting
189        // PendingIntent will be a unique instance (the 'extras' don't achieve this).
190        // This must be unique for the click event across all reminders (so using
191        // event ID + startTime should be unique).  This also must be unique from
192        // the delete event (which also uses DismissAlarmsService).
193        Uri.Builder builder = Events.CONTENT_URI.buildUpon();
194        ContentUris.appendId(builder, eventId);
195        ContentUris.appendId(builder, startMillis);
196        intent.setData(builder.build());
197        intent.setAction(action);
198        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
199    }
200
201    private static PendingIntent createSnoozeIntent(Context context, long eventId,
202            long startMillis, long endMillis, int notificationId) {
203        Intent intent = new Intent();
204        intent.setClass(context, SnoozeAlarmsService.class);
205        intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId);
206        intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis);
207        intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis);
208        intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId);
209
210        Uri.Builder builder = Events.CONTENT_URI.buildUpon();
211        ContentUris.appendId(builder, eventId);
212        ContentUris.appendId(builder, startMillis);
213        intent.setData(builder.build());
214        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
215    }
216
217    private static PendingIntent createAlertActivityIntent(Context context) {
218        Intent clickIntent = new Intent();
219        clickIntent.setClass(context, AlertActivity.class);
220        clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
221        return PendingIntent.getActivity(context, 0, clickIntent,
222                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
223    }
224
225    public static NotificationWrapper makeBasicNotification(Context context, String title,
226            String summaryText, long startMillis, long endMillis, long eventId,
227            int notificationId, boolean doPopup, int priority) {
228        Notification n = buildBasicNotification(new Notification.Builder(context),
229                context, title, summaryText, startMillis, endMillis, eventId, notificationId,
230                doPopup, priority, false);
231        return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup);
232    }
233
234    private static Notification buildBasicNotification(Notification.Builder notificationBuilder,
235            Context context, String title, String summaryText, long startMillis, long endMillis,
236            long eventId, int notificationId, boolean doPopup, int priority,
237            boolean addActionButtons) {
238        Resources resources = context.getResources();
239        if (title == null || title.length() == 0) {
240            title = resources.getString(R.string.no_title_label);
241        }
242
243        // Create an intent triggered by clicking on the status icon, that dismisses the
244        // notification and shows the event.
245        PendingIntent clickIntent = createClickEventIntent(context, eventId, startMillis,
246                endMillis, notificationId);
247
248        // Create a delete intent triggered by dismissing the notification.
249        PendingIntent deleteIntent = createDeleteEventIntent(context, eventId, startMillis,
250            endMillis, notificationId);
251
252        // Create the base notification.
253        notificationBuilder.setContentTitle(title);
254        notificationBuilder.setContentText(summaryText);
255        notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar);
256        notificationBuilder.setContentIntent(clickIntent);
257        notificationBuilder.setDeleteIntent(deleteIntent);
258        if (doPopup) {
259            notificationBuilder.setFullScreenIntent(createAlertActivityIntent(context), true);
260        }
261
262        PendingIntent snoozeIntent = null;
263        PendingIntent emailIntent = null;
264        if (addActionButtons) {
265            // Create snooze intent.  TODO: change snooze to 10 minutes.
266            snoozeIntent = createSnoozeIntent(context, eventId, startMillis, endMillis,
267                    notificationId);
268
269            // Create email intent for emailing attendees.
270            emailIntent = createBroadcastMailIntent(context, eventId, title);
271        }
272
273        if (Utils.isJellybeanOrLater()) {
274            // Turn off timestamp.
275            notificationBuilder.setWhen(0);
276
277            // Should be one of the values in Notification (ie. Notification.PRIORITY_HIGH, etc).
278            // A higher priority will encourage notification manager to expand it.
279            notificationBuilder.setPriority(priority);
280
281            // Add action buttons.
282            if (snoozeIntent != null) {
283                notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark,
284                        resources.getString(R.string.snooze_label), snoozeIntent);
285            }
286            if (emailIntent != null) {
287                notificationBuilder.addAction(R.drawable.ic_menu_email_holo_dark,
288                        resources.getString(R.string.email_guests_label), emailIntent);
289            }
290            return notificationBuilder.getNotification();
291
292        } else {
293            // Old-style notification (pre-JB).  Use custom view with buttons to provide
294            // JB-like functionality (snooze/email).
295            Notification n = notificationBuilder.getNotification();
296
297            // Use custom view with buttons to provide JB-like functionality (snooze/email).
298            RemoteViews contentView = new RemoteViews(context.getPackageName(),
299                    R.layout.notification);
300            contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar);
301            contentView.setTextViewText(R.id.title,  title);
302            contentView.setTextViewText(R.id.text, summaryText);
303            if (snoozeIntent == null) {
304                contentView.setViewVisibility(R.id.email_button, View.GONE);
305            } else {
306                contentView.setViewVisibility(R.id.snooze_button, View.VISIBLE);
307                contentView.setOnClickPendingIntent(R.id.snooze_button, snoozeIntent);
308                contentView.setViewVisibility(R.id.end_padding, View.GONE);
309            }
310            if (emailIntent == null) {
311                contentView.setViewVisibility(R.id.email_button, View.GONE);
312            } else {
313                contentView.setViewVisibility(R.id.email_button, View.VISIBLE);
314                contentView.setOnClickPendingIntent(R.id.email_button, emailIntent);
315                contentView.setViewVisibility(R.id.end_padding, View.GONE);
316            }
317            n.contentView = contentView;
318
319            return n;
320        }
321    }
322
323    /**
324     * Creates an expanding notification.  The initial expanded state is decided by
325     * the notification manager based on the priority.
326     */
327    public static NotificationWrapper makeExpandingNotification(Context context, String title,
328            String summaryText, String description, long startMillis, long endMillis, long eventId,
329            int notificationId, boolean doPopup, int priority) {
330        Notification.Builder basicBuilder = new Notification.Builder(context);
331        Notification notification = buildBasicNotification(basicBuilder, context, title,
332                summaryText, startMillis, endMillis, eventId, notificationId, doPopup,
333                priority, true);
334        if (Utils.isJellybeanOrLater()) {
335            // Create a new-style expanded notification
336            Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle(
337                    basicBuilder);
338            if (description != null) {
339                description = mBlankLinePattern.matcher(description).replaceAll("");
340                description = description.trim();
341            }
342            CharSequence text;
343            if (TextUtils.isEmpty(description)) {
344                text = summaryText;
345            } else {
346                SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
347                stringBuilder.append(summaryText);
348                stringBuilder.append("\n\n");
349                stringBuilder.setSpan(new RelativeSizeSpan(0.5f), summaryText.length(),
350                        stringBuilder.length(), 0);
351                stringBuilder.append(description);
352                text = stringBuilder;
353            }
354            expandedBuilder.bigText(text);
355            notification = expandedBuilder.build();
356        }
357        return new NotificationWrapper(notification, notificationId, eventId, startMillis,
358                endMillis, doPopup);
359    }
360
361    /**
362     * Creates an expanding digest notification for expired events.
363     */
364    public static NotificationWrapper makeDigestNotification(Context context,
365            ArrayList<AlertService.NotificationInfo> notificationInfos, String digestTitle,
366            boolean expandable) {
367        if (notificationInfos == null || notificationInfos.size() < 1) {
368            return null;
369        }
370
371        Resources res = context.getResources();
372        int numEvents = notificationInfos.size();
373        long[] eventIds = new long[notificationInfos.size()];
374        for (int i = 0; i < notificationInfos.size(); i++) {
375            eventIds[i] = notificationInfos.get(i).eventId;
376        }
377
378        // Create an intent triggered by clicking on the status icon that shows the alerts list.
379        PendingIntent pendingClickIntent = createAlertActivityIntent(context);
380
381        // Create an intent triggered by dismissing the digest notification that clears all
382        // expired events.
383        Intent deleteIntent = new Intent();
384        deleteIntent.setClass(context, DismissAlarmsService.class);
385        deleteIntent.setAction(DELETE_ALL_ACTION);
386        deleteIntent.putExtra(AlertUtils.EVENT_IDS_KEY, eventIds);
387        PendingIntent pendingDeleteIntent = PendingIntent.getService(context, 0, deleteIntent,
388                PendingIntent.FLAG_UPDATE_CURRENT);
389
390        if (digestTitle == null || digestTitle.length() == 0) {
391            digestTitle = res.getString(R.string.no_title_label);
392        }
393
394        Notification.Builder notificationBuilder = new Notification.Builder(context);
395        notificationBuilder.setContentText(digestTitle);
396        notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar_multiple);
397        notificationBuilder.setContentIntent(pendingClickIntent);
398        notificationBuilder.setDeleteIntent(pendingDeleteIntent);
399        String nEventsStr = res.getQuantityString(R.plurals.Nevents, numEvents, numEvents);
400        notificationBuilder.setContentTitle(nEventsStr);
401
402        Notification n;
403        if (Utils.isJellybeanOrLater()) {
404            // New-style notification...
405
406            // Set to min priority to encourage the notification manager to collapse it.
407            notificationBuilder.setPriority(Notification.PRIORITY_MIN);
408
409            if (expandable) {
410                // Multiple reminders.  Combine into an expanded digest notification.
411                Notification.InboxStyle expandedBuilder = new Notification.InboxStyle(
412                        notificationBuilder);
413                int i = 0;
414                for (AlertService.NotificationInfo info : notificationInfos) {
415                    if (i < NOTIFICATION_DIGEST_MAX_LENGTH) {
416                        String name = info.eventName;
417                        if (TextUtils.isEmpty(name)) {
418                            name = context.getResources().getString(R.string.no_title_label);
419                        }
420                        String timeLocation = AlertUtils.formatTimeLocation(context,
421                                info.startMillis, info.allDay, info.location);
422
423                        TextAppearanceSpan primaryTextSpan = new TextAppearanceSpan(context,
424                                R.style.NotificationPrimaryText);
425                        TextAppearanceSpan secondaryTextSpan = new TextAppearanceSpan(context,
426                                R.style.NotificationSecondaryText);
427
428                        // Event title in bold.
429                        SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
430                        stringBuilder.append(name);
431                        stringBuilder.setSpan(primaryTextSpan, 0, stringBuilder.length(), 0);
432                        stringBuilder.append("  ");
433
434                        // Followed by time and location.
435                        int secondaryIndex = stringBuilder.length();
436                        stringBuilder.append(timeLocation);
437                        stringBuilder.setSpan(secondaryTextSpan, secondaryIndex,
438                                stringBuilder.length(), 0);
439                        expandedBuilder.addLine(stringBuilder);
440                        i++;
441                    } else {
442                        break;
443                    }
444                }
445
446                // If there are too many to display, add "+X missed events" for the last line.
447                int remaining = numEvents - i;
448                if (remaining > 0) {
449                    String nMoreEventsStr = res.getQuantityString(R.plurals.N_remaining_events,
450                            remaining, remaining);
451                    // TODO: Add highlighting and icon to this last entry once framework allows it.
452                    expandedBuilder.setSummaryText(nMoreEventsStr);
453                }
454
455                // Remove the title in the expanded form (redundant with the listed items).
456                expandedBuilder.setBigContentTitle("");
457
458                n = expandedBuilder.build();
459            } else {
460                n = notificationBuilder.build();
461            }
462        } else {
463            // Old-style notification (pre-JB).  We only need a standard notification (no
464            // buttons) but use a custom view so it is consistent with the others.
465            n = notificationBuilder.getNotification();
466
467            // Use custom view with buttons to provide JB-like functionality (snooze/email).
468            RemoteViews contentView = new RemoteViews(context.getPackageName(),
469                    R.layout.notification);
470            contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar_multiple);
471            contentView.setTextViewText(R.id.title, nEventsStr);
472            contentView.setTextViewText(R.id.text, digestTitle);
473            contentView.setViewVisibility(R.id.time, View.VISIBLE);
474            contentView.setViewVisibility(R.id.email_button, View.GONE);
475            contentView.setViewVisibility(R.id.snooze_button, View.GONE);
476            contentView.setViewVisibility(R.id.end_padding, View.VISIBLE);
477            n.contentView = contentView;
478
479            // Use timestamp to force expired digest notification to the bottom (there is no
480            // priority setting before JB release).  This is hidden by the custom view.
481            n.when = 1;
482        }
483
484        NotificationWrapper nw = new NotificationWrapper(n);
485        if (AlertService.DEBUG) {
486            for (AlertService.NotificationInfo info : notificationInfos) {
487                nw.add(new NotificationWrapper(null, 0, info.eventId, info.startMillis,
488                        info.endMillis, false));
489            }
490        }
491        return nw;
492    }
493
494    private static final String[] ATTENDEES_PROJECTION = new String[] {
495        Attendees.ATTENDEE_EMAIL,           // 0
496        Attendees.ATTENDEE_STATUS,          // 1
497    };
498    private static final int ATTENDEES_INDEX_EMAIL = 0;
499    private static final int ATTENDEES_INDEX_STATUS = 1;
500    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
501    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
502            + Attendees.ATTENDEE_EMAIL + " ASC";
503
504    private static final String[] EVENT_PROJECTION = new String[] {
505        Calendars.OWNER_ACCOUNT, // 0
506        Calendars.ACCOUNT_NAME,  // 1
507        Events.TITLE,            // 2
508        Events.ORGANIZER,        // 3
509    };
510    private static final int EVENT_INDEX_OWNER_ACCOUNT = 0;
511    private static final int EVENT_INDEX_ACCOUNT_NAME = 1;
512    private static final int EVENT_INDEX_TITLE = 2;
513    private static final int EVENT_INDEX_ORGANIZER = 3;
514
515    private static Cursor getEventCursor(Context context, long eventId) {
516        return context.getContentResolver().query(
517                ContentUris.withAppendedId(Events.CONTENT_URI, eventId), EVENT_PROJECTION,
518                null, null, null);
519    }
520
521    private static Cursor getAttendeesCursor(Context context, long eventId) {
522        return context.getContentResolver().query(Attendees.CONTENT_URI,
523                ATTENDEES_PROJECTION, ATTENDEES_WHERE, new String[] { Long.toString(eventId) },
524                ATTENDEES_SORT_ORDER);
525    }
526
527    /**
528     * Creates a broadcast pending intent that fires to AlertReceiver when the email button
529     * is clicked.
530     */
531    private static PendingIntent createBroadcastMailIntent(Context context, long eventId,
532            String eventTitle) {
533        // Query for viewer account.
534        String syncAccount = null;
535        Cursor eventCursor = getEventCursor(context, eventId);
536        try {
537            if (eventCursor != null && eventCursor.moveToFirst()) {
538                syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME);
539            }
540        } finally {
541            if (eventCursor != null) {
542                eventCursor.close();
543            }
544        }
545
546        // Query attendees to see if there are any to email.
547        Cursor attendeesCursor = getAttendeesCursor(context, eventId);
548        try {
549            if (attendeesCursor != null && attendeesCursor.moveToFirst()) {
550                do {
551                    String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
552                    if (Utils.isEmailableFrom(email, syncAccount)) {
553                        // Send intent back to ourself first for a couple reasons:
554                        // 1) Workaround issue where clicking action button in notification does
555                        //    not automatically close the notification shade.
556                        // 2) Attendees list in email will always be up to date.
557                        Intent broadcastIntent = new Intent(MAIL_ACTION);
558                        broadcastIntent.setClass(context, AlertReceiver.class);
559                        broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
560                        return PendingIntent.getBroadcast(context,
561                                Long.valueOf(eventId).hashCode(), broadcastIntent,
562                                PendingIntent.FLAG_CANCEL_CURRENT);
563                    }
564                } while (attendeesCursor.moveToNext());
565            }
566            return null;
567
568        } finally {
569            if (attendeesCursor != null) {
570                attendeesCursor.close();
571            }
572        }
573    }
574
575    /**
576     * Creates an Intent for emailing the attendees of the event.  Returns null if there
577     * are no emailable attendees.
578     */
579    static Intent createEmailIntent(Context context, long eventId, String body) {
580        // TODO: Refactor to move query part into Utils.createEmailAttendeeIntent, to
581        // be shared with EventInfoFragment.
582
583        // Query for the owner account(s).
584        String ownerAccount = null;
585        String syncAccount = null;
586        String eventTitle = null;
587        String eventOrganizer = null;
588        Cursor eventCursor = getEventCursor(context, eventId);
589        try {
590            if (eventCursor != null && eventCursor.moveToFirst()) {
591                ownerAccount = eventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
592                syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME);
593                eventTitle = eventCursor.getString(EVENT_INDEX_TITLE);
594                eventOrganizer = eventCursor.getString(EVENT_INDEX_ORGANIZER);
595            }
596        } finally {
597            if (eventCursor != null) {
598                eventCursor.close();
599            }
600        }
601        if (TextUtils.isEmpty(eventTitle)) {
602            eventTitle = context.getResources().getString(R.string.no_title_label);
603        }
604
605        // Query for the attendees.
606        List<String> toEmails = new ArrayList<String>();
607        List<String> ccEmails = new ArrayList<String>();
608        Cursor attendeesCursor = getAttendeesCursor(context, eventId);
609        try {
610            if (attendeesCursor != null && attendeesCursor.moveToFirst()) {
611                do {
612                    int status = attendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
613                    String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
614                    switch(status) {
615                        case Attendees.ATTENDEE_STATUS_DECLINED:
616                            addIfEmailable(ccEmails, email, syncAccount);
617                            break;
618                        default:
619                            addIfEmailable(toEmails, email, syncAccount);
620                    }
621                } while (attendeesCursor.moveToNext());
622            }
623        } finally {
624            if (attendeesCursor != null) {
625                attendeesCursor.close();
626            }
627        }
628
629        // Add organizer only if no attendees to email (the case when too many attendees
630        // in the event to sync or show).
631        if (toEmails.size() == 0 && ccEmails.size() == 0 && eventOrganizer != null) {
632            addIfEmailable(toEmails, eventOrganizer, syncAccount);
633        }
634
635        Intent intent = null;
636        if (ownerAccount != null && (toEmails.size() > 0 || ccEmails.size() > 0)) {
637            intent = Utils.createEmailAttendeesIntent(context.getResources(), eventTitle, body,
638                    toEmails, ccEmails, ownerAccount);
639        }
640
641        if (intent == null) {
642            return null;
643        }
644        else {
645            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
646            return intent;
647        }
648    }
649
650    private static void addIfEmailable(List<String> emailList, String email, String syncAccount) {
651        if (Utils.isEmailableFrom(email, syncAccount)) {
652            emailList.add(email);
653        }
654    }
655}
656