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