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.telephony.TelephonyManager;
36import android.text.Spannable;
37import android.text.SpannableStringBuilder;
38import android.text.TextUtils;
39import android.text.style.RelativeSizeSpan;
40import android.text.style.TextAppearanceSpan;
41import android.text.style.URLSpan;
42import android.util.Log;
43import android.view.View;
44import android.widget.RemoteViews;
45
46import com.android.calendar.R;
47import com.android.calendar.Utils;
48import com.android.calendar.alerts.AlertService.NotificationWrapper;
49
50import java.util.ArrayList;
51import java.util.List;
52import java.util.regex.Pattern;
53
54/**
55 * Receives android.intent.action.EVENT_REMINDER intents and handles
56 * event reminders.  The intent URI specifies an alert id in the
57 * CalendarAlerts database table.  This class also receives the
58 * BOOT_COMPLETED intent so that it can add a status bar notification
59 * if there are Calendar event alarms that have not been dismissed.
60 * It also receives the TIME_CHANGED action so that it can fire off
61 * snoozed alarms that have become ready.  The real work is done in
62 * the AlertService class.
63 *
64 * To trigger this code after pushing the apk to device:
65 * adb shell am broadcast -a "android.intent.action.EVENT_REMINDER"
66 *    -n "com.android.calendar/.alerts.AlertReceiver"
67 */
68public class AlertReceiver extends BroadcastReceiver {
69    private static final String TAG = "AlertReceiver";
70
71    private static final String MAP_ACTION = "com.android.calendar.MAP";
72    private static final String CALL_ACTION = "com.android.calendar.CALL";
73    private static final String MAIL_ACTION = "com.android.calendar.MAIL";
74    private static final String EXTRA_EVENT_ID = "eventid";
75
76    // The broadcast for notification refreshes scheduled by the app. This is to
77    // distinguish the EVENT_REMINDER broadcast sent by the provider.
78    public static final String EVENT_REMINDER_APP_ACTION =
79            "com.android.calendar.EVENT_REMINDER_APP";
80
81    static final Object mStartingServiceSync = new Object();
82    static PowerManager.WakeLock mStartingService;
83    private static final Pattern mBlankLinePattern = Pattern.compile("^\\s*$[\n\r]",
84            Pattern.MULTILINE);
85
86    public static final String ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders";
87    private static final int NOTIFICATION_DIGEST_MAX_LENGTH = 3;
88
89    private static final String GEO_PREFIX = "geo:";
90    private static final String TEL_PREFIX = "tel:";
91    private static final int MAX_NOTIF_ACTIONS = 3;
92
93    private static Handler sAsyncHandler;
94    static {
95        HandlerThread thr = new HandlerThread("AlertReceiver async");
96        thr.start();
97        sAsyncHandler = new Handler(thr.getLooper());
98    }
99
100    @Override
101    public void onReceive(final Context context, final Intent intent) {
102        if (AlertService.DEBUG) {
103            Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString());
104        }
105        if (MAP_ACTION.equals(intent.getAction())) {
106            // Try starting the map action.
107            // If no map location is found (something changed since the notification was originally
108            // fired), update the notifications to express this change.
109            final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
110            if (eventId != -1) {
111                URLSpan[] urlSpans = getURLSpans(context, eventId);
112                Intent geoIntent = createMapActivityIntent(context, urlSpans);
113                if (geoIntent != null) {
114                    // Location was successfully found, so dismiss the shade and start maps.
115                    context.startActivity(geoIntent);
116                    closeNotificationShade(context);
117                } else {
118                    // No location was found, so update all notifications.
119                    // Our alert service does not currently allow us to specify only one
120                    // specific notification to refresh.
121                    AlertService.updateAlertNotification(context);
122                }
123            }
124        } else if (CALL_ACTION.equals(intent.getAction())) {
125            // Try starting the call action.
126            // If no call location is found (something changed since the notification was originally
127            // fired), update the notifications to express this change.
128            final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
129            if (eventId != -1) {
130                URLSpan[] urlSpans = getURLSpans(context, eventId);
131                Intent callIntent = createCallActivityIntent(context, urlSpans);
132                if (callIntent != null) {
133                    // Call location was successfully found, so dismiss the shade and start dialer.
134                    context.startActivity(callIntent);
135                    closeNotificationShade(context);
136                } else {
137                    // No call location was found, so update all notifications.
138                    // Our alert service does not currently allow us to specify only one
139                    // specific notification to refresh.
140                    AlertService.updateAlertNotification(context);
141                }
142            }
143        } else if (MAIL_ACTION.equals(intent.getAction())) {
144            closeNotificationShade(context);
145
146            // Now start the email intent.
147            final long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1);
148            if (eventId != -1) {
149                Intent i = new Intent(context, QuickResponseActivity.class);
150                i.putExtra(QuickResponseActivity.EXTRA_EVENT_ID, eventId);
151                i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
152                context.startActivity(i);
153            }
154        } else {
155            Intent i = new Intent();
156            i.setClass(context, AlertService.class);
157            i.putExtras(intent);
158            i.putExtra("action", intent.getAction());
159            Uri uri = intent.getData();
160
161            // This intent might be a BOOT_COMPLETED so it might not have a Uri.
162            if (uri != null) {
163                i.putExtra("uri", uri.toString());
164            }
165            beginStartingService(context, i);
166        }
167    }
168
169    /**
170     * Start the service to process the current event notifications, acquiring
171     * the wake lock before returning to ensure that the service will run.
172     */
173    public static void beginStartingService(Context context, Intent intent) {
174        synchronized (mStartingServiceSync) {
175            if (mStartingService == null) {
176                PowerManager pm =
177                    (PowerManager)context.getSystemService(Context.POWER_SERVICE);
178                mStartingService = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
179                        "StartingAlertService");
180                mStartingService.setReferenceCounted(false);
181            }
182            mStartingService.acquire();
183            context.startService(intent);
184        }
185    }
186
187    /**
188     * Called back by the service when it has finished processing notifications,
189     * releasing the wake lock if the service is now stopping.
190     */
191    public static void finishStartingService(Service service, int startId) {
192        synchronized (mStartingServiceSync) {
193            if (mStartingService != null) {
194                if (service.stopSelfResult(startId)) {
195                    mStartingService.release();
196                }
197            }
198        }
199    }
200
201    private static PendingIntent createClickEventIntent(Context context, long eventId,
202            long startMillis, long endMillis, int notificationId) {
203        return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId,
204                DismissAlarmsService.SHOW_ACTION);
205    }
206
207    private static PendingIntent createDeleteEventIntent(Context context, long eventId,
208            long startMillis, long endMillis, int notificationId) {
209        return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId,
210                DismissAlarmsService.DISMISS_ACTION);
211    }
212
213    private static PendingIntent createDismissAlarmsIntent(Context context, long eventId,
214            long startMillis, long endMillis, int notificationId, String action) {
215        Intent intent = new Intent();
216        intent.setClass(context, DismissAlarmsService.class);
217        intent.setAction(action);
218        intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId);
219        intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis);
220        intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis);
221        intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId);
222
223        // Must set a field that affects Intent.filterEquals so that the resulting
224        // PendingIntent will be a unique instance (the 'extras' don't achieve this).
225        // This must be unique for the click event across all reminders (so using
226        // event ID + startTime should be unique).  This also must be unique from
227        // the delete event (which also uses DismissAlarmsService).
228        Uri.Builder builder = Events.CONTENT_URI.buildUpon();
229        ContentUris.appendId(builder, eventId);
230        ContentUris.appendId(builder, startMillis);
231        intent.setData(builder.build());
232        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
233    }
234
235    private static PendingIntent createSnoozeIntent(Context context, long eventId,
236            long startMillis, long endMillis, int notificationId) {
237        Intent intent = new Intent();
238        intent.setClass(context, SnoozeAlarmsService.class);
239        intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId);
240        intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis);
241        intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis);
242        intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId);
243
244        Uri.Builder builder = Events.CONTENT_URI.buildUpon();
245        ContentUris.appendId(builder, eventId);
246        ContentUris.appendId(builder, startMillis);
247        intent.setData(builder.build());
248        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
249    }
250
251    private static PendingIntent createAlertActivityIntent(Context context) {
252        Intent clickIntent = new Intent();
253        clickIntent.setClass(context, AlertActivity.class);
254        clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
255        return PendingIntent.getActivity(context, 0, clickIntent,
256                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
257    }
258
259    public static NotificationWrapper makeBasicNotification(Context context, String title,
260            String summaryText, long startMillis, long endMillis, long eventId,
261            int notificationId, boolean doPopup, int priority) {
262        Notification n = buildBasicNotification(new Notification.Builder(context),
263                context, title, summaryText, startMillis, endMillis, eventId, notificationId,
264                doPopup, priority, false);
265        return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup);
266    }
267
268    private static Notification buildBasicNotification(Notification.Builder notificationBuilder,
269            Context context, String title, String summaryText, long startMillis, long endMillis,
270            long eventId, int notificationId, boolean doPopup, int priority,
271            boolean addActionButtons) {
272        Resources resources = context.getResources();
273        if (title == null || title.length() == 0) {
274            title = resources.getString(R.string.no_title_label);
275        }
276
277        // Create an intent triggered by clicking on the status icon, that dismisses the
278        // notification and shows the event.
279        PendingIntent clickIntent = createClickEventIntent(context, eventId, startMillis,
280                endMillis, notificationId);
281
282        // Create a delete intent triggered by dismissing the notification.
283        PendingIntent deleteIntent = createDeleteEventIntent(context, eventId, startMillis,
284            endMillis, notificationId);
285
286        // Create the base notification.
287        notificationBuilder.setContentTitle(title);
288        notificationBuilder.setContentText(summaryText);
289        notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar);
290        notificationBuilder.setContentIntent(clickIntent);
291        notificationBuilder.setDeleteIntent(deleteIntent);
292        if (doPopup) {
293            notificationBuilder.setFullScreenIntent(createAlertActivityIntent(context), true);
294        }
295
296        PendingIntent mapIntent = null, callIntent = null, snoozeIntent = null, emailIntent = null;
297        if (addActionButtons) {
298            // Send map, call, and email intent back to ourself first for a couple reasons:
299            // 1) Workaround issue where clicking action button in notification does
300            //    not automatically close the notification shade.
301            // 2) Event information will always be up to date.
302
303            // Create map and/or call intents.
304            URLSpan[] urlSpans = getURLSpans(context, eventId);
305            mapIntent = createMapBroadcastIntent(context, urlSpans, eventId);
306            callIntent = createCallBroadcastIntent(context, urlSpans, eventId);
307
308            // Create email intent for emailing attendees.
309            emailIntent = createBroadcastMailIntent(context, eventId, title);
310
311            // Create snooze intent.  TODO: change snooze to 10 minutes.
312            snoozeIntent = createSnoozeIntent(context, eventId, startMillis, endMillis,
313                    notificationId);
314        }
315
316        if (Utils.isJellybeanOrLater()) {
317            // Turn off timestamp.
318            notificationBuilder.setWhen(0);
319
320            // Should be one of the values in Notification (ie. Notification.PRIORITY_HIGH, etc).
321            // A higher priority will encourage notification manager to expand it.
322            notificationBuilder.setPriority(priority);
323
324            // Add action buttons. Show at most three, using the following priority ordering:
325            // 1. Map
326            // 2. Call
327            // 3. Email
328            // 4. Snooze
329            // Actions will only be shown if they are applicable; i.e. with no location, map will
330            // not be shown, and with no recipients, snooze will not be shown.
331            // TODO: Get icons, get strings. Maybe show preview of actual location/number?
332            int numActions = 0;
333            if (mapIntent != null && numActions < MAX_NOTIF_ACTIONS) {
334                notificationBuilder.addAction(R.drawable.ic_map,
335                        resources.getString(R.string.map_label), mapIntent);
336                numActions++;
337            }
338            if (callIntent != null && numActions < MAX_NOTIF_ACTIONS) {
339                notificationBuilder.addAction(R.drawable.ic_call,
340                        resources.getString(R.string.call_label), callIntent);
341                numActions++;
342            }
343            if (emailIntent != null && numActions < MAX_NOTIF_ACTIONS) {
344                notificationBuilder.addAction(R.drawable.ic_menu_email_holo_dark,
345                        resources.getString(R.string.email_guests_label), emailIntent);
346                numActions++;
347            }
348            if (snoozeIntent != null && numActions < MAX_NOTIF_ACTIONS) {
349                notificationBuilder.addAction(R.drawable.ic_alarm_holo_dark,
350                        resources.getString(R.string.snooze_label), snoozeIntent);
351                numActions++;
352            }
353            return notificationBuilder.getNotification();
354
355        } else {
356            // Old-style notification (pre-JB).  Use custom view with buttons to provide
357            // JB-like functionality (snooze/email).
358            Notification n = notificationBuilder.getNotification();
359
360            // Use custom view with buttons to provide JB-like functionality (snooze/email).
361            RemoteViews contentView = new RemoteViews(context.getPackageName(),
362                    R.layout.notification);
363            contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar);
364            contentView.setTextViewText(R.id.title,  title);
365            contentView.setTextViewText(R.id.text, summaryText);
366
367            int numActions = 0;
368            if (mapIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
369                contentView.setViewVisibility(R.id.map_button, View.GONE);
370            } else {
371                contentView.setViewVisibility(R.id.map_button, View.VISIBLE);
372                contentView.setOnClickPendingIntent(R.id.map_button, mapIntent);
373                contentView.setViewVisibility(R.id.end_padding, View.GONE);
374                numActions++;
375            }
376            if (callIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
377                contentView.setViewVisibility(R.id.call_button, View.GONE);
378            } else {
379                contentView.setViewVisibility(R.id.call_button, View.VISIBLE);
380                contentView.setOnClickPendingIntent(R.id.call_button, callIntent);
381                contentView.setViewVisibility(R.id.end_padding, View.GONE);
382                numActions++;
383            }
384            if (emailIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
385                contentView.setViewVisibility(R.id.email_button, View.GONE);
386            } else {
387                contentView.setViewVisibility(R.id.email_button, View.VISIBLE);
388                contentView.setOnClickPendingIntent(R.id.email_button, emailIntent);
389                contentView.setViewVisibility(R.id.end_padding, View.GONE);
390                numActions++;
391            }
392            if (snoozeIntent == null || numActions >= MAX_NOTIF_ACTIONS) {
393                contentView.setViewVisibility(R.id.snooze_button, View.GONE);
394            } else {
395                contentView.setViewVisibility(R.id.snooze_button, View.VISIBLE);
396                contentView.setOnClickPendingIntent(R.id.snooze_button, snoozeIntent);
397                contentView.setViewVisibility(R.id.end_padding, View.GONE);
398                numActions++;
399            }
400
401            n.contentView = contentView;
402
403            return n;
404        }
405    }
406
407    /**
408     * Creates an expanding notification.  The initial expanded state is decided by
409     * the notification manager based on the priority.
410     */
411    public static NotificationWrapper makeExpandingNotification(Context context, String title,
412            String summaryText, String description, long startMillis, long endMillis, long eventId,
413            int notificationId, boolean doPopup, int priority) {
414        Notification.Builder basicBuilder = new Notification.Builder(context);
415        Notification notification = buildBasicNotification(basicBuilder, context, title,
416                summaryText, startMillis, endMillis, eventId, notificationId, doPopup,
417                priority, true);
418        if (Utils.isJellybeanOrLater()) {
419            // Create a new-style expanded notification
420            Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle();
421            if (description != null) {
422                description = mBlankLinePattern.matcher(description).replaceAll("");
423                description = description.trim();
424            }
425            CharSequence text;
426            if (TextUtils.isEmpty(description)) {
427                text = summaryText;
428            } else {
429                SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
430                stringBuilder.append(summaryText);
431                stringBuilder.append("\n\n");
432                stringBuilder.setSpan(new RelativeSizeSpan(0.5f), summaryText.length(),
433                        stringBuilder.length(), 0);
434                stringBuilder.append(description);
435                text = stringBuilder;
436            }
437            expandedBuilder.bigText(text);
438            basicBuilder.setStyle(expandedBuilder);
439            notification = basicBuilder.build();
440        }
441        return new NotificationWrapper(notification, notificationId, eventId, startMillis,
442                endMillis, doPopup);
443    }
444
445    /**
446     * Creates an expanding digest notification for expired events.
447     */
448    public static NotificationWrapper makeDigestNotification(Context context,
449            ArrayList<AlertService.NotificationInfo> notificationInfos, String digestTitle,
450            boolean expandable) {
451        if (notificationInfos == null || notificationInfos.size() < 1) {
452            return null;
453        }
454
455        Resources res = context.getResources();
456        int numEvents = notificationInfos.size();
457        long[] eventIds = new long[notificationInfos.size()];
458        long[] startMillis = new long[notificationInfos.size()];
459        for (int i = 0; i < notificationInfos.size(); i++) {
460            eventIds[i] = notificationInfos.get(i).eventId;
461            startMillis[i] = notificationInfos.get(i).startMillis;
462        }
463
464        // Create an intent triggered by clicking on the status icon that shows the alerts list.
465        PendingIntent pendingClickIntent = createAlertActivityIntent(context);
466
467        // Create an intent triggered by dismissing the digest notification that clears all
468        // expired events.
469        Intent deleteIntent = new Intent();
470        deleteIntent.setClass(context, DismissAlarmsService.class);
471        deleteIntent.setAction(DismissAlarmsService.DISMISS_ACTION);
472        deleteIntent.putExtra(AlertUtils.EVENT_IDS_KEY, eventIds);
473        deleteIntent.putExtra(AlertUtils.EVENT_STARTS_KEY, startMillis);
474        PendingIntent pendingDeleteIntent = PendingIntent.getService(context, 0, deleteIntent,
475                PendingIntent.FLAG_UPDATE_CURRENT);
476
477        if (digestTitle == null || digestTitle.length() == 0) {
478            digestTitle = res.getString(R.string.no_title_label);
479        }
480
481        Notification.Builder notificationBuilder = new Notification.Builder(context);
482        notificationBuilder.setContentText(digestTitle);
483        notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar_multiple);
484        notificationBuilder.setContentIntent(pendingClickIntent);
485        notificationBuilder.setDeleteIntent(pendingDeleteIntent);
486        String nEventsStr = res.getQuantityString(R.plurals.Nevents, numEvents, numEvents);
487        notificationBuilder.setContentTitle(nEventsStr);
488
489        Notification n;
490        if (Utils.isJellybeanOrLater()) {
491            // New-style notification...
492
493            // Set to min priority to encourage the notification manager to collapse it.
494            notificationBuilder.setPriority(Notification.PRIORITY_MIN);
495
496            if (expandable) {
497                // Multiple reminders.  Combine into an expanded digest notification.
498                Notification.InboxStyle expandedBuilder = new Notification.InboxStyle();
499                int i = 0;
500                for (AlertService.NotificationInfo info : notificationInfos) {
501                    if (i < NOTIFICATION_DIGEST_MAX_LENGTH) {
502                        String name = info.eventName;
503                        if (TextUtils.isEmpty(name)) {
504                            name = context.getResources().getString(R.string.no_title_label);
505                        }
506                        String timeLocation = AlertUtils.formatTimeLocation(context,
507                                info.startMillis, info.allDay, info.location);
508
509                        TextAppearanceSpan primaryTextSpan = new TextAppearanceSpan(context,
510                                R.style.NotificationPrimaryText);
511                        TextAppearanceSpan secondaryTextSpan = new TextAppearanceSpan(context,
512                                R.style.NotificationSecondaryText);
513
514                        // Event title in bold.
515                        SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
516                        stringBuilder.append(name);
517                        stringBuilder.setSpan(primaryTextSpan, 0, stringBuilder.length(), 0);
518                        stringBuilder.append("  ");
519
520                        // Followed by time and location.
521                        int secondaryIndex = stringBuilder.length();
522                        stringBuilder.append(timeLocation);
523                        stringBuilder.setSpan(secondaryTextSpan, secondaryIndex,
524                                stringBuilder.length(), 0);
525                        expandedBuilder.addLine(stringBuilder);
526                        i++;
527                    } else {
528                        break;
529                    }
530                }
531
532                // If there are too many to display, add "+X missed events" for the last line.
533                int remaining = numEvents - i;
534                if (remaining > 0) {
535                    String nMoreEventsStr = res.getQuantityString(R.plurals.N_remaining_events,
536                            remaining, remaining);
537                    // TODO: Add highlighting and icon to this last entry once framework allows it.
538                    expandedBuilder.setSummaryText(nMoreEventsStr);
539                }
540
541                // Remove the title in the expanded form (redundant with the listed items).
542                expandedBuilder.setBigContentTitle("");
543                notificationBuilder.setStyle(expandedBuilder);
544            }
545
546            n = notificationBuilder.build();
547        } else {
548            // Old-style notification (pre-JB).  We only need a standard notification (no
549            // buttons) but use a custom view so it is consistent with the others.
550            n = notificationBuilder.getNotification();
551
552            // Use custom view with buttons to provide JB-like functionality (snooze/email).
553            RemoteViews contentView = new RemoteViews(context.getPackageName(),
554                    R.layout.notification);
555            contentView.setImageViewResource(R.id.image, R.drawable.stat_notify_calendar_multiple);
556            contentView.setTextViewText(R.id.title, nEventsStr);
557            contentView.setTextViewText(R.id.text, digestTitle);
558            contentView.setViewVisibility(R.id.time, View.VISIBLE);
559            contentView.setViewVisibility(R.id.map_button, View.GONE);
560            contentView.setViewVisibility(R.id.call_button, View.GONE);
561            contentView.setViewVisibility(R.id.email_button, View.GONE);
562            contentView.setViewVisibility(R.id.snooze_button, View.GONE);
563            contentView.setViewVisibility(R.id.end_padding, View.VISIBLE);
564            n.contentView = contentView;
565
566            // Use timestamp to force expired digest notification to the bottom (there is no
567            // priority setting before JB release).  This is hidden by the custom view.
568            n.when = 1;
569        }
570
571        NotificationWrapper nw = new NotificationWrapper(n);
572        if (AlertService.DEBUG) {
573            for (AlertService.NotificationInfo info : notificationInfos) {
574                nw.add(new NotificationWrapper(null, 0, info.eventId, info.startMillis,
575                        info.endMillis, false));
576            }
577        }
578        return nw;
579    }
580
581    private void closeNotificationShade(Context context) {
582        Intent closeNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
583        context.sendBroadcast(closeNotificationShadeIntent);
584    }
585
586    private static final String[] ATTENDEES_PROJECTION = new String[] {
587        Attendees.ATTENDEE_EMAIL,           // 0
588        Attendees.ATTENDEE_STATUS,          // 1
589    };
590    private static final int ATTENDEES_INDEX_EMAIL = 0;
591    private static final int ATTENDEES_INDEX_STATUS = 1;
592    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
593    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
594            + Attendees.ATTENDEE_EMAIL + " ASC";
595
596    private static final String[] EVENT_PROJECTION = new String[] {
597        Calendars.OWNER_ACCOUNT, // 0
598        Calendars.ACCOUNT_NAME,  // 1
599        Events.TITLE,            // 2
600        Events.ORGANIZER,        // 3
601    };
602    private static final int EVENT_INDEX_OWNER_ACCOUNT = 0;
603    private static final int EVENT_INDEX_ACCOUNT_NAME = 1;
604    private static final int EVENT_INDEX_TITLE = 2;
605    private static final int EVENT_INDEX_ORGANIZER = 3;
606
607    private static Cursor getEventCursor(Context context, long eventId) {
608        return context.getContentResolver().query(
609                ContentUris.withAppendedId(Events.CONTENT_URI, eventId), EVENT_PROJECTION,
610                null, null, null);
611    }
612
613    private static Cursor getAttendeesCursor(Context context, long eventId) {
614        return context.getContentResolver().query(Attendees.CONTENT_URI,
615                ATTENDEES_PROJECTION, ATTENDEES_WHERE, new String[] { Long.toString(eventId) },
616                ATTENDEES_SORT_ORDER);
617    }
618
619    private static Cursor getLocationCursor(Context context, long eventId) {
620        return context.getContentResolver().query(
621                ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
622                new String[] { Events.EVENT_LOCATION }, null, null, null);
623    }
624
625    /**
626     * Creates a broadcast pending intent that fires to AlertReceiver when the email button
627     * is clicked.
628     */
629    private static PendingIntent createBroadcastMailIntent(Context context, long eventId,
630            String eventTitle) {
631        // Query for viewer account.
632        String syncAccount = null;
633        Cursor eventCursor = getEventCursor(context, eventId);
634        try {
635            if (eventCursor != null && eventCursor.moveToFirst()) {
636                syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME);
637            }
638        } finally {
639            if (eventCursor != null) {
640                eventCursor.close();
641            }
642        }
643
644        // Query attendees to see if there are any to email.
645        Cursor attendeesCursor = getAttendeesCursor(context, eventId);
646        try {
647            if (attendeesCursor != null && attendeesCursor.moveToFirst()) {
648                do {
649                    String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
650                    if (Utils.isEmailableFrom(email, syncAccount)) {
651                        Intent broadcastIntent = new Intent(MAIL_ACTION);
652                        broadcastIntent.setClass(context, AlertReceiver.class);
653                        broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
654                        return PendingIntent.getBroadcast(context,
655                                Long.valueOf(eventId).hashCode(), broadcastIntent,
656                                PendingIntent.FLAG_CANCEL_CURRENT);
657                    }
658                } while (attendeesCursor.moveToNext());
659            }
660            return null;
661
662        } finally {
663            if (attendeesCursor != null) {
664                attendeesCursor.close();
665            }
666        }
667    }
668
669    /**
670     * Creates an Intent for emailing the attendees of the event.  Returns null if there
671     * are no emailable attendees.
672     */
673    static Intent createEmailIntent(Context context, long eventId, String body) {
674        // TODO: Refactor to move query part into Utils.createEmailAttendeeIntent, to
675        // be shared with EventInfoFragment.
676
677        // Query for the owner account(s).
678        String ownerAccount = null;
679        String syncAccount = null;
680        String eventTitle = null;
681        String eventOrganizer = null;
682        Cursor eventCursor = getEventCursor(context, eventId);
683        try {
684            if (eventCursor != null && eventCursor.moveToFirst()) {
685                ownerAccount = eventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
686                syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME);
687                eventTitle = eventCursor.getString(EVENT_INDEX_TITLE);
688                eventOrganizer = eventCursor.getString(EVENT_INDEX_ORGANIZER);
689            }
690        } finally {
691            if (eventCursor != null) {
692                eventCursor.close();
693            }
694        }
695        if (TextUtils.isEmpty(eventTitle)) {
696            eventTitle = context.getResources().getString(R.string.no_title_label);
697        }
698
699        // Query for the attendees.
700        List<String> toEmails = new ArrayList<String>();
701        List<String> ccEmails = new ArrayList<String>();
702        Cursor attendeesCursor = getAttendeesCursor(context, eventId);
703        try {
704            if (attendeesCursor != null && attendeesCursor.moveToFirst()) {
705                do {
706                    int status = attendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
707                    String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
708                    switch(status) {
709                        case Attendees.ATTENDEE_STATUS_DECLINED:
710                            addIfEmailable(ccEmails, email, syncAccount);
711                            break;
712                        default:
713                            addIfEmailable(toEmails, email, syncAccount);
714                    }
715                } while (attendeesCursor.moveToNext());
716            }
717        } finally {
718            if (attendeesCursor != null) {
719                attendeesCursor.close();
720            }
721        }
722
723        // Add organizer only if no attendees to email (the case when too many attendees
724        // in the event to sync or show).
725        if (toEmails.size() == 0 && ccEmails.size() == 0 && eventOrganizer != null) {
726            addIfEmailable(toEmails, eventOrganizer, syncAccount);
727        }
728
729        Intent intent = null;
730        if (ownerAccount != null && (toEmails.size() > 0 || ccEmails.size() > 0)) {
731            intent = Utils.createEmailAttendeesIntent(context.getResources(), eventTitle, body,
732                    toEmails, ccEmails, ownerAccount);
733        }
734
735        if (intent == null) {
736            return null;
737        }
738        else {
739            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
740            return intent;
741        }
742    }
743
744    private static void addIfEmailable(List<String> emailList, String email, String syncAccount) {
745        if (Utils.isEmailableFrom(email, syncAccount)) {
746            emailList.add(email);
747        }
748    }
749
750    /**
751     * Using the linkify magic, get a list of URLs from the event's location. If no such links
752     * are found, we should end up with a single geo link of the entire string.
753     */
754    private static URLSpan[] getURLSpans(Context context, long eventId) {
755        Cursor locationCursor = getLocationCursor(context, eventId);
756
757        // Default to empty list
758        URLSpan[] urlSpans = new URLSpan[0];
759        if (locationCursor != null && locationCursor.moveToFirst()) {
760            String location = locationCursor.getString(0); // Only one item in this cursor.
761            if (location != null && !location.isEmpty()) {
762                Spannable text = Utils.extendedLinkify(location, true);
763                // The linkify method should have found at least one link, at the very least.
764                // If no smart links were found, it should have set the whole string as a geo link.
765                urlSpans = text.getSpans(0, text.length(), URLSpan.class);
766            }
767            locationCursor.close();
768        }
769
770        return urlSpans;
771    }
772
773    /**
774     * Create a pending intent to send ourself a broadcast to start maps, using the first map
775     * link available.
776     * If no links are found, return null.
777     */
778    private static PendingIntent createMapBroadcastIntent(Context context, URLSpan[] urlSpans,
779            long eventId) {
780        for (int span_i = 0; span_i < urlSpans.length; span_i++) {
781            URLSpan urlSpan = urlSpans[span_i];
782            String urlString = urlSpan.getURL();
783            if (urlString.startsWith(GEO_PREFIX)) {
784                Intent broadcastIntent = new Intent(MAP_ACTION);
785                broadcastIntent.setClass(context, AlertReceiver.class);
786                broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
787                return PendingIntent.getBroadcast(context,
788                        Long.valueOf(eventId).hashCode(), broadcastIntent,
789                        PendingIntent.FLAG_CANCEL_CURRENT);
790            }
791        }
792
793        // No geo link was found, so return null;
794        return null;
795    }
796
797    /**
798     * Create an intent to take the user to maps, using the first map link available.
799     * If no links are found, return null.
800     */
801    private static Intent createMapActivityIntent(Context context, URLSpan[] urlSpans) {
802        for (int span_i = 0; span_i < urlSpans.length; span_i++) {
803            URLSpan urlSpan = urlSpans[span_i];
804            String urlString = urlSpan.getURL();
805            if (urlString.startsWith(GEO_PREFIX)) {
806                Intent geoIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlString));
807                geoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
808                return geoIntent;
809            }
810        }
811
812        // No geo link was found, so return null;
813        return null;
814    }
815
816    /**
817     * Create a pending intent to send ourself a broadcast to take the user to dialer, or any other
818     * app capable of making phone calls. Use the first phone number available. If no phone number
819     * is found, or if the device is not capable of making phone calls (i.e. a tablet), return null.
820     */
821    private static PendingIntent createCallBroadcastIntent(Context context, URLSpan[] urlSpans,
822            long eventId) {
823        // Return null if the device is unable to make phone calls.
824        TelephonyManager tm =
825                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
826        if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
827            return null;
828        }
829
830        for (int span_i = 0; span_i < urlSpans.length; span_i++) {
831            URLSpan urlSpan = urlSpans[span_i];
832            String urlString = urlSpan.getURL();
833            if (urlString.startsWith(TEL_PREFIX)) {
834                Intent broadcastIntent = new Intent(CALL_ACTION);
835                broadcastIntent.setClass(context, AlertReceiver.class);
836                broadcastIntent.putExtra(EXTRA_EVENT_ID, eventId);
837                return PendingIntent.getBroadcast(context,
838                        Long.valueOf(eventId).hashCode(), broadcastIntent,
839                        PendingIntent.FLAG_CANCEL_CURRENT);
840            }
841        }
842
843        // No tel link was found, so return null;
844        return null;
845    }
846
847    /**
848     * Create an intent to take the user to dialer, or any other app capable of making phone calls.
849     * Use the first phone number available. If no phone number is found, or if the device is
850     * not capable of making phone calls (i.e. a tablet), return null.
851     */
852    private static Intent createCallActivityIntent(Context context, URLSpan[] urlSpans) {
853        // Return null if the device is unable to make phone calls.
854        TelephonyManager tm =
855                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
856        if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
857            return null;
858        }
859
860        for (int span_i = 0; span_i < urlSpans.length; span_i++) {
861            URLSpan urlSpan = urlSpans[span_i];
862            String urlString = urlSpan.getURL();
863            if (urlString.startsWith(TEL_PREFIX)) {
864                Intent callIntent = new Intent(Intent.ACTION_DIAL, Uri.parse(urlString));
865                callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
866                return callIntent;
867            }
868        }
869
870        // No tel link was found, so return null;
871        return null;
872    }
873}
874