Commit 3a07a68d authored by Sara Ting's avatar Sara Ting
Browse files

Adding alert scheduling to app, to allow unbundled app's alerts to work on more devices.

Bug:7383861
Change-Id: I5dcffb8ac586966b21e938728be0393e6776f704
parent ddc1b123
......@@ -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);
}
......
......@@ -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);
......
/*
* 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);
}
}
......@@ -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]",
......
......@@ -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;
/**
......@@ -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;
......@@ -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)
......@@ -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) {
......
......@@ -98,7 +98,7 @@ public class AlertUtils {
* listeners when a reminder should be fired. The provider will keep
* scheduled reminders up to date but apps may use this to implement snooze
* functionality without modifying the reminders table. Scheduled alarms
* will generate an intent using {@link #ACTION_EVENT_REMINDER}.
* will generate an intent using AlertReceiver.EVENT_REMINDER_APP_ACTION.
*
* @param context A context for referencing system resources
* @param manager The AlarmManager to use or null
......@@ -121,7 +121,7 @@ public class AlertUtils {
private static void scheduleAlarmHelper(Context context, AlarmManagerInterface manager,
long alarmTime, boolean quietUpdate) {
int alarmType = AlarmManager.RTC_WAKEUP;
Intent intent = new Intent(CalendarContract.ACTION_EVENT_REMINDER);
Intent intent = new Intent(AlertReceiver.EVENT_REMINDER_APP_ACTION);
intent.setClass(context, AlertReceiver.class);
if (quietUpdate) {
alarmType = AlarmManager.RTC;
......
/*
* 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.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Instances;
import android.provider.CalendarContract.Reminders;
import android.test.AndroidTestCase;
import android.test.IsolatedContext;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@SmallTest
public class AlarmSchedulerTest extends AndroidTestCase {
private static final int BATCH_SIZE = 50;
private MockProvider mMockProvider;
private MockAlarmManager mMockAlarmManager;
private IsolatedContext mIsolatedContext;
/**
* A helper class to mock query results from the test data.
*/
private static class MockProvider extends MockContentProvider {
private ArrayList<EventInfo> mEvents = new ArrayList<EventInfo>();
private ArrayList<String> mExpectedRemindersQueries = new ArrayList<String>();
private int mCurrentReminderQueryIndex = 0;
/**
* Contains info for a test event and its reminder.
*/
private static class EventInfo {
long mEventId;
long mBegin;
boolean mAllDay;
int mReminderMinutes;
public EventInfo(long eventId, boolean allDay, long begin, int reminderMinutes) {
mEventId = eventId;
mAllDay = allDay;
mBegin = begin;
mReminderMinutes = reminderMinutes;
}
}
/**
* Adds event/reminder data for testing. These will always be returned in the mocked
* query result cursors.
*/
void addEventInfo(long eventId, boolean allDay, long begin, int reminderMinutes) {
mEvents.add(new EventInfo(eventId, allDay, begin, reminderMinutes));
}
private MatrixCursor getInstancesCursor() {
MatrixCursor instancesCursor = new MatrixCursor(AlarmScheduler.INSTANCES_PROJECTION);
int i = 0;
HashSet<Long> eventIds = new HashSet<Long>();
for (EventInfo event : mEvents) {
if (!eventIds.contains(event.mEventId)) {
Object[] ca = {
event.mEventId,
event.mBegin,
event.mAllDay ? 1 : 0,
};
instancesCursor.addRow(ca);
eventIds.add(event.mEventId);
}
}
return instancesCursor;
}
private MatrixCursor getRemindersCursor() {
MatrixCursor remindersCursor = new MatrixCursor(AlarmScheduler.REMINDERS_PROJECTION);
int i = 0;
for (EventInfo event : mEvents) {
Object[] ca = {
event.mEventId,
event.mReminderMinutes,
1,
};
remindersCursor.addRow(ca);
}
return remindersCursor;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (uri.toString().startsWith(Instances.CONTENT_URI.toString())) {
return getInstancesCursor();
} else if (Reminders.CONTENT_URI.equals(uri)) {
if (mExpectedRemindersQueries.size() > 0) {
if (mExpectedRemindersQueries.size() <= mCurrentReminderQueryIndex ||
!mExpectedRemindersQueries.get(mCurrentReminderQueryIndex).equals(
selection)) {
String msg = "Reminders query not as expected.\n";
msg += " Expected:";
msg += Arrays.deepToString(mExpectedRemindersQueries.toArray());
msg += "\n Got in position " + mCurrentReminderQueryIndex + ": ";
msg += selection;
fail(msg);
}
mCurrentReminderQueryIndex++;
}
return getRemindersCursor();
} else {
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
}
/**
* Optionally set up expectation for the reminders query selection.
*/
public void addExpectedRemindersQuery(String expectedRemindersQuery) {
this.mExpectedRemindersQueries.add(expectedRemindersQuery);
}
}
/**
* Expect an alarm for the specified time.
*/
private void expectAlarmAt(long millis) {
// AlarmScheduler adds a slight delay to the alarm so account for that here.
mMockAlarmManager.expectAlarmTime(AlarmManager.RTC_WAKEUP,
millis + AlarmScheduler.ALARM_DELAY_MS);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mMockProvider = new MockProvider();
mMockAlarmManager = new MockAlarmManager(mContext);
MockContentResolver mockResolver = new MockContentResolver();
mockResolver.addProvider(CalendarContract.AUTHORITY, mMockProvider);
mIsolatedContext = new IsolatedContext(mockResolver, mContext);
}
public void testNoEvents() {
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager,
BATCH_SIZE, System.currentTimeMillis());
assertFalse(mMockAlarmManager.isAlarmSet());
}
public void testNonAllDayEvent() {
// Set up mock test data.
long currentMillis = System.currentTimeMillis();
long startMillis = currentMillis + DateUtils.HOUR_IN_MILLIS;
int reminderMin = 10;
mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
expectAlarmAt(startMillis - reminderMin * DateUtils.MINUTE_IN_MILLIS);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testAllDayEvent() {
// Set up mock allday data.
long startMillisUtc = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
long startMillisLocal = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012,
Time.getCurrentTimezone());
long currentMillis = startMillisLocal - DateUtils.DAY_IN_MILLIS;
int reminderMin = 15;
mMockProvider.addEventInfo(1, true, startMillisUtc, reminderMin);
expectAlarmAt(startMillisLocal - reminderMin * DateUtils.MINUTE_IN_MILLIS);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testAllDayAndNonAllDayEvents() {
// Set up mock test data.
long startMillisUtc = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
long startMillisLocal = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012,
Time.getCurrentTimezone());
long currentMillis = startMillisLocal - DateUtils.DAY_IN_MILLIS;
mMockProvider.addEventInfo(1, true, startMillisUtc, 15);
mMockProvider.addEventInfo(1, false, startMillisLocal, 10);
expectAlarmAt(startMillisLocal - 15 * DateUtils.MINUTE_IN_MILLIS);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testExpiredReminder() {
// Set up mock test data.
long currentMillis = System.currentTimeMillis();
long startMillis = currentMillis + DateUtils.HOUR_IN_MILLIS;
int reminderMin = 61;
mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
// Invoke scheduleNextAlarm and verify no alarm was set.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertFalse(mMockAlarmManager.isAlarmSet());
}
public void testAlarmMax() {
// Set up mock test data for a reminder greater than 1 day in the future.
// This will be maxed out to 1 day out.
long currentMillis = System.currentTimeMillis();
long startMillis = currentMillis + DateUtils.DAY_IN_MILLIS * 3;
int reminderMin = (int) DateUtils.DAY_IN_MILLIS / (1000 * 60);
mMockProvider.addEventInfo(1, false, startMillis, reminderMin);
expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testMultipleEvents() {
// Set up multiple events where a later event time has an earlier reminder time.
long currentMillis = System.currentTimeMillis();
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS, 0);
mMockProvider.addEventInfo(2, false, currentMillis + DateUtils.MINUTE_IN_MILLIS * 60, 45);
mMockProvider.addEventInfo(3, false, currentMillis + DateUtils.MINUTE_IN_MILLIS * 30, 10);
// Expect event 2's reminder.
expectAlarmAt(currentMillis + DateUtils.MINUTE_IN_MILLIS * 15);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testRecurringEvents() {
long currentMillis = System.currentTimeMillis();
// Event in 3 days, with a 2 day reminder
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS * 3,
(int) DateUtils.DAY_IN_MILLIS * 2 / (1000 * 60) /* 2 day reminder */);
// Event for tomorrow, with a 2 day reminder
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS,
(int) DateUtils.DAY_IN_MILLIS * 2 / (1000 * 60) /* 2 day reminder */);
// Expect the reminder for the top event because the reminder time for the bottom
// one already passed.
expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testMultipleRemindersForEvent() {
// Set up mock test data.
long currentMillis = System.currentTimeMillis();
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS, 10);
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS, 20);
mMockProvider.addEventInfo(1, false, currentMillis + DateUtils.DAY_IN_MILLIS, 15);
// Expect earliest reminder.
expectAlarmAt(currentMillis + DateUtils.DAY_IN_MILLIS - DateUtils.MINUTE_IN_MILLIS * 20);
// Invoke scheduleNextAlarm and verify alarm was set at the expected time.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, BATCH_SIZE,
currentMillis);
assertTrue(mMockAlarmManager.isAlarmSet());
}
public void testLargeBatch() {
// Add enough events to require several batches.
long currentMillis = System.currentTimeMillis();
int batchSize = 5;
for (int i = 19; i > 0; i--) {
mMockProvider.addEventInfo(i, false, currentMillis + DateUtils.HOUR_IN_MILLIS * i,
10);
}
// Set up expectations for the batch queries.
expectAlarmAt(currentMillis + DateUtils.MINUTE_IN_MILLIS * 50);
mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (19,18,17,16,15)");
mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (14,13,12,11,10)");
mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (9,8,7,6,5)");
mMockProvider.addExpectedRemindersQuery("method=1 AND event_id IN (4,3,2,1)");
// Invoke scheduleNextAlarm and verify alarm and reminder query batches.
AlarmScheduler.scheduleNextAlarm(mIsolatedContext, mMockAlarmManager, batchSize,
currentMillis);
}
}
......@@ -385,28 +385,6 @@ public class AlertServiceTest extends AndroidTestCase {
}
}
private class MockAlarmManager implements AlarmManagerInterface {
private int expectedAlarmType = -1;
private long expectedAlarmTime = -1;
public void expectAlarmTime(int type, long millis) {
this.expectedAlarmType = type;
this.expectedAlarmTime = millis;
}
@Override
public void set(int actualAlarmType, long actualAlarmTime, PendingIntent operation) {
assertNotNull(operation);
if (expectedAlarmType != -1) {
assertEquals("Alarm type not expected.", expectedAlarmType, actualAlarmType);
assertEquals("Alarm time not expected. Expected:" + DateUtils.formatDateTime(
mContext, expectedAlarmTime, DateUtils.FORMAT_SHOW_TIME) + ", actual:"
+ DateUtils.formatDateTime(mContext, actualAlarmTime,
DateUtils.FORMAT_SHOW_TIME), expectedAlarmTime, actualAlarmTime);
}
}
}
// TODO
// Catch updates of new state, notify time, and received time
// Test ringer, vibrate,
......@@ -422,7 +400,7 @@ public class AlertServiceTest extends AndroidTestCase {
// Test no alert
long currentTime = 1000000;
AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(), prefs,
AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(mContext), prefs,
at.getAlertCursor(), currentTime, AlertService.MAX_NOTIFICATIONS);
ntm.validateNotificationsAndReset();
}
......@@ -431,7 +409,7 @@ public class AlertServiceTest extends AndroidTestCase {
@SmallTest
public void testGenerateAlerts_single() {
MockSharedPreferences prefs = new MockSharedPreferences();
MockAlarmManager alarmMgr = new MockAlarmManager();
MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
AlertsTable at = new AlertsTable();
NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
AlertService.MAX_NOTIFICATIONS);
......@@ -468,7 +446,7 @@ public class AlertServiceTest extends AndroidTestCase {
public void testGenerateAlerts_multiple() {
int maxNotifications = 10;
MockSharedPreferences prefs = new MockSharedPreferences();
MockAlarmManager alarmMgr = new MockAlarmManager();
MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
AlertsTable at = new AlertsTable();
NotificationTestManager ntm = new NotificationTestManager(at.mAlerts, maxNotifications);
......@@ -541,7 +519,7 @@ public class AlertServiceTest extends AndroidTestCase {
@SmallTest
public void testGenerateAlerts_maxAlerts() {
MockSharedPreferences prefs = new MockSharedPreferences();
MockAlarmManager alarmMgr = new MockAlarmManager();
MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
AlertsTable at = new AlertsTable();
// Current time - 5:00
......@@ -625,14 +603,14 @@ public class AlertServiceTest extends AndroidTestCase {
// If this does not result in a failure (MockSharedPreferences fails for duplicate
// queries), then test passes.
AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(), prefs,
AlertService.generateAlerts(mContext, ntm, new MockAlarmManager(mContext), prefs,
at.getAlertCursor(), currentTime, AlertService.MAX_NOTIFICATIONS);
}
public void testGenerateAlerts_refreshTime() {
AlertsTable at = new AlertsTable();
MockSharedPreferences prefs = new MockSharedPreferences();
MockAlarmManager alarmMgr = new MockAlarmManager();
MockAlarmManager alarmMgr = new MockAlarmManager(mContext);
NotificationTestManager ntm = new NotificationTestManager(at.mAlerts,
AlertService.MAX_NOTIFICATIONS);
......@@ -648,28 +626,30 @@ public class AlertServiceTest extends AndroidTestCase {
yesterday.set(System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS);
Time tomorrow = new Time();
tomorrow.set(System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS);
long allDayStart = createTimeInMillis(0, 0, 0, day, month, year, Time.TIMEZONE_UTC);
long allDayStart = Utils.createTimeInMillis(0, 0, 0, day, month, year, Time.TIMEZONE_UTC);
/* today 10am - 10:30am */
int id4 = at.addAlertRow(4, SCHEDULED, ACCEPTED, 0,
createTimeInMillis(0, 0, 10, day, month, year, Time.getCurrentTimezone()),
createTimeInMillis(0, 30, 10, day, month, year, Time.getCurrentTimezone()), 0);
Utils.createTimeInMillis(0, 0, 10, day, month, year, Time.getCurrentTimezone()),
Utils.createTimeInMillis(0, 30, 10, day, month, year, Time.getCurrentTimezone()),
0);
/* today 6am - 6am (0 duration event) */
int id3 = at.addAlertRow(3, SCHEDULED, ACCEPTED, 0,
createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()),
createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()), 0);
Utils.createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()),
Utils.createTimeInMillis(0, 0, 6, day, month, year, Time.getCurrentTimezone()), 0);
/* today allDay */
int id2 = at.addAlertRow(2, SCHEDULED, ACCEPTED, 1, allDayStart,
allDayStart + DateUtils.HOUR_IN_MILLIS * 24, 0);
/* yesterday 11pm - today 7am (multiday event) */
int id1 = at.addAlertRow(1, SCHEDULED, ACCEPTED, 0,
createTimeInMillis(0, 0, 23, yesterday.monthDay, yesterday.month, yesterday.year,
Time.getCurrentTimezone()),
createTimeInMillis(0, 0, 7, day, month, year, Time.getCurrentTimezone()), 0);
Utils.createTimeInMillis(0, 0, 23, yesterday.monthDay, yesterday.month,
yesterday.year, Time.getCurrentTimezone()),
Utils.createTimeInMillis(0, 0, 7, day, month, year, Time.getCurrentTimezone()), 0);
// Test at midnight - next refresh should be 15 min later (15 min into the all
// day event).
long currentTime = createTimeInMillis(0, 0, 0, day, month, year, Time.getCurrentTimezone());
long currentTime = Utils.createTimeInMillis(0, 0, 0, day, month, year,
Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 15 * DateUtils.MINUTE_IN_MILLIS);
ntm.expectTestNotification(4, id1, PRIORITY_HIGH);
ntm.expectTestNotification(3, id2, PRIORITY_HIGH);
......@@ -680,7 +660,8 @@ public class AlertServiceTest extends AndroidTestCase {
ntm.validateNotificationsAndReset();
// Test at 12:30am - next refresh should be 30 min later (1/4 into event 'id1').
currentTime = createTimeInMillis(0, 30, 0, day, month, year, Time.getCurrentTimezone());
currentTime = Utils.createTimeInMillis(0, 30, 0, day, month, year,
Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 30 * DateUtils.MINUTE_IN_MILLIS);
ntm.expectTestNotification(3, id1, PRIORITY_HIGH);
ntm.expectTestNotification(2, id3, PRIORITY_HIGH);
......@@ -691,7 +672,8 @@ public class AlertServiceTest extends AndroidTestCase {
ntm.validateNotificationsAndReset();
// Test at 5:55am - next refresh should be 20 min later (15 min after 'id3').
currentTime = createTimeInMillis(0, 55, 5, day, month, year, Time.getCurrentTimezone());
currentTime = Utils.createTimeInMillis(0, 55, 5, day, month, year,
Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 20 * DateUtils.MINUTE_IN_MILLIS);
ntm.expectTestNotification(2, id3, PRIORITY_HIGH);
ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
......@@ -702,7 +684,8 @@ public class AlertServiceTest extends AndroidTestCase {
ntm.validateNotificationsAndReset();
// Test at 10:14am - next refresh should be 1 min later (15 min into event 'id4').
currentTime = createTimeInMillis(0, 14, 10, day, month, year, Time.getCurrentTimezone());
currentTime = Utils.createTimeInMillis(0, 14, 10, day, month, year,
Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, currentTime + 1 * DateUtils.MINUTE_IN_MILLIS);
ntm.expectTestNotification(1, id4, PRIORITY_HIGH);
ntm.expectTestNotification(2, id2, PRIORITY_DEFAULT);
......@@ -713,9 +696,10 @@ public class AlertServiceTest extends AndroidTestCase {
ntm.validateNotificationsAndReset();
// Test at 10:15am - next refresh should be tomorrow midnight (end of all day event 'id2').
currentTime = createTimeInMillis(0, 15, 10, day, month, year, Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, createTimeInMillis(0, 0, 23, tomorrow.monthDay,
tomorrow.month, tomorrow.year, Time.getCurrentTimezone()));
currentTime = Utils.createTimeInMillis(0, 15, 10, day, month, year,
Time.getCurrentTimezone());
alarmMgr.expectAlarmTime(AlarmManager.RTC, Utils.createTimeInMillis(0, 0, 23,
tomorrow.monthDay, tomorrow.month, tomorrow.year, Time.getCurrentTimezone()));
ntm.expectTestNotification(1, id2, PRIORITY_DEFAULT);
ntm.expectTestNotification(AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID,
new int[] {id4, id3, id1}, PRIORITY_MIN);
......@@ -730,18 +714,10 @@ public class AlertServiceTest extends AndroidTestCase {
}
private static long createTimeInMillis(int hour, int minute) {
return createTimeInMillis(0 /* second */, minute, hour, 1 /* day */, 1 /* month */,
return Utils.createTimeInMillis(0 /* second */, minute, hour, 1 /* day */, 1 /* month */,
2012 /* year */, Time.getCurrentTimezone());
}
private static long createTimeInMillis(int second, int minute, int hour, int monthDay,
int month, int year, String timezone) {
Time t = new Time(timezone);
t.set(second, minute, hour, monthDay, month, year);
t.normalize(false);
return t.toMillis(false);
}
@SmallTest
public void testProcessQuery_skipDeclinedDismissed() {
int declinedEventId = 1;
......@@ -864,8 +840,8 @@ public class AlertServiceTest extends AndroidTestCase {
@SmallTest
public void testProcessQuery_recurringAllDayEvent() {
int eventId = 1;
long day1 = createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
long day2 = createTimeInMillis(0, 0, 0, 2, 5, 2012, Time.TIMEZONE_UTC);
long day1 = Utils.createTimeInMillis(0, 0, 0, 1, 5, 2012, Time.TIMEZONE_UTC);
long day2 = Utils.createTimeInMillis(0, 0, 0, 2, 5, 2012, Time.TIMEZONE_UTC);
ArrayList<NotificationInfo> highPriority = new ArrayList<NotificationInfo>();
ArrayList<NotificationInfo> mediumPriority = new ArrayList<NotificationInfo>();
......@@ -890,7 +866,7 @@ public class AlertServiceTest extends AndroidTestCase {
// Increment time just past the earlier event (to 12:10am). The earlier one should
// be chosen.
highPriority.clear();
currentTime = createTimeInMillis(0, 10, 0, 1, 5, 2012, Time.getCurrentTimezone());
currentTime = Utils.createTimeInMillis(0, 10, 0, 1, 5, 2012, Time.getCurrentTimezone());
AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
mediumPriority, lowPriority);
assertEquals(0, lowPriority.size());
......@@ -901,7 +877,7 @@ public class AlertServiceTest extends AndroidTestCase {
// Increment time to 15 min past the earlier event: the later one should be chosen.
highPriority.clear();
currentTime = createTimeInMillis(0, 15, 0, 1, 5, 2012, Time.getCurrentTimezone());
currentTime = Utils.createTimeInMillis(0, 15, 0, 1, 5, 2012, Time.getCurrentTimezone());
AlertService.processQuery(at.getAlertCursor(), mContext, currentTime, highPriority,
mediumPriority, lowPriority);
assertEquals(0, lowPriority.size());
......
/*
* 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.PendingIntent;
import android.content.Context;
import android.text.format.DateUtils;
import junit.framework.Assert;
public class MockAlarmManager implements AlarmManagerInterface {
private Context context;
private int expectedAlarmType = -1;
private long expectedAlarmTime = -1;
private boolean alarmSet = false;
MockAlarmManager(Context context) {
this.context = context;
}
public void expectAlarmTime(int type, long millis) {
this.expectedAlarmType = type;
this.expectedAlarmTime = millis;
}
@Override
public void set(int actualAlarmType, long actualAlarmTime, PendingIntent operation) {
Assert.assertNotNull(operation);
alarmSet = true;
if (expectedAlarmType != -1) {
Assert.assertEquals("Alarm type not expected.", expectedAlarmType, actualAlarmType);
Assert.assertEquals("Alarm time not expected. Expected:" + DateUtils.formatDateTime(
context, expectedAlarmTime, DateUtils.FORMAT_SHOW_TIME) + ", actual:"
+ DateUtils.formatDateTime(context, actualAlarmTime,
DateUtils.FORMAT_SHOW_TIME), expectedAlarmTime, actualAlarmTime);
}
}
/**
* Returns whether set() was invoked.
*/
public boolean isAlarmSet() {
return alarmSet;
}
}
/*
* 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.text.format.Time;
class Utils {
public static long createTimeInMillis(int second, int minute, int hour, int monthDay,
int month, int year, String timezone) {
Time t = new Time(timezone);
t.set(second, minute, hour, monthDay, month, year);
t.normalize(false);
return t.toMillis(false);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment