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 17 package com.android.calendar.alerts; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.app.Service; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.res.Resources; 27 import android.database.Cursor; 28 import android.net.Uri; 29 import android.os.Handler; 30 import android.os.HandlerThread; 31 import android.os.PowerManager; 32 import android.provider.CalendarContract.Attendees; 33 import android.provider.CalendarContract.Calendars; 34 import android.provider.CalendarContract.Events; 35 import android.telephony.TelephonyManager; 36 import android.text.Spannable; 37 import android.text.SpannableStringBuilder; 38 import android.text.TextUtils; 39 import android.text.style.RelativeSizeSpan; 40 import android.text.style.TextAppearanceSpan; 41 import android.text.style.URLSpan; 42 import android.util.Log; 43 import android.view.View; 44 import android.widget.RemoteViews; 45 46 import com.android.calendar.R; 47 import com.android.calendar.Utils; 48 import com.android.calendar.alerts.AlertService.NotificationWrapper; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import 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 */ 68 public 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