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