13a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting/*
23a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * Copyright (C) 2012 The Android Open Source Project
33a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting *
43a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * Licensed under the Apache License, Version 2.0 (the "License");
53a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * you may not use this file except in compliance with the License.
63a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * You may obtain a copy of the License at
73a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting *
83a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting *      http://www.apache.org/licenses/LICENSE-2.0
93a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting *
103a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * Unless required by applicable law or agreed to in writing, software
113a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * distributed under the License is distributed on an "AS IS" BASIS,
123a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
133a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * See the License for the specific language governing permissions and
143a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * limitations under the License.
153a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting */
163a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
173a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingpackage com.android.calendar.alerts;
183a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
193a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.app.AlarmManager;
203a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.app.PendingIntent;
213a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.content.ContentResolver;
223a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.content.ContentUris;
233a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.content.Context;
243a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.content.Intent;
253a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.database.Cursor;
263a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.net.Uri;
273a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.provider.CalendarContract;
283a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.provider.CalendarContract.Events;
293a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.provider.CalendarContract.Instances;
303a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.provider.CalendarContract.Reminders;
313a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.text.format.DateUtils;
323a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.text.format.Time;
333a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport android.util.Log;
343a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
353a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport com.android.calendar.Utils;
363a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
373a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport java.util.ArrayList;
383a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport java.util.HashMap;
393a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport java.util.List;
403a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingimport java.util.Map;
413a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
423a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting/**
433a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events
443a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting * and reminders tables for the next upcoming alert.
453a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting */
463a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Tingpublic class AlarmScheduler {
473a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final String TAG = "AlarmScheduler";
483a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
493a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final String INSTANCES_WHERE = Events.VISIBLE + "=? AND "
503a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND "
513a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            + Events.ALL_DAY + "=?";
523a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    static final String[] INSTANCES_PROJECTION = new String[] {
533a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Instances.EVENT_ID,
543a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Instances.BEGIN,
553a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Instances.ALL_DAY,
563a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    };
573a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int INSTANCES_INDEX_EVENTID = 0;
583a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int INSTANCES_INDEX_BEGIN = 1;
593a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int INSTANCES_INDEX_ALL_DAY = 2;
603a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
613a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final String REMINDERS_WHERE = Reminders.METHOD + "=1 AND "
623a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            + Reminders.EVENT_ID + " IN ";
633a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    static final String[] REMINDERS_PROJECTION = new String[] {
643a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Reminders.EVENT_ID,
653a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Reminders.MINUTES,
663a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Reminders.METHOD,
673a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    };
683a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int REMINDERS_INDEX_EVENT_ID = 0;
693a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int REMINDERS_INDEX_MINUTES = 1;
703a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int REMINDERS_INDEX_METHOD = 2;
713a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
723a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons:
733a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // (1) so that the concurrent reminder broadcast from the provider doesn't result
743a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // in a double ring, and (2) some OEMs modified the provider to not add an alert to
753a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // the CalendarAlerts table until the alert time, so for the unbundled app's
763a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // notifications to work on these devices, a delay ensures that AlertService won't
773a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // read from the CalendarAlerts table until the alert is present.
783a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    static final int ALARM_DELAY_MS = 1000;
793a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
803a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...".  This
813a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // sets the max # of events in the query before batching into multiple queries, to
823a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // limit the SQL query length.
833a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final int REMINDER_QUERY_BATCH_SIZE = 50;
843a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
853a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // We really need to query for reminder times that fall in some interval, but
863a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // the Reminders table only stores the reminder interval (10min, 15min, etc), and
873a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // we cannot do the join with the Events table to calculate the actual alert time
883a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // from outside of the provider.  So the best we can do for now consider events
893a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // whose start times begin within some interval (ie. 1 week out).  This means
903a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // reminders which are configured for more than 1 week out won't fire on time.  We
913a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // can minimize this to being only 1 day late by putting a 1 day max on the alarm time.
923a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final long EVENT_LOOKAHEAD_WINDOW_MS = DateUtils.WEEK_IN_MILLIS;
933a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static final long MAX_ALARM_ELAPSED_MS = DateUtils.DAY_IN_MILLIS;
943a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
953a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    /**
963a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * Schedules the nearest upcoming alarm, to refresh notifications.
973a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     *
983a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * This is historically done in the provider but we dupe this here so the unbundled
993a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * app will work on devices that have modified this portion of the provider.  This
1003a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * has the limitation of querying events within some interval from now (ie. looks at
1013a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * reminders for all events occurring in the next week).  This means for example,
1023a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * a 2 week notification will not fire on time.
1033a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     */
1043a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    public static void scheduleNextAlarm(Context context) {
1053a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        scheduleNextAlarm(context, AlertUtils.createAlarmManager(context),
1063a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis());
1073a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    }
1083a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1093a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    // VisibleForTesting
1103a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    static void scheduleNextAlarm(Context context, AlarmManagerInterface alarmManager,
1113a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            int batchSize, long currentMillis) {
1123a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Cursor instancesCursor = null;
1133a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        try {
1143a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            instancesCursor = queryUpcomingEvents(context, context.getContentResolver(),
1153a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    currentMillis);
1163a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            if (instancesCursor != null) {
1173a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                queryNextReminderAndSchedule(instancesCursor, context,
1183a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                        context.getContentResolver(), alarmManager, batchSize, currentMillis);
1193a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
1203a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        } finally {
1213a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            if (instancesCursor != null) {
1223a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                instancesCursor.close();
1233a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
1243a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
1253a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    }
1263a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1273a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    /**
1283a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * Queries events starting within a fixed interval from now.
1293a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     */
1303a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static Cursor queryUpcomingEvents(Context context, ContentResolver contentResolver,
1313a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            long currentMillis) {
1323a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Time time = new Time();
1333a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        time.normalize(false);
1343a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        long localOffset = time.gmtoff * 1000;
1353a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        final long localStartMin = currentMillis;
1363a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        final long localStartMax = localStartMin + EVENT_LOOKAHEAD_WINDOW_MS;
1373a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        final long utcStartMin = localStartMin - localOffset;
1383a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        final long utcStartMax = utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS;
1393a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1403a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Expand Instances table range by a day on either end to account for
1413a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // all-day events.
1423a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
1433a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        ContentUris.appendId(uriBuilder, localStartMin - DateUtils.DAY_IN_MILLIS);
1443a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS);
1453a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1463a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Build query for all events starting within the fixed interval.
1473a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        StringBuilder queryBuilder = new StringBuilder();
1483a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        queryBuilder.append("(");
1493a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        queryBuilder.append(INSTANCES_WHERE);
1503a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        queryBuilder.append(") OR (");
1513a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        queryBuilder.append(INSTANCES_WHERE);
1523a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        queryBuilder.append(")");
1533a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        String[] queryArgs = new String[] {
1543a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                // allday selection
1553a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                "1",                           /* visible = ? */
1563a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                String.valueOf(utcStartMin),   /* begin >= ? */
1573a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                String.valueOf(utcStartMax),   /* begin <= ? */
1583a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                "1",                           /* allDay = ? */
1593a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1603a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                // non-allday selection
1613a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                "1",                           /* visible = ? */
1623a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                String.valueOf(localStartMin), /* begin >= ? */
1633a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                String.valueOf(localStartMax), /* begin <= ? */
1643a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                "0"                            /* allDay = ? */
1653a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        };
1663a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1673a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Cursor cursor = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION,
1683a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                queryBuilder.toString(), queryArgs, null);
1693a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        return cursor;
1703a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    }
1713a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1723a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    /**
1733a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * Queries for all the reminders of the events in the instancesCursor, and schedules
1743a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * the alarm for the next upcoming reminder.
1753a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     */
1763a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static void queryNextReminderAndSchedule(Cursor instancesCursor, Context context,
1773a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            ContentResolver contentResolver, AlarmManagerInterface alarmManager,
1783a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            int batchSize, long currentMillis) {
1793a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        if (AlertService.DEBUG) {
1803a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            int eventCount = instancesCursor.getCount();
1813a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            if (eventCount == 0) {
1823a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                Log.d(TAG, "No events found starting within 1 week.");
1833a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            } else {
1843a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                Log.d(TAG, "Query result count for events starting within 1 week: " + eventCount);
1853a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
1863a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
1873a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
1883a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Put query results of all events starting within some interval into map of event ID to
1893a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // local start time.
1903a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Map<Integer, List<Long>> eventMap = new HashMap<Integer, List<Long>>();
1913a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Time timeObj = new Time();
1923a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        long nextAlarmTime = Long.MAX_VALUE;
1933a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        int nextAlarmEventId = 0;
1943a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        instancesCursor.moveToPosition(-1);
1953a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        while (!instancesCursor.isAfterLast()) {
1963a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            int index = 0;
1973a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            eventMap.clear();
1983a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            StringBuilder eventIdsForQuery = new StringBuilder();
1993a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            eventIdsForQuery.append('(');
2003a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            while (index++ < batchSize && instancesCursor.moveToNext()) {
2013a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                int eventId = instancesCursor.getInt(INSTANCES_INDEX_EVENTID);
2023a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                long begin = instancesCursor.getLong(INSTANCES_INDEX_BEGIN);
2033a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                boolean allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
2043a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                long localStartTime;
2053a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                if (allday) {
2063a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    // Adjust allday to local time.
2073a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    localStartTime = Utils.convertAlldayUtcToLocal(timeObj, begin,
2083a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            Time.getCurrentTimezone());
2093a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                } else {
2103a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    localStartTime = begin;
2113a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                }
2123a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                List<Long> startTimes = eventMap.get(eventId);
2133a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                if (startTimes == null) {
2143a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    startTimes = new ArrayList<Long>();
2153a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    eventMap.put(eventId, startTimes);
2163a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    eventIdsForQuery.append(eventId);
2173a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    eventIdsForQuery.append(",");
2183a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                }
2193a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                startTimes.add(localStartTime);
2203a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2213a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                // Log for debugging.
2223a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                if (Log.isLoggable(TAG, Log.DEBUG)) {
2233a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    timeObj.set(localStartTime);
2243a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    StringBuilder msg = new StringBuilder();
2253a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    msg.append("Events cursor result -- eventId:").append(eventId);
2263a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    msg.append(", allDay:").append(allday);
2273a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    msg.append(", start:").append(localStartTime);
2283a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")");
2293a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    Log.d(TAG, msg.toString());
2303a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                }
2313a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
2323a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            if (eventIdsForQuery.charAt(eventIdsForQuery.length() - 1) == ',') {
2333a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                eventIdsForQuery.deleteCharAt(eventIdsForQuery.length() - 1);
2343a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
2353a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            eventIdsForQuery.append(')');
2363a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2373a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            // Query the reminders table for the events found.
2383a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            Cursor cursor = null;
2393a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            try {
2403a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                cursor = contentResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
2413a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                        REMINDERS_WHERE + eventIdsForQuery, null, null);
2423a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2433a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                // Process the reminders query results to find the next reminder time.
2443a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                cursor.moveToPosition(-1);
2453a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                while (cursor.moveToNext()) {
2463a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    int eventId = cursor.getInt(REMINDERS_INDEX_EVENT_ID);
2473a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    int reminderMinutes = cursor.getInt(REMINDERS_INDEX_MINUTES);
2483a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    List<Long> startTimes = eventMap.get(eventId);
2493a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    if (startTimes != null) {
2503a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                        for (Long startTime : startTimes) {
2513a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            long alarmTime = startTime -
2523a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                    reminderMinutes * DateUtils.MINUTE_IN_MILLIS;
2533a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            if (alarmTime > currentMillis && alarmTime < nextAlarmTime) {
2543a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                nextAlarmTime = alarmTime;
2553a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                nextAlarmEventId = eventId;
2563a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            }
2573a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2583a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            if (Log.isLoggable(TAG, Log.DEBUG)) {
2593a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                timeObj.set(alarmTime);
2603a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                StringBuilder msg = new StringBuilder();
2613a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                msg.append("Reminders cursor result -- eventId:").append(eventId);
2623a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                msg.append(", startTime:").append(startTime);
2633a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                msg.append(", minutes:").append(reminderMinutes);
2643a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                msg.append(", alarmTime:").append(alarmTime);
2653a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P"))
2663a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                        .append(")");
2673a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                                Log.d(TAG, msg.toString());
2683a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                            }
2693a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                        }
2703a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    }
2713a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                }
2723a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            } finally {
2733a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                if (cursor != null) {
2743a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    cursor.close();
2753a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                }
2763a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            }
2773a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
2783a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2793a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Schedule the alarm for the next reminder time.
2803a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        if (nextAlarmTime < Long.MAX_VALUE) {
2813a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            scheduleAlarm(context, nextAlarmEventId, nextAlarmTime, currentMillis, alarmManager);
2823a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
2833a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    }
2843a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
2853a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    /**
2863a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified
2873a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * alarm time with a slight delay (to account for the possible duplicate broadcast
2883a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     * from the provider).
2893a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting     */
2903a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    private static void scheduleAlarm(Context context, long eventId, long alarmTime,
2913a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            long currentMillis, AlarmManagerInterface alarmManager) {
2923a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Max out the alarm time to 1 day out, so an alert for an event far in the future
2933a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // (not present in our event query results for a limited range) can only be at
2943a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // most 1 day late.
2953a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        long maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS;
2963a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        if (alarmTime > maxAlarmTime) {
2973a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            alarmTime = maxAlarmTime;
2983a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
2993a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
3003a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Add a slight delay (see comments on the member var).
3013a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        alarmTime += ALARM_DELAY_MS;
3023a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
3033a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        if (AlertService.DEBUG) {
3043a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            Time time = new Time();
3053a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            time.set(alarmTime);
3063a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            String schedTime = time.format("%a, %b %d, %Y %I:%M%P");
3073a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting            Log.d(TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId
3083a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting                    + " at " + alarmTime + " (" + schedTime + ")");
3093a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        }
3103a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting
3113a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager.  The extra is
3123a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // only used by AlertService for logging.  It is ignored by Intent.filterEquals,
3133a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // so this scheduling will still overwrite the alarm that was previously pending.
3143a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // Note that the 'setClass' is required, because otherwise it seems the broadcast
3153a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        // can be eaten by other apps and we somehow may never receive it.
3163a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION);
3173a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        intent.setClass(context, AlertReceiver.class);
3183a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
3193a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
3203a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting        alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi);
3213a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting    }
3223a07a68da6460c36a5dbec5b8828baa4355dbe04Sara Ting}
323