Fix calendar reminder notification is not shown in idle state
M introduce "doze" which prevents alarm goes off when devices is in idle
state.
The solution is to use setExactAndAllowWhileIdle, however,
there is a restriction that we can only have one such an alarm within 15
mins during dozing.
Before explaining the changes, here is the brief introduction how reminder
notification works.
CalendarProvider send EVENT_REMINDER broadcast to notify calendar app
to show notification.
It sets two alarms, one to send this broadcast and set one another to
schedule the next coming reminder.
The scheduler alarm goes off one minute after the reminder alarm.
Here are the changes:
1.Due to the 15 min restriction, the current architecture is not working.
This commit removes the scheduler alarm.
CalendarProvider now listens for the EVENT_REMINDER broadcast and
schedule the next reminder immediately.
2. When there is no reminder within 1 day, CalendarProvider will setup
an alarm on the next day to start the scheduler again.
This alarm needs to goes off in idle state. But due to the 15 mins
restriction, now the time is set to 15 mins earlier.
3. Also remove SCHEDULE broadcast which is basically same as the
ACTION_CHECK_NEXT_ALARM broadcast.
4. ag/720759 make sure the reminder alarm goes off during dozing.
Please notice that even with this commit, we can't have more than
one reminder notification within 15 minutes due to the restriction
mentioned above.
Bug: 22182280
Change-Id: I950dab5595a98a26370f2cd8decf0b32d9363591
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8c80116..0b95490 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -101,11 +101,16 @@
</intent-filter>
</receiver>
- <receiver android:name="CalendarProviderBroadcastReceiver">
+ <receiver android:name="CalendarProviderBroadcastReceiver"
+ android:exported="false">
<intent-filter>
- <action android:name="com.android.providers.calendar.intent.CalendarProvider2" />
+ <action android:name="com.android.providers.calendar.intent.CalendarProvider2"/>
<category android:name="com.android.providers.calendar"/>
</intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.EVENT_REMINDER"/>
+ <data android:scheme="content" />
+ </intent-filter>
</receiver>
<service android:name="CalendarProviderIntentService"/>
diff --git a/src/com/android/providers/calendar/CalendarAlarmManager.java b/src/com/android/providers/calendar/CalendarAlarmManager.java
index 08269eb..ac14713 100644
--- a/src/com/android/providers/calendar/CalendarAlarmManager.java
+++ b/src/com/android/providers/calendar/CalendarAlarmManager.java
@@ -28,6 +28,7 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
+import android.os.Build;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
@@ -57,12 +58,22 @@
// TODO: use a service to schedule alarms rather than private URI
/* package */static final String SCHEDULE_ALARM_PATH = "schedule_alarms";
/* package */static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove";
- private static final String REMOVE_ALARM_VALUE = "removeAlarms";
+ /* package */static final String KEY_REMOVE_ALARMS = "removeAlarms";
/* package */static final Uri SCHEDULE_ALARM_REMOVE_URI = Uri.withAppendedPath(
CalendarContract.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH);
/* package */static final Uri SCHEDULE_ALARM_URI = Uri.withAppendedPath(
CalendarContract.CONTENT_URI, SCHEDULE_ALARM_PATH);
+ /**
+ * If no alarms are scheduled in the next 24h, check for future alarms again after this period
+ * has passed. Scheduling the check 15 minutes earlier than 24h to prevent the scheduler alarm
+ * from using up the alarms quota for reminders during dozing.
+ *
+ * @see AlarmManager#setExactAndAllowWhileIdle
+ */
+ private static final long ALARM_CHECK_WHEN_NO_ALARM_IS_SCHEDULED_INTERVAL_MILLIS =
+ DateUtils.DAY_IN_MILLIS - (15 * DateUtils.MINUTE_IN_MILLIS);
+
static final String INVALID_CALENDARALERTS_SELECTOR =
"_id IN (SELECT ca." + CalendarAlerts._ID + " FROM "
+ Tables.CALENDAR_ALERTS + " AS ca"
@@ -147,7 +158,21 @@
mAlarmLock = new Object();
}
- void scheduleNextAlarm(boolean removeAlarms) {
+ private Intent getCheckNextAlarmIntent(boolean removeAlarms) {
+ Intent intent = new Intent(CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM);
+ intent.setClass(mContext, CalendarProviderBroadcastReceiver.class);
+ intent.putExtra(KEY_REMOVE_ALARMS, removeAlarms);
+ return intent;
+ }
+
+ /**
+ * Called by CalendarProvider to check the next alarm. A small delay is added before the real
+ * checking happens in order to batch the requests.
+ *
+ * @param removeAlarms Remove scheduled alarms or not. See @{link
+ * #removeScheduledAlarmsLocked} for details.
+ */
+ void checkNextAlarm(boolean removeAlarms) {
// We must always run the following when 'removeAlarms' is true. Previously it
// was possible to have a race condition on startup between TIME_CHANGED and
// BOOT_COMPLETED broadcast actions. This resulted in alarms being
@@ -158,8 +183,7 @@
if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
Log.d(CalendarProvider2.TAG, "Scheduling check of next Alarm");
}
- Intent intent = new Intent(ACTION_CHECK_NEXT_ALARM);
- intent.putExtra(REMOVE_ALARM_VALUE, removeAlarms);
+ Intent intent = getCheckNextAlarmIntent(removeAlarms);
PendingIntent pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
PendingIntent.FLAG_NO_CREATE);
if (pending != null) {
@@ -169,12 +193,42 @@
pending = PendingIntent.getBroadcast(mContext, 0 /* ignored */, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
- // Trigger the check in 5s from now
+ // Trigger the check in 5s from now, so that we can have batch processing.
long triggerAtTime = SystemClock.elapsedRealtime() + ALARM_CHECK_DELAY_MILLIS;
- set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending);
+ // Given to the short delay, we just use setExact here.
+ setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending);
}
}
+ /**
+ * Similar to {@link #checkNextAlarm}, but schedule the checking at specific {@code
+ * triggerTime}. In general, we do not need an alarm for scheduling. Instead we set the next
+ * alarm check immediately when a reminder is shown. The only use case for this
+ * is to schedule the next alarm check when there is no reminder within 1 day.
+ *
+ * @param triggerTimeMillis Time to run the next alarm check, in milliseconds.
+ */
+ void scheduleNextAlarmCheck(long triggerTimeMillis) {
+ Intent intent = getCheckNextAlarmIntent(false /* removeAlarms*/);
+ PendingIntent pending = PendingIntent.getBroadcast(
+ mContext, 0, intent, PendingIntent.FLAG_NO_CREATE);
+ if (pending != null) {
+ // Cancel any previous alarms that do the same thing.
+ cancel(pending);
+ }
+ pending = PendingIntent.getBroadcast(
+ mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
+ Time time = new Time();
+ time.set(triggerTimeMillis);
+ String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+ Log.d(CalendarProvider2.TAG,
+ "scheduleNextAlarmCheck at: " + triggerTimeMillis + timeStr);
+ }
+ setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pending);
+ }
+
PowerManager.WakeLock getScheduleNextAlarmWakeLock() {
return mScheduleNextAlarmWakeLock;
}
@@ -202,7 +256,8 @@
* This method runs in a background thread and schedules an alarm for the
* next calendar event, if necessary.
*
- * @param db TODO
+ * @param removeAlarms
+ * @param cp2
*/
void runScheduleNextAlarm(boolean removeAlarms, CalendarProvider2 cp2) {
SQLiteDatabase db = cp2.mDb;
@@ -224,28 +279,6 @@
}
}
- void scheduleNextAlarmCheck(long triggerTime) {
- Intent intent = new Intent(CalendarReceiver.SCHEDULE);
- intent.setClass(mContext, CalendarReceiver.class);
- PendingIntent pending = PendingIntent.getBroadcast(
- mContext, 0, intent, PendingIntent.FLAG_NO_CREATE);
- if (pending != null) {
- // Cancel any previous alarms that do the same thing.
- cancel(pending);
- }
- pending = PendingIntent.getBroadcast(
- mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
- Time time = new Time();
- time.set(triggerTime);
- String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(CalendarProvider2.TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
- }
-
- set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
- }
-
/**
* This method looks at the 24-hour window from now for any events that it
* needs to schedule. This method runs within a database transaction. It
@@ -468,15 +501,12 @@
scheduleAlarm(currentMillis);
}
- // If we scheduled an event alarm, then schedule the next alarm check
- // for one minute past that alarm. Otherwise, if there were no
- // event alarms scheduled, then check again in 24 hours. If a new
+ // No event alarm is scheduled, check again in 24 hours. If a new
// event is inserted before the next alarm check, then this method
// will be run again when the new event is inserted.
- if (nextAlarmTime != Long.MAX_VALUE) {
- scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS);
- } else {
- scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS);
+ if (nextAlarmTime == Long.MAX_VALUE) {
+ scheduleNextAlarmCheck(
+ currentMillis + ALARM_CHECK_WHEN_NO_ALARM_IS_SCHEDULED_INTERVAL_MILLIS);
}
}
@@ -500,15 +530,23 @@
+ CalendarAlerts.STATE_SCHEDULED, null /* whereArgs */);
}
- public void set(int type, long triggerAtTime, PendingIntent operation) {
+ public void setExact(int type, long triggerAtTime, PendingIntent operation) {
mAlarmManager.setExact(type, triggerAtTime, operation);
}
+ public void setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
+ mAlarmManager.setExactAndAllowWhileIdle(type, triggerAtTime, operation);
+ }
+
public void cancel(PendingIntent operation) {
mAlarmManager.cancel(operation);
}
public void scheduleAlarm(long alarmTime) {
+ // Debug log for investigating dozing related bugs, remove it once we confirm it is stable.
+ if (Build.IS_DEBUGGABLE) {
+ Log.d(TAG, "schedule reminder alarm fired at " + alarmTime);
+ }
CalendarContract.CalendarAlerts.scheduleAlarm(mContext, mAlarmManager, alarmTime);
}
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 6e6db3b..c323bfe 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -472,13 +472,13 @@
}
if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
updateTimezoneDependentFields();
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
// Try to clean up if things were screwy due to a full disk
updateTimezoneDependentFields();
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
} else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
}
}
};
@@ -2329,7 +2329,7 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "insertInternal() changing reminder");
}
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
break;
}
case CALENDAR_ALERTS: {
@@ -3113,7 +3113,7 @@
long id = cursor.getLong(0);
result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
}
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
sendUpdateNotification(callerIsSyncAdapter);
} finally {
cursor.close();
@@ -3311,7 +3311,7 @@
}
if (!isBatch) {
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
sendUpdateNotification(callerIsSyncAdapter);
}
return result;
@@ -3918,14 +3918,14 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updateInternal() changing event");
}
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
}
sendUpdateNotification(id, callerIsSyncAdapter);
}
} else {
deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
sendUpdateNotification(callerIsSyncAdapter);
}
}
@@ -4038,7 +4038,7 @@
// scheduleNextAlarmLocked will remove any alarms for
// non-visible events anyways. removeScheduledAlarmsLocked
// does not actually have the effect we want
- mCalendarAlarm.scheduleNextAlarm(false);
+ mCalendarAlarm.checkNextAlarm(false);
}
// update the widget
sendUpdateNotification(callerIsSyncAdapter);
@@ -4114,22 +4114,15 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updateInternal() changing reminder");
}
- mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
+ mCalendarAlarm.checkNextAlarm(false /* do not remove alarms */);
return count;
}
case EXTENDED_PROPERTIES_ID:
return updateEventRelatedTable(uri, Tables.EXTENDED_PROPERTIES, true, values,
null, null, callerIsSyncAdapter);
-
- // TODO: replace the SCHEDULE_ALARM private URIs with a
- // service
- case SCHEDULE_ALARM: {
- mCalendarAlarm.scheduleNextAlarm(false);
- return 0;
- }
case SCHEDULE_ALARM_REMOVE: {
- mCalendarAlarm.scheduleNextAlarm(true);
+ mCalendarAlarm.checkNextAlarm(true);
return 0;
}
@@ -4630,7 +4623,6 @@
private static final int EVENT_ENTITIES = 18;
private static final int EVENT_ENTITIES_ID = 19;
private static final int EVENT_DAYS = 20;
- private static final int SCHEDULE_ALARM = 21;
private static final int SCHEDULE_ALARM_REMOVE = 22;
private static final int TIME = 23;
private static final int CALENDAR_ENTITIES = 24;
@@ -4683,8 +4675,6 @@
CALENDAR_ALERTS_BY_INSTANCE);
sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate", SYNCSTATE);
sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
- sUriMatcher.addURI(CalendarContract.AUTHORITY, CalendarAlarmManager.SCHEDULE_ALARM_PATH,
- SCHEDULE_ALARM);
sUriMatcher.addURI(CalendarContract.AUTHORITY,
CalendarAlarmManager.SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE);
sUriMatcher.addURI(CalendarContract.AUTHORITY, "time/#", TIME);
diff --git a/src/com/android/providers/calendar/CalendarProviderBroadcastReceiver.java b/src/com/android/providers/calendar/CalendarProviderBroadcastReceiver.java
index 3c50eb2..af00bfa 100644
--- a/src/com/android/providers/calendar/CalendarProviderBroadcastReceiver.java
+++ b/src/com/android/providers/calendar/CalendarProviderBroadcastReceiver.java
@@ -20,13 +20,16 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.provider.CalendarContract;
import android.util.Log;
public class CalendarProviderBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (!CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM.equals(intent.getAction())) {
+ String action = intent.getAction();
+ if (!CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM.equals(action)
+ && !CalendarContract.ACTION_EVENT_REMINDER.equals(action)) {
setResultCode(Activity.RESULT_CANCELED);
return;
}
diff --git a/src/com/android/providers/calendar/CalendarProviderIntentService.java b/src/com/android/providers/calendar/CalendarProviderIntentService.java
index 39dfa7a..dcaf264 100644
--- a/src/com/android/providers/calendar/CalendarProviderIntentService.java
+++ b/src/com/android/providers/calendar/CalendarProviderIntentService.java
@@ -17,12 +17,12 @@
import android.app.IntentService;
import android.content.Intent;
+import android.provider.CalendarContract;
import android.util.Log;
public class CalendarProviderIntentService extends IntentService {
private static final String TAG = CalendarProvider2.TAG;
- private static final String REMOVE_ALARMS_VALUE = "removeAlarms";
public CalendarProviderIntentService() {
super("CalendarProviderIntentService");
@@ -33,18 +33,19 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Received Intent: " + intent);
}
- final String action = intent.getAction();
- if (!CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM.equals(action)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Invalid Intent action: " + action);
- }
- return;
- }
final CalendarProvider2 provider = CalendarProvider2.getInstance();
- // Schedule the next alarm
- final boolean removeAlarms = intent.getBooleanExtra(REMOVE_ALARMS_VALUE, false);
- provider.getOrCreateCalendarAlarmManager().runScheduleNextAlarm(removeAlarms, provider);
- // Release the wake lock that was set in the Broadcast Receiver
- provider.getOrCreateCalendarAlarmManager().releaseScheduleNextAlarmWakeLock();
+ final String action = intent.getAction();
+ if (CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM.equals(action)
+ || CalendarContract.ACTION_EVENT_REMINDER.equals(action)) {
+ // Schedule the next alarm. Please be noted that for ACTION_EVENT_REMINDER broadcast,
+ // we never remove scheduled alarms.
+ final boolean removeAlarms = intent
+ .getBooleanExtra(CalendarAlarmManager.KEY_REMOVE_ALARMS, false);
+ provider.getOrCreateCalendarAlarmManager().runScheduleNextAlarm(removeAlarms, provider);
+ // Release the wake lock that was set in the Broadcast Receiver
+ provider.getOrCreateCalendarAlarmManager().releaseScheduleNextAlarmWakeLock();
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Invalid Intent action: " + action);
+ }
}
}
diff --git a/src/com/android/providers/calendar/CalendarReceiver.java b/src/com/android/providers/calendar/CalendarReceiver.java
index 5f1acd3..d1d8a5a 100644
--- a/src/com/android/providers/calendar/CalendarReceiver.java
+++ b/src/com/android/providers/calendar/CalendarReceiver.java
@@ -35,7 +35,6 @@
*/
public class CalendarReceiver extends BroadcastReceiver {
private static final String TAG = "CalendarReceiver";
- static final String SCHEDULE = "com.android.providers.calendar.SCHEDULE_ALARM";
private final ExecutorService executor = Executors.newCachedThreadPool();
private PowerManager.WakeLock mWakeLock;
@@ -55,10 +54,7 @@
executor.submit(new Runnable() {
@Override
public void run() {
- if (action.equals(SCHEDULE)) {
- cr.update(CalendarAlarmManager.SCHEDULE_ALARM_URI, null /* values */,
- null /* where */, null /* selectionArgs */);
- } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
removeScheduledAlarms(cr);
}
result.finish();
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
index e4146dc..4d07bc1 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
@@ -58,7 +58,12 @@
}
@Override
- public void set(int type, long triggerAtTime, PendingIntent operation) {
+ public void setExact(int type, long triggerAtTime, PendingIntent operation) {
+ }
+
+ @Override
+ public void setExactAndAllowWhileIdle(int type, long triggerAtTime,
+ PendingIntent operation) {
}
@Override