1 /* 2 * Copyright (C) 2008 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.NotificationManager; 21 import android.app.Service; 22 import android.content.ContentResolver; 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.os.Process; 37 import android.provider.CalendarContract; 38 import android.provider.CalendarContract.Attendees; 39 import android.provider.CalendarContract.CalendarAlerts; 40 import android.text.TextUtils; 41 import android.text.format.DateUtils; 42 import android.text.format.Time; 43 import android.util.Log; 44 45 import com.android.calendar.GeneralPreferences; 46 import com.android.calendar.OtherPreferences; 47 import com.android.calendar.R; 48 import com.android.calendar.Utils; 49 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.TimeZone; 54 55 /** 56 * This service is used to handle calendar event reminders. 57 */ 58 public class AlertService extends Service { 59 static final boolean DEBUG = true; 60 private static final String TAG = "AlertService"; 61 62 private volatile Looper mServiceLooper; 63 private volatile ServiceHandler mServiceHandler; 64 65 static final String[] ALERT_PROJECTION = new String[] { 66 CalendarAlerts._ID, // 0 67 CalendarAlerts.EVENT_ID, // 1 68 CalendarAlerts.STATE, // 2 69 CalendarAlerts.TITLE, // 3 70 CalendarAlerts.EVENT_LOCATION, // 4 71 CalendarAlerts.SELF_ATTENDEE_STATUS, // 5 72 CalendarAlerts.ALL_DAY, // 6 73 CalendarAlerts.ALARM_TIME, // 7 74 CalendarAlerts.MINUTES, // 8 75 CalendarAlerts.BEGIN, // 9 76 CalendarAlerts.END, // 10 77 CalendarAlerts.DESCRIPTION, // 11 78 }; 79 80 private static final int ALERT_INDEX_ID = 0; 81 private static final int ALERT_INDEX_EVENT_ID = 1; 82 private static final int ALERT_INDEX_STATE = 2; 83 private static final int ALERT_INDEX_TITLE = 3; 84 private static final int ALERT_INDEX_EVENT_LOCATION = 4; 85 private static final int ALERT_INDEX_SELF_ATTENDEE_STATUS = 5; 86 private static final int ALERT_INDEX_ALL_DAY = 6; 87 private static final int ALERT_INDEX_ALARM_TIME = 7; 88 private static final int ALERT_INDEX_MINUTES = 8; 89 private static final int ALERT_INDEX_BEGIN = 9; 90 private static final int ALERT_INDEX_END = 10; 91 private static final int ALERT_INDEX_DESCRIPTION = 11; 92 93 private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR " 94 + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<="; 95 96 private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] { 97 Integer.toString(CalendarAlerts.STATE_FIRED), 98 Integer.toString(CalendarAlerts.STATE_SCHEDULED) 99 }; 100 101 private static final String ACTIVE_ALERTS_SORT = "begin DESC, end DESC"; 102 103 private static final String DISMISS_OLD_SELECTION = CalendarAlerts.END + "<? AND " 104 + CalendarAlerts.STATE + "=?"; 105 106 private static final int MINUTE_MS = 60 * 1000; 107 108 // The grace period before changing a notification's priority bucket. 109 private static final int MIN_DEPRIORITIZE_GRACE_PERIOD_MS = 15 * MINUTE_MS; 110 111 // Hard limit to the number of notifications displayed. 112 public static final int MAX_NOTIFICATIONS = 20; 113 114 // Shared prefs key for storing whether the EVENT_REMINDER event from the provider 115 // was ever received. Some OEMs modified this provider broadcast, so we had to 116 // do the alarm scheduling here in the app, for the unbundled app's reminders to work. 117 // If the EVENT_REMINDER event was ever received, we know we can skip our secondary 118 // alarm scheduling. 119 private static final String PROVIDER_REMINDER_PREF_KEY = 120 "preference_received_provider_reminder_broadcast"; 121 private static Boolean sReceivedProviderReminderBroadcast = null; 122 123 // Added wrapper for testing 124 public static class NotificationWrapper { 125 Notification mNotification; 126 long mEventId; 127 long mBegin; 128 long mEnd; 129 ArrayList<NotificationWrapper> mNw; 130 NotificationWrapper(Notification n, int notificationId, long eventId, long startMillis, long endMillis, boolean doPopup)131 public NotificationWrapper(Notification n, int notificationId, long eventId, 132 long startMillis, long endMillis, boolean doPopup) { 133 mNotification = n; 134 mEventId = eventId; 135 mBegin = startMillis; 136 mEnd = endMillis; 137 138 // popup? 139 // notification id? 140 } 141 NotificationWrapper(Notification n)142 public NotificationWrapper(Notification n) { 143 mNotification = n; 144 } 145 add(NotificationWrapper nw)146 public void add(NotificationWrapper nw) { 147 if (mNw == null) { 148 mNw = new ArrayList<NotificationWrapper>(); 149 } 150 mNw.add(nw); 151 } 152 } 153 154 // Added wrapper for testing 155 public static class NotificationMgrWrapper extends NotificationMgr { 156 NotificationManager mNm; 157 NotificationMgrWrapper(NotificationManager nm)158 public NotificationMgrWrapper(NotificationManager nm) { 159 mNm = nm; 160 } 161 162 @Override cancel(int id)163 public void cancel(int id) { 164 mNm.cancel(id); 165 } 166 167 @Override notify(int id, NotificationWrapper nw)168 public void notify(int id, NotificationWrapper nw) { 169 mNm.notify(id, nw.mNotification); 170 } 171 } 172 processMessage(Message msg)173 void processMessage(Message msg) { 174 Bundle bundle = (Bundle) msg.obj; 175 176 // On reboot, update the notification bar with the contents of the 177 // CalendarAlerts table. 178 String action = bundle.getString("action"); 179 if (DEBUG) { 180 Log.d(TAG, bundle.getLong(android.provider.CalendarContract.CalendarAlerts.ALARM_TIME) 181 + " Action = " + action); 182 } 183 184 // Some OEMs had changed the provider's EVENT_REMINDER broadcast to their own event, 185 // which broke our unbundled app's reminders. So we added backup alarm scheduling to the 186 // app, but we know we can turn it off if we ever receive the EVENT_REMINDER broadcast. 187 boolean providerReminder = action.equals( 188 android.provider.CalendarContract.ACTION_EVENT_REMINDER); 189 if (providerReminder) { 190 if (sReceivedProviderReminderBroadcast == null) { 191 sReceivedProviderReminderBroadcast = Utils.getSharedPreference(this, 192 PROVIDER_REMINDER_PREF_KEY, false); 193 } 194 195 if (!sReceivedProviderReminderBroadcast) { 196 sReceivedProviderReminderBroadcast = true; 197 Log.d(TAG, "Setting key " + PROVIDER_REMINDER_PREF_KEY + " to: true"); 198 Utils.setSharedPreference(this, PROVIDER_REMINDER_PREF_KEY, true); 199 } 200 } 201 202 if (providerReminder || 203 action.equals(Intent.ACTION_PROVIDER_CHANGED) || 204 action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) || 205 action.equals(AlertReceiver.EVENT_REMINDER_APP_ACTION) || 206 action.equals(Intent.ACTION_LOCALE_CHANGED)) { 207 208 // b/7652098: Add a delay after the provider-changed event before refreshing 209 // notifications to help issue with the unbundled app installed on HTC having 210 // stale notifications. 211 if (action.equals(Intent.ACTION_PROVIDER_CHANGED)) { 212 try { 213 Thread.sleep(5000); 214 } catch (Exception e) { 215 // Ignore. 216 } 217 } 218 219 // If we dismissed a notification for a new event, then we need to sync the cache when 220 // an ACTION_PROVIDER_CHANGED event has been sent. Unfortunately, the data provider 221 // has a delay of CalendarProvider2.SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS (ie. 30 sec.) 222 // until it notifies us that the sync adapter has finished. 223 // TODO(psliwowski): Find a quicker way to be notified when the data provider has the 224 // syncId for event. 225 GlobalDismissManager.syncSenderDismissCache(this); 226 updateAlertNotification(this); 227 } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { 228 // The provider usually initiates this setting up of alarms on startup, 229 // but there was a bug (b/7221716) where a race condition caused this step to be 230 // skipped, resulting in missed alarms. This is a stopgap to minimize this bug 231 // for devices that don't have the provider fix, by initiating this a 2nd time here. 232 // However, it would still theoretically be possible to hit the race condition 233 // the 2nd time and still miss alarms. 234 // 235 // TODO: Remove this when the provider fix is rolled out everywhere. 236 Intent intent = new Intent(); 237 intent.setClass(this, InitAlarmsService.class); 238 startService(intent); 239 } else if (action.equals(Intent.ACTION_TIME_CHANGED)) { 240 doTimeChanged(); 241 } else if (action.equals(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS)) { 242 dismissOldAlerts(this); 243 } else { 244 Log.w(TAG, "Invalid action: " + action); 245 } 246 247 // Schedule the alarm for the next upcoming reminder, if not done by the provider. 248 if (sReceivedProviderReminderBroadcast == null || !sReceivedProviderReminderBroadcast) { 249 Log.d(TAG, "Scheduling next alarm with AlarmScheduler. " 250 + "sEventReminderReceived: " + sReceivedProviderReminderBroadcast); 251 AlarmScheduler.scheduleNextAlarm(this); 252 } 253 } 254 dismissOldAlerts(Context context)255 static void dismissOldAlerts(Context context) { 256 ContentResolver cr = context.getContentResolver(); 257 final long currentTime = System.currentTimeMillis(); 258 ContentValues vals = new ContentValues(); 259 vals.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); 260 cr.update(CalendarAlerts.CONTENT_URI, vals, DISMISS_OLD_SELECTION, new String[] { 261 Long.toString(currentTime), Integer.toString(CalendarAlerts.STATE_SCHEDULED) 262 }); 263 } 264 updateAlertNotification(Context context)265 static boolean updateAlertNotification(Context context) { 266 ContentResolver cr = context.getContentResolver(); 267 NotificationMgr nm = new NotificationMgrWrapper( 268 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); 269 final long currentTime = System.currentTimeMillis(); 270 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 271 272 if (DEBUG) { 273 Log.d(TAG, "Beginning updateAlertNotification"); 274 } 275 276 if (!prefs.getBoolean(GeneralPreferences.KEY_ALERTS, true)) { 277 if (DEBUG) { 278 Log.d(TAG, "alert preference is OFF"); 279 } 280 281 // If we shouldn't be showing notifications cancel any existing ones 282 // and return. 283 nm.cancelAll(); 284 return true; 285 } 286 287 // Sync CalendarAlerts with global dismiss cache before query it 288 GlobalDismissManager.syncReceiverDismissCache(context); 289 Cursor alertCursor = cr.query(CalendarAlerts.CONTENT_URI, ALERT_PROJECTION, 290 (ACTIVE_ALERTS_SELECTION + currentTime), ACTIVE_ALERTS_SELECTION_ARGS, 291 ACTIVE_ALERTS_SORT); 292 293 if (alertCursor == null || alertCursor.getCount() == 0) { 294 if (alertCursor != null) { 295 alertCursor.close(); 296 } 297 298 if (DEBUG) Log.d(TAG, "No fired or scheduled alerts"); 299 nm.cancelAll(); 300 return false; 301 } 302 303 return generateAlerts(context, nm, AlertUtils.createAlarmManager(context), prefs, 304 alertCursor, currentTime, MAX_NOTIFICATIONS); 305 } 306 generateAlerts(Context context, NotificationMgr nm, AlarmManagerInterface alarmMgr, SharedPreferences prefs, Cursor alertCursor, final long currentTime, final int maxNotifications)307 public static boolean generateAlerts(Context context, NotificationMgr nm, 308 AlarmManagerInterface alarmMgr, SharedPreferences prefs, Cursor alertCursor, 309 final long currentTime, final int maxNotifications) { 310 if (DEBUG) { 311 Log.d(TAG, "alertCursor count:" + alertCursor.getCount()); 312 } 313 314 // Process the query results and bucketize events. 315 ArrayList<NotificationInfo> highPriorityEvents = new ArrayList<NotificationInfo>(); 316 ArrayList<NotificationInfo> mediumPriorityEvents = new ArrayList<NotificationInfo>(); 317 ArrayList<NotificationInfo> lowPriorityEvents = new ArrayList<NotificationInfo>(); 318 int numFired = processQuery(alertCursor, context, currentTime, highPriorityEvents, 319 mediumPriorityEvents, lowPriorityEvents); 320 321 if (highPriorityEvents.size() + mediumPriorityEvents.size() 322 + lowPriorityEvents.size() == 0) { 323 nm.cancelAll(); 324 return true; 325 } 326 327 long nextRefreshTime = Long.MAX_VALUE; 328 int currentNotificationId = 1; 329 NotificationPrefs notificationPrefs = new NotificationPrefs(context, prefs, 330 (numFired == 0)); 331 332 // If there are more high/medium priority events than we can show, bump some to 333 // the low priority digest. 334 redistributeBuckets(highPriorityEvents, mediumPriorityEvents, lowPriorityEvents, 335 maxNotifications); 336 337 // Post the individual higher priority events (future and recently started 338 // concurrent events). Order these so that earlier start times appear higher in 339 // the notification list. 340 for (int i = 0; i < highPriorityEvents.size(); i++) { 341 NotificationInfo info = highPriorityEvents.get(i); 342 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 343 info.allDay, info.location); 344 postNotification(info, summaryText, context, true, notificationPrefs, nm, 345 currentNotificationId++); 346 347 // Keep concurrent events high priority (to appear higher in the notification list) 348 // until 15 minutes into the event. 349 nextRefreshTime = Math.min(nextRefreshTime, getNextRefreshTime(info, currentTime)); 350 } 351 352 // Post the medium priority events (concurrent events that started a while ago). 353 // Order these so more recent start times appear higher in the notification list. 354 // 355 // TODO: Post these with the same notification priority level as the higher priority 356 // events, so that all notifications will be co-located together. 357 for (int i = mediumPriorityEvents.size() - 1; i >= 0; i--) { 358 NotificationInfo info = mediumPriorityEvents.get(i); 359 // TODO: Change to a relative time description like: "Started 40 minutes ago". 360 // This requires constant refreshing to the message as time goes. 361 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 362 info.allDay, info.location); 363 postNotification(info, summaryText, context, false, notificationPrefs, nm, 364 currentNotificationId++); 365 366 // Refresh when concurrent event ends so it will drop into the expired digest. 367 nextRefreshTime = Math.min(nextRefreshTime, getNextRefreshTime(info, currentTime)); 368 } 369 370 // Post the low priority events as 1 combined notification. 371 int numLowPriority = lowPriorityEvents.size(); 372 if (numLowPriority > 0) { 373 String expiredDigestTitle = getDigestTitle(lowPriorityEvents); 374 NotificationWrapper notification; 375 if (numLowPriority == 1) { 376 // If only 1 expired event, display an "old-style" basic alert. 377 NotificationInfo info = lowPriorityEvents.get(0); 378 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis, 379 info.allDay, info.location); 380 notification = AlertReceiver.makeBasicNotification(context, info.eventName, 381 summaryText, info.startMillis, info.endMillis, info.eventId, 382 AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false, 383 Notification.PRIORITY_MIN); 384 } else { 385 // Multiple expired events are listed in a digest. 386 notification = AlertReceiver.makeDigestNotification(context, 387 lowPriorityEvents, expiredDigestTitle, false); 388 } 389 390 // Add options for a quiet update. 391 addNotificationOptions(notification, true, expiredDigestTitle, 392 notificationPrefs.getDefaultVibrate(), 393 notificationPrefs.getRingtoneAndSilence(), 394 false); /* Do not show the LED for the expired events. */ 395 396 if (DEBUG) { 397 Log.d(TAG, "Quietly posting digest alarm notification, numEvents:" + numLowPriority 398 + ", notificationId:" + AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID); 399 } 400 401 // Post the new notification for the group. 402 nm.notify(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, notification); 403 } else { 404 nm.cancel(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID); 405 if (DEBUG) { 406 Log.d(TAG, "No low priority events, canceling the digest notification."); 407 } 408 } 409 410 // Remove the notifications that are hanging around from the previous refresh. 411 if (currentNotificationId <= maxNotifications) { 412 nm.cancelAllBetween(currentNotificationId, maxNotifications); 413 if (DEBUG) { 414 Log.d(TAG, "Canceling leftover notification IDs " + currentNotificationId + "-" 415 + maxNotifications); 416 } 417 } 418 419 // Schedule the next silent refresh time so notifications will change 420 // buckets (eg. drop into expired digest, etc). 421 if (nextRefreshTime < Long.MAX_VALUE && nextRefreshTime > currentTime) { 422 AlertUtils.scheduleNextNotificationRefresh(context, alarmMgr, nextRefreshTime); 423 if (DEBUG) { 424 long minutesBeforeRefresh = (nextRefreshTime - currentTime) / MINUTE_MS; 425 Time time = new Time(); 426 time.set(nextRefreshTime); 427 String msg = String.format("Scheduling next notification refresh in %d min at: " 428 + "%d:%02d", minutesBeforeRefresh, time.hour, time.minute); 429 Log.d(TAG, msg); 430 } 431 } else if (nextRefreshTime < currentTime) { 432 Log.e(TAG, "Illegal state: next notification refresh time found to be in the past."); 433 } 434 435 // Flushes old fired alerts from internal storage, if needed. 436 AlertUtils.flushOldAlertsFromInternalStorage(context); 437 438 return true; 439 } 440 441 /** 442 * Redistributes events in the priority lists based on the max # of notifications we 443 * can show. 444 */ redistributeBuckets(ArrayList<NotificationInfo> highPriorityEvents, ArrayList<NotificationInfo> mediumPriorityEvents, ArrayList<NotificationInfo> lowPriorityEvents, int maxNotifications)445 static void redistributeBuckets(ArrayList<NotificationInfo> highPriorityEvents, 446 ArrayList<NotificationInfo> mediumPriorityEvents, 447 ArrayList<NotificationInfo> lowPriorityEvents, int maxNotifications) { 448 449 // If too many high priority alerts, shift the remaining high priority and all the 450 // medium priority ones to the low priority bucket. Note that order is important 451 // here; these lists are sorted by descending start time. Maintain that ordering 452 // so posted notifications are in the expected order. 453 if (highPriorityEvents.size() > maxNotifications) { 454 // Move mid-priority to the digest. 455 lowPriorityEvents.addAll(0, mediumPriorityEvents); 456 457 // Move the rest of the high priority ones (latest ones) to the digest. 458 List<NotificationInfo> itemsToMoveSublist = highPriorityEvents.subList( 459 0, highPriorityEvents.size() - maxNotifications); 460 // TODO: What order for high priority in the digest? 461 lowPriorityEvents.addAll(0, itemsToMoveSublist); 462 if (DEBUG) { 463 logEventIdsBumped(mediumPriorityEvents, itemsToMoveSublist); 464 } 465 mediumPriorityEvents.clear(); 466 // Clearing the sublist view removes the items from the highPriorityEvents list. 467 itemsToMoveSublist.clear(); 468 } 469 470 // Bump the medium priority events if necessary. 471 if (mediumPriorityEvents.size() + highPriorityEvents.size() > maxNotifications) { 472 int spaceRemaining = maxNotifications - highPriorityEvents.size(); 473 474 // Reached our max, move the rest to the digest. Since these are concurrent 475 // events, we move the ones with the earlier start time first since they are 476 // further in the past and less important. 477 List<NotificationInfo> itemsToMoveSublist = mediumPriorityEvents.subList( 478 spaceRemaining, mediumPriorityEvents.size()); 479 lowPriorityEvents.addAll(0, itemsToMoveSublist); 480 if (DEBUG) { 481 logEventIdsBumped(itemsToMoveSublist, null); 482 } 483 484 // Clearing the sublist view removes the items from the mediumPriorityEvents list. 485 itemsToMoveSublist.clear(); 486 } 487 } 488 logEventIdsBumped(List<NotificationInfo> list1, List<NotificationInfo> list2)489 private static void logEventIdsBumped(List<NotificationInfo> list1, 490 List<NotificationInfo> list2) { 491 StringBuilder ids = new StringBuilder(); 492 if (list1 != null) { 493 for (NotificationInfo info : list1) { 494 ids.append(info.eventId); 495 ids.append(","); 496 } 497 } 498 if (list2 != null) { 499 for (NotificationInfo info : list2) { 500 ids.append(info.eventId); 501 ids.append(","); 502 } 503 } 504 if (ids.length() > 0 && ids.charAt(ids.length() - 1) == ',') { 505 ids.setLength(ids.length() - 1); 506 } 507 if (ids.length() > 0) { 508 Log.d(TAG, "Reached max postings, bumping event IDs {" + ids.toString() 509 + "} to digest."); 510 } 511 } 512 getNextRefreshTime(NotificationInfo info, long currentTime)513 private static long getNextRefreshTime(NotificationInfo info, long currentTime) { 514 long startAdjustedForAllDay = info.startMillis; 515 long endAdjustedForAllDay = info.endMillis; 516 if (info.allDay) { 517 Time t = new Time(); 518 startAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, 519 Time.getCurrentTimezone()); 520 endAdjustedForAllDay = Utils.convertAlldayUtcToLocal(t, info.startMillis, 521 Time.getCurrentTimezone()); 522 } 523 524 // We change an event's priority bucket at 15 minutes into the event or 1/4 event duration. 525 long nextRefreshTime = Long.MAX_VALUE; 526 long gracePeriodCutoff = startAdjustedForAllDay + 527 getGracePeriodMs(startAdjustedForAllDay, endAdjustedForAllDay, info.allDay); 528 if (gracePeriodCutoff > currentTime) { 529 nextRefreshTime = Math.min(nextRefreshTime, gracePeriodCutoff); 530 } 531 532 // ... and at the end (so expiring ones drop into a digest). 533 if (endAdjustedForAllDay > currentTime && endAdjustedForAllDay > gracePeriodCutoff) { 534 nextRefreshTime = Math.min(nextRefreshTime, endAdjustedForAllDay); 535 } 536 return nextRefreshTime; 537 } 538 539 /** 540 * Processes the query results and bucketizes the alerts. 541 * 542 * @param highPriorityEvents This will contain future events, and concurrent events 543 * that started recently (less than the interval DEPRIORITIZE_GRACE_PERIOD_MS). 544 * @param mediumPriorityEvents This will contain concurrent events that started 545 * more than DEPRIORITIZE_GRACE_PERIOD_MS ago. 546 * @param lowPriorityEvents Will contain events that have ended. 547 * @return Returns the number of new alerts to fire. If this is 0, it implies 548 * a quiet update. 549 */ processQuery(final Cursor alertCursor, final Context context, final long currentTime, ArrayList<NotificationInfo> highPriorityEvents, ArrayList<NotificationInfo> mediumPriorityEvents, ArrayList<NotificationInfo> lowPriorityEvents)550 static int processQuery(final Cursor alertCursor, final Context context, 551 final long currentTime, ArrayList<NotificationInfo> highPriorityEvents, 552 ArrayList<NotificationInfo> mediumPriorityEvents, 553 ArrayList<NotificationInfo> lowPriorityEvents) { 554 // Experimental reminder setting to only remind for events that have 555 // been responded to with "yes" or "maybe". 556 String skipRemindersPref = Utils.getSharedPreference(context, 557 OtherPreferences.KEY_OTHER_REMINDERS_RESPONDED, ""); 558 // Skip no-response events if the "Skip Reminders" preference has the second option, 559 // "If declined or not responded", is selected. 560 // Note that by default, the first option will be selected, so this will be false. 561 boolean remindRespondedOnly = skipRemindersPref.equals(context.getResources(). 562 getStringArray(R.array.preferences_skip_reminders_values)[1]); 563 // Experimental reminder setting to silence reminders when they are 564 // during the pre-defined quiet hours. 565 boolean useQuietHours = Utils.getSharedPreference(context, 566 OtherPreferences.KEY_OTHER_QUIET_HOURS, false); 567 // Note that the start time may be either before or after the end time, 568 // depending on whether quiet hours cross through midnight. 569 int quietHoursStartHour = 570 OtherPreferences.QUIET_HOURS_DEFAULT_START_HOUR; 571 int quietHoursStartMinute = 572 OtherPreferences.QUIET_HOURS_DEFAULT_START_MINUTE; 573 int quietHoursEndHour = 574 OtherPreferences.QUIET_HOURS_DEFAULT_END_HOUR; 575 int quietHoursEndMinute = 576 OtherPreferences.QUIET_HOURS_DEFAULT_END_MINUTE; 577 if (useQuietHours) { 578 quietHoursStartHour = Utils.getSharedPreference(context, 579 OtherPreferences.KEY_OTHER_QUIET_HOURS_START_HOUR, 580 OtherPreferences.QUIET_HOURS_DEFAULT_START_HOUR); 581 quietHoursStartMinute = Utils.getSharedPreference(context, 582 OtherPreferences.KEY_OTHER_QUIET_HOURS_START_MINUTE, 583 OtherPreferences.QUIET_HOURS_DEFAULT_START_MINUTE); 584 quietHoursEndHour = Utils.getSharedPreference(context, 585 OtherPreferences.KEY_OTHER_QUIET_HOURS_END_HOUR, 586 OtherPreferences.QUIET_HOURS_DEFAULT_END_HOUR); 587 quietHoursEndMinute = Utils.getSharedPreference(context, 588 OtherPreferences.KEY_OTHER_QUIET_HOURS_END_MINUTE, 589 OtherPreferences.QUIET_HOURS_DEFAULT_END_MINUTE); 590 } 591 Time time = new Time(); 592 593 ContentResolver cr = context.getContentResolver(); 594 HashMap<Long, NotificationInfo> eventIds = new HashMap<Long, NotificationInfo>(); 595 int numFired = 0; 596 try { 597 while (alertCursor.moveToNext()) { 598 final long alertId = alertCursor.getLong(ALERT_INDEX_ID); 599 final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID); 600 final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES); 601 final String eventName = alertCursor.getString(ALERT_INDEX_TITLE); 602 final String description = alertCursor.getString(ALERT_INDEX_DESCRIPTION); 603 final String location = alertCursor.getString(ALERT_INDEX_EVENT_LOCATION); 604 final int status = alertCursor.getInt(ALERT_INDEX_SELF_ATTENDEE_STATUS); 605 final boolean declined = status == Attendees.ATTENDEE_STATUS_DECLINED; 606 final boolean responded = status != Attendees.ATTENDEE_STATUS_NONE 607 && status != Attendees.ATTENDEE_STATUS_INVITED; 608 final long beginTime = alertCursor.getLong(ALERT_INDEX_BEGIN); 609 final long endTime = alertCursor.getLong(ALERT_INDEX_END); 610 final Uri alertUri = ContentUris 611 .withAppendedId(CalendarAlerts.CONTENT_URI, alertId); 612 final long alarmTime = alertCursor.getLong(ALERT_INDEX_ALARM_TIME); 613 boolean forceQuiet = false; 614 if (useQuietHours) { 615 // Quiet hours have been set. 616 time.set(alarmTime); 617 // Check whether the alarm will fire after the quiet hours 618 // start time and/or before the quiet hours end time. 619 boolean alarmAfterQuietHoursStart = 620 (time.hour > quietHoursStartHour || 621 (time.hour == quietHoursStartHour 622 && time.minute >= quietHoursStartMinute)); 623 boolean alarmBeforeQuietHoursEnd = 624 (time.hour < quietHoursEndHour || 625 (time.hour == quietHoursEndHour 626 && time.minute <= quietHoursEndMinute)); 627 // Check if quiet hours crosses through midnight, iff: 628 // start hour is after end hour, or 629 // start hour is equal to end hour, and start minute is 630 // after end minute. 631 // i.e. 22:30 - 06:45; 12:45 - 12:00 632 // 01:05 - 10:30; 05:00 - 05:30 633 boolean quietHoursCrossesMidnight = 634 quietHoursStartHour > quietHoursEndHour || 635 (quietHoursStartHour == quietHoursEndHour 636 && quietHoursStartMinute > quietHoursEndMinute); 637 if (quietHoursCrossesMidnight) { 638 // Quiet hours crosses midnight. Alarm should be quiet 639 // if it's after start time OR before end time. 640 if (alarmAfterQuietHoursStart || 641 alarmBeforeQuietHoursEnd) { 642 forceQuiet = true; 643 } 644 } else { 645 // Quiet hours doesn't cross midnight. Alarm should be 646 // quiet if it's after start time AND before end time. 647 if (alarmAfterQuietHoursStart && 648 alarmBeforeQuietHoursEnd) { 649 forceQuiet = true; 650 } 651 } 652 } 653 int state = alertCursor.getInt(ALERT_INDEX_STATE); 654 final boolean allDay = alertCursor.getInt(ALERT_INDEX_ALL_DAY) != 0; 655 656 // Use app local storage to keep track of fired alerts to fix problem of multiple 657 // installed calendar apps potentially causing missed alarms. 658 boolean newAlertOverride = false; 659 if (AlertUtils.BYPASS_DB && ((currentTime - alarmTime) / MINUTE_MS < 1)) { 660 // To avoid re-firing alerts, only fire if alarmTime is very recent. Otherwise 661 // we can get refires for non-dismissed alerts after app installation, or if the 662 // SharedPrefs was cleared too early. This means alerts that were timed while 663 // the phone was off may show up silently in the notification bar. 664 boolean alreadyFired = AlertUtils.hasAlertFiredInSharedPrefs(context, eventId, 665 beginTime, alarmTime); 666 if (!alreadyFired) { 667 newAlertOverride = true; 668 } 669 } 670 671 if (DEBUG) { 672 StringBuilder msgBuilder = new StringBuilder(); 673 msgBuilder.append("alertCursor result: alarmTime:").append(alarmTime) 674 .append(" alertId:").append(alertId) 675 .append(" eventId:").append(eventId) 676 .append(" state: ").append(state) 677 .append(" minutes:").append(minutes) 678 .append(" declined:").append(declined) 679 .append(" responded:").append(responded) 680 .append(" beginTime:").append(beginTime) 681 .append(" endTime:").append(endTime) 682 .append(" allDay:").append(allDay) 683 .append(" alarmTime:").append(alarmTime) 684 .append(" forceQuiet:").append(forceQuiet); 685 if (AlertUtils.BYPASS_DB) { 686 msgBuilder.append(" newAlertOverride: " + newAlertOverride); 687 } 688 Log.d(TAG, msgBuilder.toString()); 689 } 690 691 ContentValues values = new ContentValues(); 692 int newState = -1; 693 boolean newAlert = false; 694 695 // Uncomment for the behavior of clearing out alerts after the 696 // events ended. b/1880369 697 // 698 // if (endTime < currentTime) { 699 // newState = CalendarAlerts.DISMISSED; 700 // } else 701 702 // Remove declined events 703 boolean sendAlert = !declined; 704 // Check for experimental reminder settings. 705 if (remindRespondedOnly) { 706 // If the experimental setting is turned on, then only send 707 // the alert if you've responded to the event. 708 sendAlert = sendAlert && responded; 709 } 710 if (sendAlert) { 711 if (state == CalendarAlerts.STATE_SCHEDULED || newAlertOverride) { 712 newState = CalendarAlerts.STATE_FIRED; 713 numFired++; 714 // If quiet hours are forcing the alarm to be silent, 715 // keep newAlert as false so it will not make noise. 716 if (!forceQuiet) { 717 newAlert = true; 718 } 719 720 // Record the received time in the CalendarAlerts table. 721 // This is useful for finding bugs that cause alarms to be 722 // missed or delayed. 723 values.put(CalendarAlerts.RECEIVED_TIME, currentTime); 724 } 725 } else { 726 newState = CalendarAlerts.STATE_DISMISSED; 727 } 728 729 // Update row if state changed 730 if (newState != -1) { 731 values.put(CalendarAlerts.STATE, newState); 732 state = newState; 733 734 if (AlertUtils.BYPASS_DB) { 735 AlertUtils.setAlertFiredInSharedPrefs(context, eventId, beginTime, 736 alarmTime); 737 } 738 } 739 740 if (state == CalendarAlerts.STATE_FIRED) { 741 // Record the time posting to notification manager. 742 // This is used for debugging missed alarms. 743 values.put(CalendarAlerts.NOTIFY_TIME, currentTime); 744 } 745 746 // Write row to if anything changed 747 if (values.size() > 0) cr.update(alertUri, values, null, null); 748 749 if (state != CalendarAlerts.STATE_FIRED) { 750 continue; 751 } 752 753 // TODO: Prefer accepted events in case of ties. 754 NotificationInfo newInfo = new NotificationInfo(eventName, location, 755 description, beginTime, endTime, eventId, allDay, newAlert); 756 757 // Adjust for all day events to ensure the right bucket. Don't use the 1/4 event 758 // duration grace period for these. 759 long beginTimeAdjustedForAllDay = beginTime; 760 String tz = null; 761 if (allDay) { 762 tz = TimeZone.getDefault().getID(); 763 beginTimeAdjustedForAllDay = Utils.convertAlldayUtcToLocal(null, beginTime, 764 tz); 765 } 766 767 // Handle multiple alerts for the same event ID. 768 if (eventIds.containsKey(eventId)) { 769 NotificationInfo oldInfo = eventIds.get(eventId); 770 long oldBeginTimeAdjustedForAllDay = oldInfo.startMillis; 771 if (allDay) { 772 oldBeginTimeAdjustedForAllDay = Utils.convertAlldayUtcToLocal(null, 773 oldInfo.startMillis, tz); 774 } 775 776 // Determine whether to replace the previous reminder with this one. 777 // Query results are sorted so this one will always have a lower start time. 778 long oldStartInterval = oldBeginTimeAdjustedForAllDay - currentTime; 779 long newStartInterval = beginTimeAdjustedForAllDay - currentTime; 780 boolean dropOld; 781 if (newStartInterval < 0 && oldStartInterval > 0) { 782 // Use this reminder if this event started recently 783 dropOld = Math.abs(newStartInterval) < MIN_DEPRIORITIZE_GRACE_PERIOD_MS; 784 } else { 785 // ... or if this one has a closer start time. 786 dropOld = Math.abs(newStartInterval) < Math.abs(oldStartInterval); 787 } 788 789 if (dropOld) { 790 // This is a recurring event that has a more relevant start time, 791 // drop other reminder in favor of this one. 792 // 793 // It will only be present in 1 of these buckets; just remove from 794 // multiple buckets since this occurrence is rare enough that the 795 // inefficiency of multiple removals shouldn't be a big deal to 796 // justify a more complicated data structure. Expired events don't 797 // have individual notifications so we don't need to clean that up. 798 highPriorityEvents.remove(oldInfo); 799 mediumPriorityEvents.remove(oldInfo); 800 if (DEBUG) { 801 Log.d(TAG, "Dropping alert for recurring event ID:" + oldInfo.eventId 802 + ", startTime:" + oldInfo.startMillis 803 + " in favor of startTime:" + newInfo.startMillis); 804 } 805 } else { 806 // Skip duplicate reminders for the same event instance. 807 continue; 808 } 809 } 810 811 // TODO: Prioritize by "primary" calendar 812 eventIds.put(eventId, newInfo); 813 long highPriorityCutoff = currentTime - 814 getGracePeriodMs(beginTime, endTime, allDay); 815 816 if (beginTimeAdjustedForAllDay > highPriorityCutoff) { 817 // High priority = future events or events that just started 818 highPriorityEvents.add(newInfo); 819 } else if (allDay && tz != null && DateUtils.isToday(beginTimeAdjustedForAllDay)) { 820 // Medium priority = in progress all day events 821 mediumPriorityEvents.add(newInfo); 822 } else { 823 lowPriorityEvents.add(newInfo); 824 } 825 } 826 // TODO(psliwowski): move this to account synchronization 827 GlobalDismissManager.processEventIds(context, eventIds.keySet()); 828 } finally { 829 if (alertCursor != null) { 830 alertCursor.close(); 831 } 832 } 833 return numFired; 834 } 835 836 /** 837 * High priority cutoff should be 1/4 event duration or 15 min, whichever is longer. 838 */ 839 private static long getGracePeriodMs(long beginTime, long endTime, boolean allDay) { 840 if (allDay) { 841 // We don't want all day events to be high priority for hours, so automatically 842 // demote these after 15 min. 843 return MIN_DEPRIORITIZE_GRACE_PERIOD_MS; 844 } else { 845 return Math.max(MIN_DEPRIORITIZE_GRACE_PERIOD_MS, ((endTime - beginTime) / 4)); 846 } 847 } 848 849 private static String getDigestTitle(ArrayList<NotificationInfo> events) { 850 StringBuilder digestTitle = new StringBuilder(); 851 for (NotificationInfo eventInfo : events) { 852 if (!TextUtils.isEmpty(eventInfo.eventName)) { 853 if (digestTitle.length() > 0) { 854 digestTitle.append(", "); 855 } 856 digestTitle.append(eventInfo.eventName); 857 } 858 } 859 return digestTitle.toString(); 860 } 861 postNotification(NotificationInfo info, String summaryText, Context context, boolean highPriority, NotificationPrefs prefs, NotificationMgr notificationMgr, int notificationId)862 private static void postNotification(NotificationInfo info, String summaryText, 863 Context context, boolean highPriority, NotificationPrefs prefs, 864 NotificationMgr notificationMgr, int notificationId) { 865 int priorityVal = Notification.PRIORITY_DEFAULT; 866 if (highPriority) { 867 priorityVal = Notification.PRIORITY_HIGH; 868 } 869 870 String tickerText = getTickerText(info.eventName, info.location); 871 NotificationWrapper notification = AlertReceiver.makeExpandingNotification(context, 872 info.eventName, summaryText, info.description, info.startMillis, 873 info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), priorityVal); 874 875 boolean quietUpdate = true; 876 String ringtone = NotificationPrefs.EMPTY_RINGTONE; 877 if (info.newAlert) { 878 quietUpdate = prefs.quietUpdate; 879 880 // If we've already played a ringtone, don't play any more sounds so only 881 // 1 sound per group of notifications. 882 ringtone = prefs.getRingtoneAndSilence(); 883 } 884 addNotificationOptions(notification, quietUpdate, tickerText, 885 prefs.getDefaultVibrate(), ringtone, 886 true); /* Show the LED for these non-expired events */ 887 888 // Post the notification. 889 notificationMgr.notify(notificationId, notification); 890 891 if (DEBUG) { 892 Log.d(TAG, "Posting individual alarm notification, eventId:" + info.eventId 893 + ", notificationId:" + notificationId 894 + (TextUtils.isEmpty(ringtone) ? ", quiet" : ", LOUD") 895 + (highPriority ? ", high-priority" : "")); 896 } 897 } 898 getTickerText(String eventName, String location)899 private static String getTickerText(String eventName, String location) { 900 String tickerText = eventName; 901 if (!TextUtils.isEmpty(location)) { 902 tickerText = eventName + " - " + location; 903 } 904 return tickerText; 905 } 906 907 static class NotificationInfo { 908 String eventName; 909 String location; 910 String description; 911 long startMillis; 912 long endMillis; 913 long eventId; 914 boolean allDay; 915 boolean newAlert; 916 NotificationInfo(String eventName, String location, String description, long startMillis, long endMillis, long eventId, boolean allDay, boolean newAlert)917 NotificationInfo(String eventName, String location, String description, long startMillis, 918 long endMillis, long eventId, boolean allDay, boolean newAlert) { 919 this.eventName = eventName; 920 this.location = location; 921 this.description = description; 922 this.startMillis = startMillis; 923 this.endMillis = endMillis; 924 this.eventId = eventId; 925 this.newAlert = newAlert; 926 this.allDay = allDay; 927 } 928 } 929 addNotificationOptions(NotificationWrapper nw, boolean quietUpdate, String tickerText, boolean defaultVibrate, String reminderRingtone, boolean showLights)930 private static void addNotificationOptions(NotificationWrapper nw, boolean quietUpdate, 931 String tickerText, boolean defaultVibrate, String reminderRingtone, 932 boolean showLights) { 933 Notification notification = nw.mNotification; 934 if (showLights) { 935 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 936 notification.defaults |= Notification.DEFAULT_LIGHTS; 937 } 938 939 // Quietly update notification bar. Nothing new. Maybe something just got deleted. 940 if (!quietUpdate) { 941 // Flash ticker in status bar 942 if (!TextUtils.isEmpty(tickerText)) { 943 notification.tickerText = tickerText; 944 } 945 946 // Generate either a pop-up dialog, status bar notification, or 947 // neither. Pop-up dialog and status bar notification may include a 948 // sound, an alert, or both. A status bar notification also includes 949 // a toast. 950 if (defaultVibrate) { 951 notification.defaults |= Notification.DEFAULT_VIBRATE; 952 } 953 954 // Possibly generate a sound. If 'Silent' is chosen, the ringtone 955 // string will be empty. 956 notification.sound = TextUtils.isEmpty(reminderRingtone) ? null : Uri 957 .parse(reminderRingtone); 958 } 959 } 960 961 /* package */ static class NotificationPrefs { 962 boolean quietUpdate; 963 private Context context; 964 private SharedPreferences prefs; 965 966 // These are lazily initialized, do not access any of the following directly; use getters. 967 private int doPopup = -1; 968 private int defaultVibrate = -1; 969 private String ringtone = null; 970 971 private static final String EMPTY_RINGTONE = ""; 972 NotificationPrefs(Context context, SharedPreferences prefs, boolean quietUpdate)973 NotificationPrefs(Context context, SharedPreferences prefs, boolean quietUpdate) { 974 this.context = context; 975 this.prefs = prefs; 976 this.quietUpdate = quietUpdate; 977 } 978 getDoPopup()979 private boolean getDoPopup() { 980 if (doPopup < 0) { 981 if (prefs.getBoolean(GeneralPreferences.KEY_ALERTS_POPUP, false)) { 982 doPopup = 1; 983 } else { 984 doPopup = 0; 985 } 986 } 987 return doPopup == 1; 988 } 989 getDefaultVibrate()990 private boolean getDefaultVibrate() { 991 if (defaultVibrate < 0) { 992 defaultVibrate = Utils.getDefaultVibrate(context, prefs) ? 1 : 0; 993 } 994 return defaultVibrate == 1; 995 } 996 getRingtoneAndSilence()997 private String getRingtoneAndSilence() { 998 if (ringtone == null) { 999 if (quietUpdate) { 1000 ringtone = EMPTY_RINGTONE; 1001 } else { 1002 ringtone = Utils.getRingTonePreference(context); 1003 } 1004 } 1005 String retVal = ringtone; 1006 ringtone = EMPTY_RINGTONE; 1007 return retVal; 1008 } 1009 } 1010 doTimeChanged()1011 private void doTimeChanged() { 1012 ContentResolver cr = getContentResolver(); 1013 // TODO Move this into Provider 1014 rescheduleMissedAlarms(cr, this, AlertUtils.createAlarmManager(this)); 1015 updateAlertNotification(this); 1016 } 1017 1018 private static final String SORT_ORDER_ALARMTIME_ASC = 1019 CalendarContract.CalendarAlerts.ALARM_TIME + " ASC"; 1020 1021 private static final String WHERE_RESCHEDULE_MISSED_ALARMS = 1022 CalendarContract.CalendarAlerts.STATE 1023 + "=" 1024 + CalendarContract.CalendarAlerts.STATE_SCHEDULED 1025 + " AND " 1026 + CalendarContract.CalendarAlerts.ALARM_TIME 1027 + "<?" 1028 + " AND " 1029 + CalendarContract.CalendarAlerts.ALARM_TIME 1030 + ">?" 1031 + " AND " 1032 + CalendarContract.CalendarAlerts.END + ">=?"; 1033 1034 /** 1035 * Searches the CalendarAlerts table for alarms that should have fired but 1036 * have not and then reschedules them. This method can be called at boot 1037 * time to restore alarms that may have been lost due to a phone reboot. 1038 * 1039 * @param cr the ContentResolver 1040 * @param context the Context 1041 * @param manager the AlarmManager 1042 */ rescheduleMissedAlarms(ContentResolver cr, Context context, AlarmManagerInterface manager)1043 private static final void rescheduleMissedAlarms(ContentResolver cr, Context context, 1044 AlarmManagerInterface manager) { 1045 // Get all the alerts that have been scheduled but have not fired 1046 // and should have fired by now and are not too old. 1047 long now = System.currentTimeMillis(); 1048 long ancient = now - DateUtils.DAY_IN_MILLIS; 1049 String[] projection = new String[] { 1050 CalendarContract.CalendarAlerts.ALARM_TIME, 1051 }; 1052 1053 // TODO: construct an explicit SQL query so that we can add 1054 // "GROUPBY" instead of doing a sort and de-dup 1055 Cursor cursor = cr.query(CalendarAlerts.CONTENT_URI, projection, 1056 WHERE_RESCHEDULE_MISSED_ALARMS, (new String[] { 1057 Long.toString(now), Long.toString(ancient), Long.toString(now) 1058 }), SORT_ORDER_ALARMTIME_ASC); 1059 if (cursor == null) { 1060 return; 1061 } 1062 1063 if (DEBUG) { 1064 Log.d(TAG, "missed alarms found: " + cursor.getCount()); 1065 } 1066 1067 try { 1068 long alarmTime = -1; 1069 1070 while (cursor.moveToNext()) { 1071 long newAlarmTime = cursor.getLong(0); 1072 if (alarmTime != newAlarmTime) { 1073 if (DEBUG) { 1074 Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime); 1075 } 1076 AlertUtils.scheduleAlarm(context, manager, newAlarmTime); 1077 alarmTime = newAlarmTime; 1078 } 1079 } 1080 } finally { 1081 cursor.close(); 1082 } 1083 } 1084 1085 private final class ServiceHandler extends Handler { ServiceHandler(Looper looper)1086 public ServiceHandler(Looper looper) { 1087 super(looper); 1088 } 1089 1090 @Override handleMessage(Message msg)1091 public void handleMessage(Message msg) { 1092 processMessage(msg); 1093 // NOTE: We MUST not call stopSelf() directly, since we need to 1094 // make sure the wake lock acquired by AlertReceiver is released. 1095 AlertReceiver.finishStartingService(AlertService.this, msg.arg1); 1096 } 1097 } 1098 1099 @Override onCreate()1100 public void onCreate() { 1101 HandlerThread thread = new HandlerThread("AlertService", 1102 Process.THREAD_PRIORITY_BACKGROUND); 1103 thread.start(); 1104 1105 mServiceLooper = thread.getLooper(); 1106 mServiceHandler = new ServiceHandler(mServiceLooper); 1107 1108 // Flushes old fired alerts from internal storage, if needed. 1109 AlertUtils.flushOldAlertsFromInternalStorage(getApplication()); 1110 } 1111 1112 @Override onStartCommand(Intent intent, int flags, int startId)1113 public int onStartCommand(Intent intent, int flags, int startId) { 1114 if (intent != null) { 1115 Message msg = mServiceHandler.obtainMessage(); 1116 msg.arg1 = startId; 1117 msg.obj = intent.getExtras(); 1118 mServiceHandler.sendMessage(msg); 1119 } 1120 return START_REDELIVER_INTENT; 1121 } 1122 1123 @Override onDestroy()1124 public void onDestroy() { 1125 mServiceLooper.quit(); 1126 } 1127 1128 @Override onBind(Intent intent)1129 public IBinder onBind(Intent intent) { 1130 return null; 1131 } 1132 } 1133