AlertReceiver.java revision f6deec2a00340052f074715ad7e95d2671aebcd7
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 com.android.calendar.R; 20import com.android.calendar.Utils; 21 22import android.app.Notification; 23import android.app.PendingIntent; 24import android.app.Service; 25import android.content.BroadcastReceiver; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.Context; 29import android.content.Intent; 30import android.content.res.Resources; 31import android.database.Cursor; 32import android.net.Uri; 33import android.os.PowerManager; 34import android.provider.CalendarContract.Attendees; 35import android.provider.CalendarContract.Calendars; 36import android.provider.CalendarContract.Events; 37import android.text.SpannableStringBuilder; 38import android.text.TextUtils; 39import android.text.style.RelativeSizeSpan; 40import android.text.style.TextAppearanceSpan; 41import android.util.Log; 42 43import java.util.ArrayList; 44import java.util.List; 45 46/** 47 * Receives android.intent.action.EVENT_REMINDER intents and handles 48 * event reminders. The intent URI specifies an alert id in the 49 * CalendarAlerts database table. This class also receives the 50 * BOOT_COMPLETED intent so that it can add a status bar notification 51 * if there are Calendar event alarms that have not been dismissed. 52 * It also receives the TIME_CHANGED action so that it can fire off 53 * snoozed alarms that have become ready. The real work is done in 54 * the AlertService class. 55 * 56 * To trigger this code after pushing the apk to device: 57 * adb shell am broadcast -a "android.intent.action.EVENT_REMINDER" 58 * -n "com.android.calendar/.alerts.AlertReceiver" 59 */ 60public class AlertReceiver extends BroadcastReceiver { 61 private static final String TAG = "AlertReceiver"; 62 63 private static final String DELETE_ACTION = "delete"; 64 65 static final Object mStartingServiceSync = new Object(); 66 static PowerManager.WakeLock mStartingService; 67 68 public static final String ACTION_DISMISS_OLD_REMINDERS = "removeOldReminders"; 69 private static final int NOTIFICATION_DIGEST_MAX_LENGTH = 3; 70 71 @Override 72 public void onReceive(Context context, Intent intent) { 73 if (AlertService.DEBUG) { 74 Log.d(TAG, "onReceive: a=" + intent.getAction() + " " + intent.toString()); 75 } 76 if (DELETE_ACTION.equals(intent.getAction())) { 77 78 /* The user has clicked the "Clear All Notifications" 79 * buttons so dismiss all Calendar alerts. 80 */ 81 // TODO Grab a wake lock here? 82 Intent serviceIntent = new Intent(context, DismissAlarmsService.class); 83 context.startService(serviceIntent); 84 } else { 85 Intent i = new Intent(); 86 i.setClass(context, AlertService.class); 87 i.putExtras(intent); 88 i.putExtra("action", intent.getAction()); 89 Uri uri = intent.getData(); 90 91 // This intent might be a BOOT_COMPLETED so it might not have a Uri. 92 if (uri != null) { 93 i.putExtra("uri", uri.toString()); 94 } 95 beginStartingService(context, i); 96 } 97 } 98 99 /** 100 * Start the service to process the current event notifications, acquiring 101 * the wake lock before returning to ensure that the service will run. 102 */ 103 public static void beginStartingService(Context context, Intent intent) { 104 synchronized (mStartingServiceSync) { 105 if (mStartingService == null) { 106 PowerManager pm = 107 (PowerManager)context.getSystemService(Context.POWER_SERVICE); 108 mStartingService = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 109 "StartingAlertService"); 110 mStartingService.setReferenceCounted(false); 111 } 112 mStartingService.acquire(); 113 context.startService(intent); 114 } 115 } 116 117 /** 118 * Called back by the service when it has finished processing notifications, 119 * releasing the wake lock if the service is now stopping. 120 */ 121 public static void finishStartingService(Service service, int startId) { 122 synchronized (mStartingServiceSync) { 123 if (mStartingService != null) { 124 if (service.stopSelfResult(startId)) { 125 mStartingService.release(); 126 } 127 } 128 } 129 } 130 131 private static PendingIntent createClickEventIntent(Context context, long eventId, 132 long startMillis, long endMillis, int notificationId) { 133 return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId, 134 "com.android.calendar.CLICK", true); 135 } 136 137 private static PendingIntent createDeleteEventIntent(Context context, long eventId, 138 long startMillis, long endMillis, int notificationId) { 139 return createDismissAlarmsIntent(context, eventId, startMillis, endMillis, notificationId, 140 "com.android.calendar.DELETE", false); 141 } 142 143 private static PendingIntent createDismissAlarmsIntent(Context context, long eventId, 144 long startMillis, long endMillis, int notificationId, String action, 145 boolean showEvent) { 146 Intent intent = new Intent(); 147 intent.setClass(context, DismissAlarmsService.class); 148 intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId); 149 intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis); 150 intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis); 151 intent.putExtra(AlertUtils.SHOW_EVENT_KEY, showEvent); 152 intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId); 153 154 // Must set a field that affects Intent.filterEquals so that the resulting 155 // PendingIntent will be a unique instance (the 'extras' don't achieve this). 156 // This must be unique for the click event across all reminders (so using 157 // event ID + startTime should be unique). This also must be unique from 158 // the delete event (which also uses DismissAlarmsService). 159 Uri.Builder builder = Events.CONTENT_URI.buildUpon(); 160 ContentUris.appendId(builder, eventId); 161 ContentUris.appendId(builder, startMillis); 162 intent.setData(builder.build()); 163 intent.setAction(action); 164 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 165 } 166 167 private static PendingIntent createSnoozeIntent(Context context, long eventId, 168 long startMillis, long endMillis, int notificationId) { 169 Intent intent = new Intent(); 170 intent.setClass(context, SnoozeAlarmsService.class); 171 intent.putExtra(AlertUtils.EVENT_ID_KEY, eventId); 172 intent.putExtra(AlertUtils.EVENT_START_KEY, startMillis); 173 intent.putExtra(AlertUtils.EVENT_END_KEY, endMillis); 174 intent.putExtra(AlertUtils.NOTIFICATION_ID_KEY, notificationId); 175 176 Uri.Builder builder = Events.CONTENT_URI.buildUpon(); 177 ContentUris.appendId(builder, eventId); 178 ContentUris.appendId(builder, startMillis); 179 intent.setData(builder.build()); 180 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 181 } 182 183 public static Notification makeBasicNotification(Context context, String title, 184 String summaryText, long startMillis, long endMillis, long eventId, 185 int notificationId, boolean doPopup) { 186 return makeBasicNotificationBuilder(context, title, summaryText, startMillis, endMillis, 187 eventId, notificationId, doPopup, false, false).build(); 188 } 189 190 private static Notification.Builder makeBasicNotificationBuilder(Context context, String title, 191 String summaryText, long startMillis, long endMillis, long eventId, 192 int notificationId, boolean doPopup, boolean highPriority, boolean addActionButtons) { 193 Resources resources = context.getResources(); 194 if (title == null || title.length() == 0) { 195 title = resources.getString(R.string.no_title_label); 196 } 197 198 // Create an intent triggered by clicking on the status icon, that dismisses the 199 // notification and shows the event. 200 PendingIntent clickIntent = createClickEventIntent(context, eventId, startMillis, 201 endMillis, notificationId); 202 203 // Create a delete intent triggered by dismissing the notification. 204 PendingIntent deleteIntent = createDeleteEventIntent(context, eventId, startMillis, 205 endMillis, notificationId); 206 207 // Create the base notification. 208 Notification.Builder notificationBuilder = new Notification.Builder(context); 209 notificationBuilder.setContentTitle(title); 210 notificationBuilder.setContentText(summaryText); 211 // TODO: change to the clock icon 212 notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar); 213 notificationBuilder.setContentIntent(clickIntent); 214 notificationBuilder.setDeleteIntent(deleteIntent); 215 if (addActionButtons) { 216 // Create a snooze button. TODO: change snooze to 10 minutes. 217 PendingIntent snoozeIntent = createSnoozeIntent(context, eventId, startMillis, 218 endMillis, notificationId); 219 notificationBuilder.addAction(R.drawable.snooze, 220 resources.getString(R.string.snooze_5min_label), snoozeIntent); 221 222 // Create an email button. 223 PendingIntent emailIntent = createEmailIntent(context, eventId, title); 224 if (emailIntent != null) { 225 notificationBuilder.addAction(R.drawable.ic_menu_email_holo_dark, 226 resources.getString(R.string.email_guests_label), emailIntent); 227 } 228 } 229 if (doPopup) { 230 notificationBuilder.setFullScreenIntent(clickIntent, true); 231 } 232 233 // Setting to a higher priority will encourage notification manager to expand the 234 // notification. 235 if (highPriority) { 236 notificationBuilder.setPriority(Notification.PRIORITY_HIGH); 237 } else { 238 notificationBuilder.setPriority(Notification.PRIORITY_DEFAULT); 239 } 240 return notificationBuilder; 241 } 242 243 /** 244 * Creates an expanding notification. The initial expanded state is decided by 245 * the notification manager based on the priority. 246 */ 247 public static Notification makeExpandingNotification(Context context, String title, 248 String summaryText, String description, long startMillis, long endMillis, long eventId, 249 int notificationId, boolean doPopup, boolean highPriority) { 250 Notification.Builder basicBuilder = makeBasicNotificationBuilder(context, title, 251 summaryText, startMillis, endMillis, eventId, notificationId, 252 doPopup, highPriority, true); 253 254 // Create an expanded notification 255 Notification.BigTextStyle expandedBuilder = new Notification.BigTextStyle( 256 basicBuilder); 257 if (description != null) { 258 description = description.trim(); 259 } 260 CharSequence text; 261 if (TextUtils.isEmpty(description)) { 262 text = summaryText; 263 } else { 264 SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); 265 stringBuilder.append(summaryText); 266 stringBuilder.append("\n\n"); 267 stringBuilder.setSpan(new RelativeSizeSpan(0.5f), summaryText.length(), 268 stringBuilder.length(), 0); 269 stringBuilder.append(description); 270 text = stringBuilder; 271 } 272 expandedBuilder.bigText(text); 273 return expandedBuilder.build(); 274 } 275 276 /** 277 * Creates an expanding digest notification for expired events. 278 */ 279 public static Notification makeDigestNotification(Context context, 280 List<AlertService.NotificationInfo> notificationInfos, String digestTitle, 281 boolean expandable) { 282 if (notificationInfos == null || notificationInfos.size() < 1) { 283 return null; 284 } 285 286 Resources res = context.getResources(); 287 int numEvents = notificationInfos.size(); 288 289 // Create an intent triggered by clicking on the status icon that shows the alerts list. 290 Intent clickIntent = new Intent(); 291 clickIntent.setClass(context, AlertActivity.class); 292 clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 293 PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0, clickIntent, 294 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 295 296 // Create an intent triggered by dismissing the digest notification that clears all 297 // expired events. 298 Intent deleteIntent = new Intent(); 299 deleteIntent.setClass(context, DismissAlarmsService.class); 300 deleteIntent.setAction(DELETE_ACTION); 301 deleteIntent.putExtra(AlertUtils.DELETE_EXPIRED_ONLY_KEY, true); 302 PendingIntent pendingDeleteIntent = PendingIntent.getService(context, 0, deleteIntent, 303 PendingIntent.FLAG_UPDATE_CURRENT); 304 305 if (digestTitle == null || digestTitle.length() == 0) { 306 digestTitle = res.getString(R.string.no_title_label); 307 } 308 309 Notification.Builder notificationBuilder = new Notification.Builder(context); 310 notificationBuilder.setContentTitle(digestTitle); 311 notificationBuilder.setSmallIcon(R.drawable.stat_notify_calendar); 312 notificationBuilder.setContentIntent(pendingClickIntent); 313 notificationBuilder.setDeleteIntent(pendingDeleteIntent); 314 String nEventsStr = res.getQuantityString(R.plurals.Nevents, numEvents, numEvents); 315 notificationBuilder.setContentText(nEventsStr); 316 317 // Set to min priority to encourage the notification manager to collapse it. 318 notificationBuilder.setPriority(Notification.PRIORITY_MIN); 319 320 if (expandable) { 321 // Multiple reminders. Combine into an expanded digest notification. 322 Notification.InboxStyle expandedBuilder = new Notification.InboxStyle( 323 notificationBuilder); 324 int i = 0; 325 for (AlertService.NotificationInfo info : notificationInfos) { 326 if (i < NOTIFICATION_DIGEST_MAX_LENGTH) { 327 String name = info.eventName; 328 if (TextUtils.isEmpty(name)) { 329 name = context.getResources().getString(R.string.no_title_label); 330 } 331 String timeLocation = AlertUtils.formatTimeLocation(context, info.startMillis, 332 info.allDay, info.location); 333 334 TextAppearanceSpan primaryTextSpan = new TextAppearanceSpan(context, 335 R.style.NotificationPrimaryText); 336 TextAppearanceSpan secondaryTextSpan = new TextAppearanceSpan(context, 337 R.style.NotificationSecondaryText); 338 339 // Event title in bold. 340 SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); 341 stringBuilder.append(name); 342 stringBuilder.setSpan(primaryTextSpan, 0, stringBuilder.length(), 0); 343 stringBuilder.append(" "); 344 345 // Followed by time and location. 346 int secondaryIndex = stringBuilder.length(); 347 stringBuilder.append(timeLocation); 348 stringBuilder.setSpan(secondaryTextSpan, secondaryIndex, stringBuilder.length(), 349 0); 350 expandedBuilder.addLine(stringBuilder); 351 i++; 352 } else { 353 break; 354 } 355 } 356 357 // If there are too many to display, add "+X missed events" for the last line. 358 int remaining = numEvents - i; 359 if (remaining > 0) { 360 String nMoreEventsStr = res.getQuantityString(R.plurals.N_missed_events, remaining, 361 remaining); 362 // TODO: Add highlighting and icon to this last entry once framework allows it. 363 expandedBuilder.setSummaryText(nMoreEventsStr); 364 } 365 366 // Remove the title in the expanded form (redundant with the listed items). 367 expandedBuilder.setBigContentTitle(""); 368 369 return expandedBuilder.build(); 370 } else { 371 return notificationBuilder.build(); 372 } 373 } 374 375 private static final String[] ATTENDEES_PROJECTION = new String[] { 376 Attendees.ATTENDEE_EMAIL, // 0 377 Attendees.ATTENDEE_STATUS, // 1 378 }; 379 private static final int ATTENDEES_INDEX_EMAIL = 0; 380 private static final int ATTENDEES_INDEX_STATUS = 1; 381 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; 382 private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " 383 + Attendees.ATTENDEE_EMAIL + " ASC"; 384 385 private static final String[] EVENT_PROJECTION = new String[] { 386 Calendars.OWNER_ACCOUNT, // 0 387 Calendars.ACCOUNT_NAME // 1 388 }; 389 private static final int EVENT_INDEX_OWNER_ACCOUNT = 0; 390 private static final int EVENT_INDEX_ACCOUNT_NAME = 1; 391 392 /** 393 * Creates an Intent for emailing the attendees of the event. Returns null if there 394 * are no emailable attendees. 395 */ 396 private static PendingIntent createEmailIntent(Context context, long eventId, 397 String eventTitle) { 398 ContentResolver resolver = context.getContentResolver(); 399 400 // TODO: Refactor to move query part into Utils.createEmailAttendeeIntent, to 401 // be shared with EventInfoFragment. 402 403 // Query for the owner account(s). 404 String ownerAccount = null; 405 String syncAccount = null; 406 Cursor eventCursor = resolver.query( 407 ContentUris.withAppendedId(Events.CONTENT_URI, eventId), EVENT_PROJECTION, 408 null, null, null); 409 if (eventCursor.moveToFirst()) { 410 ownerAccount = eventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT); 411 syncAccount = eventCursor.getString(EVENT_INDEX_ACCOUNT_NAME); 412 } 413 414 // Query for the attendees. 415 List<String> toEmails = new ArrayList<String>(); 416 List<String> ccEmails = new ArrayList<String>(); 417 Cursor attendeesCursor = resolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, 418 ATTENDEES_WHERE, new String[] { Long.toString(eventId) }, ATTENDEES_SORT_ORDER); 419 if (attendeesCursor.moveToFirst()) { 420 do { 421 int status = attendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 422 String email = attendeesCursor.getString(ATTENDEES_INDEX_EMAIL); 423 switch(status) { 424 case Attendees.ATTENDEE_STATUS_DECLINED: 425 addIfEmailable(ccEmails, email, syncAccount); 426 break; 427 default: 428 addIfEmailable(toEmails, email, syncAccount); 429 } 430 } while (attendeesCursor.moveToNext()); 431 } 432 433 Intent intent = null; 434 if (ownerAccount != null && (toEmails.size() > 0 || ccEmails.size() > 0)) { 435 intent = Utils.createEmailAttendeesIntent(context.getResources(), eventTitle, 436 toEmails, ccEmails, ownerAccount); 437 } 438 439 if (intent == null) { 440 return null; 441 } 442 else { 443 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 444 return PendingIntent.getActivity(context, Long.valueOf(eventId).hashCode(), intent, 445 PendingIntent.FLAG_CANCEL_CURRENT); 446 } 447 } 448 449 private static void addIfEmailable(List<String> emailList, String email, String syncAccount) { 450 if (Utils.isValidEmail(email) && !email.equals(syncAccount)) { 451 emailList.add(email); 452 } 453 } 454} 455