Loading src/com/android/calendar/GeneralPreferences.java +1 −1 Original line number Diff line number Diff line Loading @@ -222,7 +222,7 @@ public class GeneralPreferences extends PreferenceFragment implements if (mAlert.isChecked()) { intent.setAction(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS); } else { intent.setAction(CalendarContract.ACTION_EVENT_REMINDER); intent.setAction(AlertReceiver.EVENT_REMINDER_APP_ACTION); } a.sendBroadcast(intent); } Loading src/com/android/calendar/Utils.java +1 −1 Original line number Diff line number Diff line Loading @@ -278,7 +278,7 @@ public class Utils { return mTardis; } static void setSharedPreference(Context context, String key, boolean value) { public static void setSharedPreference(Context context, String key, boolean value) { SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(key, value); Loading src/com/android/calendar/alerts/AlarmScheduler.java 0 → 100644 +322 −0 Original line number Diff line number Diff line /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.alerts; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.CalendarContract; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Instances; import android.provider.CalendarContract.Reminders; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Log; import com.android.calendar.Utils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events * and reminders tables for the next upcoming alert. */ public class AlarmScheduler { private static final String TAG = "AlarmScheduler"; private static final String INSTANCES_WHERE = Events.VISIBLE + "=? AND " + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND " + Events.ALL_DAY + "=?"; static final String[] INSTANCES_PROJECTION = new String[] { Instances.EVENT_ID, Instances.BEGIN, Instances.ALL_DAY, }; private static final int INSTANCES_INDEX_EVENTID = 0; private static final int INSTANCES_INDEX_BEGIN = 1; private static final int INSTANCES_INDEX_ALL_DAY = 2; private static final String REMINDERS_WHERE = Reminders.METHOD + "=1 AND " + Reminders.EVENT_ID + " IN "; static final String[] REMINDERS_PROJECTION = new String[] { Reminders.EVENT_ID, Reminders.MINUTES, Reminders.METHOD, }; private static final int REMINDERS_INDEX_EVENT_ID = 0; private static final int REMINDERS_INDEX_MINUTES = 1; private static final int REMINDERS_INDEX_METHOD = 2; // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons: // (1) so that the concurrent reminder broadcast from the provider doesn't result // in a double ring, and (2) some OEMs modified the provider to not add an alert to // the CalendarAlerts table until the alert time, so for the unbundled app's // notifications to work on these devices, a delay ensures that AlertService won't // read from the CalendarAlerts table until the alert is present. static final int ALARM_DELAY_MS = 1000; // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...". This // sets the max # of events in the query before batching into multiple queries, to // limit the SQL query length. private static final int REMINDER_QUERY_BATCH_SIZE = 50; // We really need to query for reminder times that fall in some interval, but // the Reminders table only stores the reminder interval (10min, 15min, etc), and // we cannot do the join with the Events table to calculate the actual alert time // from outside of the provider. So the best we can do for now consider events // whose start times begin within some interval (ie. 1 week out). This means // reminders which are configured for more than 1 week out won't fire on time. We // can minimize this to being only 1 day late by putting a 1 day max on the alarm time. private static final long EVENT_LOOKAHEAD_WINDOW_MS = DateUtils.WEEK_IN_MILLIS; private static final long MAX_ALARM_ELAPSED_MS = DateUtils.DAY_IN_MILLIS; /** * Schedules the nearest upcoming alarm, to refresh notifications. * * This is historically done in the provider but we dupe this here so the unbundled * app will work on devices that have modified this portion of the provider. This * has the limitation of querying events within some interval from now (ie. looks at * reminders for all events occurring in the next week). This means for example, * a 2 week notification will not fire on time. */ public static void scheduleNextAlarm(Context context) { scheduleNextAlarm(context, AlertUtils.createAlarmManager(context), REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis()); } // VisibleForTesting static void scheduleNextAlarm(Context context, AlarmManagerInterface alarmManager, int batchSize, long currentMillis) { Cursor instancesCursor = null; try { instancesCursor = queryUpcomingEvents(context, context.getContentResolver(), currentMillis); if (instancesCursor != null) { queryNextReminderAndSchedule(instancesCursor, context, context.getContentResolver(), alarmManager, batchSize, currentMillis); } } finally { if (instancesCursor != null) { instancesCursor.close(); } } } /** * Queries events starting within a fixed interval from now. */ private static Cursor queryUpcomingEvents(Context context, ContentResolver contentResolver, long currentMillis) { Time time = new Time(); time.normalize(false); long localOffset = time.gmtoff * 1000; final long localStartMin = currentMillis; final long localStartMax = localStartMin + EVENT_LOOKAHEAD_WINDOW_MS; final long utcStartMin = localStartMin - localOffset; final long utcStartMax = utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS; // Expand Instances table range by a day on either end to account for // all-day events. Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon(); ContentUris.appendId(uriBuilder, localStartMin - DateUtils.DAY_IN_MILLIS); ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS); // Build query for all events starting within the fixed interval. StringBuilder queryBuilder = new StringBuilder(); queryBuilder.append("("); queryBuilder.append(INSTANCES_WHERE); queryBuilder.append(") OR ("); queryBuilder.append(INSTANCES_WHERE); queryBuilder.append(")"); String[] queryArgs = new String[] { // allday selection "1", /* visible = ? */ String.valueOf(utcStartMin), /* begin >= ? */ String.valueOf(utcStartMax), /* begin <= ? */ "1", /* allDay = ? */ // non-allday selection "1", /* visible = ? */ String.valueOf(localStartMin), /* begin >= ? */ String.valueOf(localStartMax), /* begin <= ? */ "0" /* allDay = ? */ }; Cursor cursor = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION, queryBuilder.toString(), queryArgs, null); return cursor; } /** * Queries for all the reminders of the events in the instancesCursor, and schedules * the alarm for the next upcoming reminder. */ private static void queryNextReminderAndSchedule(Cursor instancesCursor, Context context, ContentResolver contentResolver, AlarmManagerInterface alarmManager, int batchSize, long currentMillis) { if (AlertService.DEBUG) { int eventCount = instancesCursor.getCount(); if (eventCount == 0) { Log.d(TAG, "No events found starting within 1 week."); } else { Log.d(TAG, "Query result count for events starting within 1 week: " + eventCount); } } // Put query results of all events starting within some interval into map of event ID to // local start time. Map<Integer, List<Long>> eventMap = new HashMap<Integer, List<Long>>(); Time timeObj = new Time(); long nextAlarmTime = Long.MAX_VALUE; int nextAlarmEventId = 0; instancesCursor.moveToPosition(-1); while (!instancesCursor.isAfterLast()) { int index = 0; eventMap.clear(); StringBuilder eventIdsForQuery = new StringBuilder(); eventIdsForQuery.append('('); while (index++ < batchSize && instancesCursor.moveToNext()) { int eventId = instancesCursor.getInt(INSTANCES_INDEX_EVENTID); long begin = instancesCursor.getLong(INSTANCES_INDEX_BEGIN); boolean allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0; long localStartTime; if (allday) { // Adjust allday to local time. localStartTime = Utils.convertAlldayUtcToLocal(timeObj, begin, Time.getCurrentTimezone()); } else { localStartTime = begin; } List<Long> startTimes = eventMap.get(eventId); if (startTimes == null) { startTimes = new ArrayList<Long>(); eventMap.put(eventId, startTimes); eventIdsForQuery.append(eventId); eventIdsForQuery.append(","); } startTimes.add(localStartTime); // Log for debugging. if (Log.isLoggable(TAG, Log.DEBUG)) { timeObj.set(localStartTime); StringBuilder msg = new StringBuilder(); msg.append("Events cursor result -- eventId:").append(eventId); msg.append(", allDay:").append(allday); msg.append(", start:").append(localStartTime); msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")"); Log.d(TAG, msg.toString()); } } if (eventIdsForQuery.charAt(eventIdsForQuery.length() - 1) == ',') { eventIdsForQuery.deleteCharAt(eventIdsForQuery.length() - 1); } eventIdsForQuery.append(')'); // Query the reminders table for the events found. Cursor cursor = null; try { cursor = contentResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, REMINDERS_WHERE + eventIdsForQuery, null, null); // Process the reminders query results to find the next reminder time. cursor.moveToPosition(-1); while (cursor.moveToNext()) { int eventId = cursor.getInt(REMINDERS_INDEX_EVENT_ID); int reminderMinutes = cursor.getInt(REMINDERS_INDEX_MINUTES); List<Long> startTimes = eventMap.get(eventId); if (startTimes != null) { for (Long startTime : startTimes) { long alarmTime = startTime - reminderMinutes * DateUtils.MINUTE_IN_MILLIS; if (alarmTime > currentMillis && alarmTime < nextAlarmTime) { nextAlarmTime = alarmTime; nextAlarmEventId = eventId; } if (Log.isLoggable(TAG, Log.DEBUG)) { timeObj.set(alarmTime); StringBuilder msg = new StringBuilder(); msg.append("Reminders cursor result -- eventId:").append(eventId); msg.append(", startTime:").append(startTime); msg.append(", minutes:").append(reminderMinutes); msg.append(", alarmTime:").append(alarmTime); msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")) .append(")"); Log.d(TAG, msg.toString()); } } } } } finally { if (cursor != null) { cursor.close(); } } } // Schedule the alarm for the next reminder time. if (nextAlarmTime < Long.MAX_VALUE) { scheduleAlarm(context, nextAlarmEventId, nextAlarmTime, currentMillis, alarmManager); } } /** * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified * alarm time with a slight delay (to account for the possible duplicate broadcast * from the provider). */ private static void scheduleAlarm(Context context, long eventId, long alarmTime, long currentMillis, AlarmManagerInterface alarmManager) { // Max out the alarm time to 1 day out, so an alert for an event far in the future // (not present in our event query results for a limited range) can only be at // most 1 day late. long maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS; if (alarmTime > maxAlarmTime) { alarmTime = maxAlarmTime; } // Add a slight delay (see comments on the member var). alarmTime += ALARM_DELAY_MS; if (AlertService.DEBUG) { Time time = new Time(); time.set(alarmTime); String schedTime = time.format("%a, %b %d, %Y %I:%M%P"); Log.d(TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId + " at " + alarmTime + " (" + schedTime + ")"); } // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager. The extra is // only used by AlertService for logging. It is ignored by Intent.filterEquals, // so this scheduling will still overwrite the alarm that was previously pending. // Note that the 'setClass' is required, because otherwise it seems the broadcast // can be eaten by other apps and we somehow may never receive it. Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION); intent.setClass(context, AlertReceiver.class); intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0); alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); } } src/com/android/calendar/alerts/AlertReceiver.java +5 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,11 @@ public class AlertReceiver extends BroadcastReceiver { private static final String MAIL_ACTION = "com.android.calendar.MAIL"; private static final String EXTRA_EVENT_ID = "eventid"; // The broadcast for notification refreshes scheduled by the app. This is to // distinguish the EVENT_REMINDER broadcast sent by the provider. public static final String EVENT_REMINDER_APP_ACTION = "com.android.calendar.EVENT_REMINDER_APP"; static final Object mStartingServiceSync = new Object(); static PowerManager.WakeLock mStartingService; private static final Pattern mBlankLinePattern = Pattern.compile("^\\s*$[\n\r]", Loading src/com/android/calendar/alerts/AlertService.java +69 −1 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import com.android.calendar.Utils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; import java.util.TimeZone; /** Loading Loading @@ -111,6 +112,19 @@ public class AlertService extends Service { // Hard limit to the number of notifications displayed. public static final int MAX_NOTIFICATIONS = 20; // Shared prefs key for storing whether the EVENT_REMINDER event from the provider // was ever received. Some OEMs modified this provider broadcast, so we had to // do the alarm scheduling here in the app, for the unbundled app's reminders to work. // If the EVENT_REMINDER event was ever received, we know we can skip our secondary // alarm scheduling. private static final String PROVIDER_REMINDER_PREF_KEY = "preference_received_provider_reminder_broadcast"; private static Boolean sReceivedProviderReminderBroadcast = null; // Temporary constants for the experiment to force some users to rely on AlarmScheduler // in the app for reminders. private static final String REMINDER_EXPERIMENT_PREF_KEY = "preference_reminder_exp"; // Added wrapper for testing public static class NotificationWrapper { Notification mNotification; Loading Loading @@ -172,8 +186,38 @@ public class AlertService extends Service { + " Action = " + action); } if (action.equals(Intent.ACTION_PROVIDER_CHANGED) || // In experiment, drop any action from EVENT_REMINDER broadcast, and rely only // on EVENT_REMINDER_APP broadcast. boolean inReminderExperiment = inReminderSchedulingExperiment(); // Some OEMs had changed the provider's EVENT_REMINDER broadcast to their own event, // which broke our unbundled app's reminders. So we added backup alarm scheduling to the // app, but we know we can turn it off if we ever receive the EVENT_REMINDER broadcast. boolean providerReminder = action.equals( android.provider.CalendarContract.ACTION_EVENT_REMINDER); if (providerReminder) { if (sReceivedProviderReminderBroadcast == null) { sReceivedProviderReminderBroadcast = Utils.getSharedPreference(this, PROVIDER_REMINDER_PREF_KEY, false); } if (!sReceivedProviderReminderBroadcast) { sReceivedProviderReminderBroadcast = true; Log.d(TAG, "Setting key " + PROVIDER_REMINDER_PREF_KEY + " to: true"); Utils.setSharedPreference(this, PROVIDER_REMINDER_PREF_KEY, true); } if (inReminderExperiment) { Log.d(TAG, "In reminder scheduling experiment, dropping action from " + "provider's EVENT_REMINDER broadcast."); return; } } if (providerReminder || action.equals(Intent.ACTION_PROVIDER_CHANGED) || action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) || action.equals(AlertReceiver.EVENT_REMINDER_APP_ACTION) || action.equals(Intent.ACTION_LOCALE_CHANGED)) { updateAlertNotification(this); } else if (action.equals(Intent.ACTION_BOOT_COMPLETED) Loading @@ -184,6 +228,30 @@ public class AlertService extends Service { } else { Log.w(TAG, "Invalid action: " + action); } // Schedule the alarm for the next upcoming reminder, if not done by the provider. if (sReceivedProviderReminderBroadcast == null || !sReceivedProviderReminderBroadcast || inReminderExperiment) { Log.d(TAG, "Scheduling next alarm with AlarmScheduler. " + "sEventReminderReceived: " + sReceivedProviderReminderBroadcast + ", inReminderExperiment: " + inReminderExperiment); AlarmScheduler.scheduleNextAlarm(this); } } /** * Temporary way to force some users through the alarm scheduling done in the app. */ private boolean inReminderSchedulingExperiment() { SharedPreferences prefs = GeneralPreferences.getSharedPreferences(this); if (!prefs.contains(REMINDER_EXPERIMENT_PREF_KEY)) { boolean inExperiment = new Random().nextBoolean(); Utils.setSharedPreference(this, REMINDER_EXPERIMENT_PREF_KEY, inExperiment); Log.d(TAG, "Setting key " + REMINDER_EXPERIMENT_PREF_KEY + " to: " + inExperiment); return inExperiment; } return prefs.getBoolean(REMINDER_EXPERIMENT_PREF_KEY, true); } static void dismissOldAlerts(Context context) { Loading Loading
src/com/android/calendar/GeneralPreferences.java +1 −1 Original line number Diff line number Diff line Loading @@ -222,7 +222,7 @@ public class GeneralPreferences extends PreferenceFragment implements if (mAlert.isChecked()) { intent.setAction(AlertReceiver.ACTION_DISMISS_OLD_REMINDERS); } else { intent.setAction(CalendarContract.ACTION_EVENT_REMINDER); intent.setAction(AlertReceiver.EVENT_REMINDER_APP_ACTION); } a.sendBroadcast(intent); } Loading
src/com/android/calendar/Utils.java +1 −1 Original line number Diff line number Diff line Loading @@ -278,7 +278,7 @@ public class Utils { return mTardis; } static void setSharedPreference(Context context, String key, boolean value) { public static void setSharedPreference(Context context, String key, boolean value) { SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(key, value); Loading
src/com/android/calendar/alerts/AlarmScheduler.java 0 → 100644 +322 −0 Original line number Diff line number Diff line /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.alerts; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.CalendarContract; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Instances; import android.provider.CalendarContract.Reminders; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Log; import com.android.calendar.Utils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Schedules the next EVENT_REMINDER_APP broadcast with AlarmManager, by querying the events * and reminders tables for the next upcoming alert. */ public class AlarmScheduler { private static final String TAG = "AlarmScheduler"; private static final String INSTANCES_WHERE = Events.VISIBLE + "=? AND " + Instances.BEGIN + ">=? AND " + Instances.BEGIN + "<=? AND " + Events.ALL_DAY + "=?"; static final String[] INSTANCES_PROJECTION = new String[] { Instances.EVENT_ID, Instances.BEGIN, Instances.ALL_DAY, }; private static final int INSTANCES_INDEX_EVENTID = 0; private static final int INSTANCES_INDEX_BEGIN = 1; private static final int INSTANCES_INDEX_ALL_DAY = 2; private static final String REMINDERS_WHERE = Reminders.METHOD + "=1 AND " + Reminders.EVENT_ID + " IN "; static final String[] REMINDERS_PROJECTION = new String[] { Reminders.EVENT_ID, Reminders.MINUTES, Reminders.METHOD, }; private static final int REMINDERS_INDEX_EVENT_ID = 0; private static final int REMINDERS_INDEX_MINUTES = 1; private static final int REMINDERS_INDEX_METHOD = 2; // Add a slight delay for the EVENT_REMINDER_APP broadcast for a couple reasons: // (1) so that the concurrent reminder broadcast from the provider doesn't result // in a double ring, and (2) some OEMs modified the provider to not add an alert to // the CalendarAlerts table until the alert time, so for the unbundled app's // notifications to work on these devices, a delay ensures that AlertService won't // read from the CalendarAlerts table until the alert is present. static final int ALARM_DELAY_MS = 1000; // The reminders query looks like "SELECT ... AND eventId IN 101,102,202,...". This // sets the max # of events in the query before batching into multiple queries, to // limit the SQL query length. private static final int REMINDER_QUERY_BATCH_SIZE = 50; // We really need to query for reminder times that fall in some interval, but // the Reminders table only stores the reminder interval (10min, 15min, etc), and // we cannot do the join with the Events table to calculate the actual alert time // from outside of the provider. So the best we can do for now consider events // whose start times begin within some interval (ie. 1 week out). This means // reminders which are configured for more than 1 week out won't fire on time. We // can minimize this to being only 1 day late by putting a 1 day max on the alarm time. private static final long EVENT_LOOKAHEAD_WINDOW_MS = DateUtils.WEEK_IN_MILLIS; private static final long MAX_ALARM_ELAPSED_MS = DateUtils.DAY_IN_MILLIS; /** * Schedules the nearest upcoming alarm, to refresh notifications. * * This is historically done in the provider but we dupe this here so the unbundled * app will work on devices that have modified this portion of the provider. This * has the limitation of querying events within some interval from now (ie. looks at * reminders for all events occurring in the next week). This means for example, * a 2 week notification will not fire on time. */ public static void scheduleNextAlarm(Context context) { scheduleNextAlarm(context, AlertUtils.createAlarmManager(context), REMINDER_QUERY_BATCH_SIZE, System.currentTimeMillis()); } // VisibleForTesting static void scheduleNextAlarm(Context context, AlarmManagerInterface alarmManager, int batchSize, long currentMillis) { Cursor instancesCursor = null; try { instancesCursor = queryUpcomingEvents(context, context.getContentResolver(), currentMillis); if (instancesCursor != null) { queryNextReminderAndSchedule(instancesCursor, context, context.getContentResolver(), alarmManager, batchSize, currentMillis); } } finally { if (instancesCursor != null) { instancesCursor.close(); } } } /** * Queries events starting within a fixed interval from now. */ private static Cursor queryUpcomingEvents(Context context, ContentResolver contentResolver, long currentMillis) { Time time = new Time(); time.normalize(false); long localOffset = time.gmtoff * 1000; final long localStartMin = currentMillis; final long localStartMax = localStartMin + EVENT_LOOKAHEAD_WINDOW_MS; final long utcStartMin = localStartMin - localOffset; final long utcStartMax = utcStartMin + EVENT_LOOKAHEAD_WINDOW_MS; // Expand Instances table range by a day on either end to account for // all-day events. Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon(); ContentUris.appendId(uriBuilder, localStartMin - DateUtils.DAY_IN_MILLIS); ContentUris.appendId(uriBuilder, localStartMax + DateUtils.DAY_IN_MILLIS); // Build query for all events starting within the fixed interval. StringBuilder queryBuilder = new StringBuilder(); queryBuilder.append("("); queryBuilder.append(INSTANCES_WHERE); queryBuilder.append(") OR ("); queryBuilder.append(INSTANCES_WHERE); queryBuilder.append(")"); String[] queryArgs = new String[] { // allday selection "1", /* visible = ? */ String.valueOf(utcStartMin), /* begin >= ? */ String.valueOf(utcStartMax), /* begin <= ? */ "1", /* allDay = ? */ // non-allday selection "1", /* visible = ? */ String.valueOf(localStartMin), /* begin >= ? */ String.valueOf(localStartMax), /* begin <= ? */ "0" /* allDay = ? */ }; Cursor cursor = contentResolver.query(uriBuilder.build(), INSTANCES_PROJECTION, queryBuilder.toString(), queryArgs, null); return cursor; } /** * Queries for all the reminders of the events in the instancesCursor, and schedules * the alarm for the next upcoming reminder. */ private static void queryNextReminderAndSchedule(Cursor instancesCursor, Context context, ContentResolver contentResolver, AlarmManagerInterface alarmManager, int batchSize, long currentMillis) { if (AlertService.DEBUG) { int eventCount = instancesCursor.getCount(); if (eventCount == 0) { Log.d(TAG, "No events found starting within 1 week."); } else { Log.d(TAG, "Query result count for events starting within 1 week: " + eventCount); } } // Put query results of all events starting within some interval into map of event ID to // local start time. Map<Integer, List<Long>> eventMap = new HashMap<Integer, List<Long>>(); Time timeObj = new Time(); long nextAlarmTime = Long.MAX_VALUE; int nextAlarmEventId = 0; instancesCursor.moveToPosition(-1); while (!instancesCursor.isAfterLast()) { int index = 0; eventMap.clear(); StringBuilder eventIdsForQuery = new StringBuilder(); eventIdsForQuery.append('('); while (index++ < batchSize && instancesCursor.moveToNext()) { int eventId = instancesCursor.getInt(INSTANCES_INDEX_EVENTID); long begin = instancesCursor.getLong(INSTANCES_INDEX_BEGIN); boolean allday = instancesCursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0; long localStartTime; if (allday) { // Adjust allday to local time. localStartTime = Utils.convertAlldayUtcToLocal(timeObj, begin, Time.getCurrentTimezone()); } else { localStartTime = begin; } List<Long> startTimes = eventMap.get(eventId); if (startTimes == null) { startTimes = new ArrayList<Long>(); eventMap.put(eventId, startTimes); eventIdsForQuery.append(eventId); eventIdsForQuery.append(","); } startTimes.add(localStartTime); // Log for debugging. if (Log.isLoggable(TAG, Log.DEBUG)) { timeObj.set(localStartTime); StringBuilder msg = new StringBuilder(); msg.append("Events cursor result -- eventId:").append(eventId); msg.append(", allDay:").append(allday); msg.append(", start:").append(localStartTime); msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")).append(")"); Log.d(TAG, msg.toString()); } } if (eventIdsForQuery.charAt(eventIdsForQuery.length() - 1) == ',') { eventIdsForQuery.deleteCharAt(eventIdsForQuery.length() - 1); } eventIdsForQuery.append(')'); // Query the reminders table for the events found. Cursor cursor = null; try { cursor = contentResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, REMINDERS_WHERE + eventIdsForQuery, null, null); // Process the reminders query results to find the next reminder time. cursor.moveToPosition(-1); while (cursor.moveToNext()) { int eventId = cursor.getInt(REMINDERS_INDEX_EVENT_ID); int reminderMinutes = cursor.getInt(REMINDERS_INDEX_MINUTES); List<Long> startTimes = eventMap.get(eventId); if (startTimes != null) { for (Long startTime : startTimes) { long alarmTime = startTime - reminderMinutes * DateUtils.MINUTE_IN_MILLIS; if (alarmTime > currentMillis && alarmTime < nextAlarmTime) { nextAlarmTime = alarmTime; nextAlarmEventId = eventId; } if (Log.isLoggable(TAG, Log.DEBUG)) { timeObj.set(alarmTime); StringBuilder msg = new StringBuilder(); msg.append("Reminders cursor result -- eventId:").append(eventId); msg.append(", startTime:").append(startTime); msg.append(", minutes:").append(reminderMinutes); msg.append(", alarmTime:").append(alarmTime); msg.append(" (").append(timeObj.format("%a, %b %d, %Y %I:%M%P")) .append(")"); Log.d(TAG, msg.toString()); } } } } } finally { if (cursor != null) { cursor.close(); } } } // Schedule the alarm for the next reminder time. if (nextAlarmTime < Long.MAX_VALUE) { scheduleAlarm(context, nextAlarmEventId, nextAlarmTime, currentMillis, alarmManager); } } /** * Schedules an alarm for the EVENT_REMINDER_APP broadcast, for the specified * alarm time with a slight delay (to account for the possible duplicate broadcast * from the provider). */ private static void scheduleAlarm(Context context, long eventId, long alarmTime, long currentMillis, AlarmManagerInterface alarmManager) { // Max out the alarm time to 1 day out, so an alert for an event far in the future // (not present in our event query results for a limited range) can only be at // most 1 day late. long maxAlarmTime = currentMillis + MAX_ALARM_ELAPSED_MS; if (alarmTime > maxAlarmTime) { alarmTime = maxAlarmTime; } // Add a slight delay (see comments on the member var). alarmTime += ALARM_DELAY_MS; if (AlertService.DEBUG) { Time time = new Time(); time.set(alarmTime); String schedTime = time.format("%a, %b %d, %Y %I:%M%P"); Log.d(TAG, "Scheduling alarm for EVENT_REMINDER_APP broadcast for event " + eventId + " at " + alarmTime + " (" + schedTime + ")"); } // Schedule an EVENT_REMINDER_APP broadcast with AlarmManager. The extra is // only used by AlertService for logging. It is ignored by Intent.filterEquals, // so this scheduling will still overwrite the alarm that was previously pending. // Note that the 'setClass' is required, because otherwise it seems the broadcast // can be eaten by other apps and we somehow may never receive it. Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION); intent.setClass(context, AlertReceiver.class); intent.putExtra(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0); alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); } }
src/com/android/calendar/alerts/AlertReceiver.java +5 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,11 @@ public class AlertReceiver extends BroadcastReceiver { private static final String MAIL_ACTION = "com.android.calendar.MAIL"; private static final String EXTRA_EVENT_ID = "eventid"; // The broadcast for notification refreshes scheduled by the app. This is to // distinguish the EVENT_REMINDER broadcast sent by the provider. public static final String EVENT_REMINDER_APP_ACTION = "com.android.calendar.EVENT_REMINDER_APP"; static final Object mStartingServiceSync = new Object(); static PowerManager.WakeLock mStartingService; private static final Pattern mBlankLinePattern = Pattern.compile("^\\s*$[\n\r]", Loading
src/com/android/calendar/alerts/AlertService.java +69 −1 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import com.android.calendar.Utils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; import java.util.TimeZone; /** Loading Loading @@ -111,6 +112,19 @@ public class AlertService extends Service { // Hard limit to the number of notifications displayed. public static final int MAX_NOTIFICATIONS = 20; // Shared prefs key for storing whether the EVENT_REMINDER event from the provider // was ever received. Some OEMs modified this provider broadcast, so we had to // do the alarm scheduling here in the app, for the unbundled app's reminders to work. // If the EVENT_REMINDER event was ever received, we know we can skip our secondary // alarm scheduling. private static final String PROVIDER_REMINDER_PREF_KEY = "preference_received_provider_reminder_broadcast"; private static Boolean sReceivedProviderReminderBroadcast = null; // Temporary constants for the experiment to force some users to rely on AlarmScheduler // in the app for reminders. private static final String REMINDER_EXPERIMENT_PREF_KEY = "preference_reminder_exp"; // Added wrapper for testing public static class NotificationWrapper { Notification mNotification; Loading Loading @@ -172,8 +186,38 @@ public class AlertService extends Service { + " Action = " + action); } if (action.equals(Intent.ACTION_PROVIDER_CHANGED) || // In experiment, drop any action from EVENT_REMINDER broadcast, and rely only // on EVENT_REMINDER_APP broadcast. boolean inReminderExperiment = inReminderSchedulingExperiment(); // Some OEMs had changed the provider's EVENT_REMINDER broadcast to their own event, // which broke our unbundled app's reminders. So we added backup alarm scheduling to the // app, but we know we can turn it off if we ever receive the EVENT_REMINDER broadcast. boolean providerReminder = action.equals( android.provider.CalendarContract.ACTION_EVENT_REMINDER); if (providerReminder) { if (sReceivedProviderReminderBroadcast == null) { sReceivedProviderReminderBroadcast = Utils.getSharedPreference(this, PROVIDER_REMINDER_PREF_KEY, false); } if (!sReceivedProviderReminderBroadcast) { sReceivedProviderReminderBroadcast = true; Log.d(TAG, "Setting key " + PROVIDER_REMINDER_PREF_KEY + " to: true"); Utils.setSharedPreference(this, PROVIDER_REMINDER_PREF_KEY, true); } if (inReminderExperiment) { Log.d(TAG, "In reminder scheduling experiment, dropping action from " + "provider's EVENT_REMINDER broadcast."); return; } } if (providerReminder || action.equals(Intent.ACTION_PROVIDER_CHANGED) || action.equals(android.provider.CalendarContract.ACTION_EVENT_REMINDER) || action.equals(AlertReceiver.EVENT_REMINDER_APP_ACTION) || action.equals(Intent.ACTION_LOCALE_CHANGED)) { updateAlertNotification(this); } else if (action.equals(Intent.ACTION_BOOT_COMPLETED) Loading @@ -184,6 +228,30 @@ public class AlertService extends Service { } else { Log.w(TAG, "Invalid action: " + action); } // Schedule the alarm for the next upcoming reminder, if not done by the provider. if (sReceivedProviderReminderBroadcast == null || !sReceivedProviderReminderBroadcast || inReminderExperiment) { Log.d(TAG, "Scheduling next alarm with AlarmScheduler. " + "sEventReminderReceived: " + sReceivedProviderReminderBroadcast + ", inReminderExperiment: " + inReminderExperiment); AlarmScheduler.scheduleNextAlarm(this); } } /** * Temporary way to force some users through the alarm scheduling done in the app. */ private boolean inReminderSchedulingExperiment() { SharedPreferences prefs = GeneralPreferences.getSharedPreferences(this); if (!prefs.contains(REMINDER_EXPERIMENT_PREF_KEY)) { boolean inExperiment = new Random().nextBoolean(); Utils.setSharedPreference(this, REMINDER_EXPERIMENT_PREF_KEY, inExperiment); Log.d(TAG, "Setting key " + REMINDER_EXPERIMENT_PREF_KEY + " to: " + inExperiment); return inExperiment; } return prefs.getBoolean(REMINDER_EXPERIMENT_PREF_KEY, true); } static void dismissOldAlerts(Context context) { Loading