Home | History | Annotate | Download | only in alerts
      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 
    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 
    142         public NotificationWrapper(Notification n) {
    143             mNotification = n;
    144         }
    145 
    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 
    158         public NotificationMgrWrapper(NotificationManager nm) {
    159             mNm = nm;
    160         }
    161 
    162         @Override
    163         public void cancel(int id) {
    164             mNm.cancel(id);
    165         }
    166 
    167         @Override
    168         public void notify(int id, NotificationWrapper nw) {
    169             mNm.notify(id, nw.mNotification);
    170         }
    171     }
    172 
    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 
    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 
    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 
    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      */
    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 
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    973         NotificationPrefs(Context context, SharedPreferences prefs, boolean quietUpdate) {
    974             this.context = context;
    975             this.prefs = prefs;
    976             this.quietUpdate = quietUpdate;
    977         }
    978 
    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 
    990         private boolean getDefaultVibrate() {
    991             if (defaultVibrate < 0) {
    992                 defaultVibrate = Utils.getDefaultVibrate(context, prefs) ? 1 : 0;
    993             }
    994             return defaultVibrate == 1;
    995         }
    996 
    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 
   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      */
   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 {
   1086         public ServiceHandler(Looper looper) {
   1087             super(looper);
   1088         }
   1089 
   1090         @Override
   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
   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
   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
   1124     public void onDestroy() {
   1125         mServiceLooper.quit();
   1126     }
   1127 
   1128     @Override
   1129     public IBinder onBind(Intent intent) {
   1130         return null;
   1131     }
   1132 }
   1133