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