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