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"); thr.start()96 thr.start(); 97 sAsyncHandler = new Handler(thr.getLooper()); 98 } 99 100 @Override onReceive(final Context context, final Intent intent)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 */ beginStartingService(Context context, Intent intent)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 */ finishStartingService(Service service, int startId)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 createClickEventIntent(Context context, long eventId, long startMillis, long endMillis, int notificationId)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 createDeleteEventIntent(Context context, long eventId, long startMillis, long endMillis, int notificationId)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 createDismissAlarmsIntent(Context context, long eventId, long startMillis, long endMillis, int notificationId, String action)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 createSnoozeIntent(Context context, long eventId, long startMillis, long endMillis, int notificationId)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 createAlertActivityIntent(Context context)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 makeBasicNotification(Context context, String title, String summaryText, long startMillis, long endMillis, long eventId, int notificationId, boolean doPopup, int priority)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 buildBasicNotification(Notification.Builder notificationBuilder, Context context, String title, String summaryText, long startMillis, long endMillis, long eventId, int notificationId, boolean doPopup, int priority, boolean addActionButtons)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 */ makeExpandingNotification(Context context, String title, String summaryText, String description, long startMillis, long endMillis, long eventId, int notificationId, boolean doPopup, int priority)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 */ makeDigestNotification(Context context, ArrayList<AlertService.NotificationInfo> notificationInfos, String digestTitle, boolean expandable)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 closeNotificationShade(Context context)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 getEventCursor(Context context, long eventId)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 getAttendeesCursor(Context context, long eventId)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 getLocationCursor(Context context, long eventId)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 */ createBroadcastMailIntent(Context context, long eventId, String eventTitle)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 */ createEmailIntent(Context context, long eventId, String body)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 addIfEmailable(List<String> emailList, String email, String syncAccount)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 */ getURLSpans(Context context, long eventId)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 */ createMapBroadcastIntent(Context context, URLSpan[] urlSpans, long eventId)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 */ createMapActivityIntent(Context context, URLSpan[] urlSpans)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 */ createCallBroadcastIntent(Context context, URLSpan[] urlSpans, long eventId)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 */ createCallActivityIntent(Context context, URLSpan[] urlSpans)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