Auto-update Events.hasAlarm
The "hasAlarm" column in the Events table is supposed to be read-only
for applications, updated automatically by the provider as reminders
are added and deleted. This wasn't implemented (the Calendar app was
doing it manually).
Bug 5424486
Change-Id: Id4d167fe081b77fbd514b9a700359fd84d9e43e8
diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
index a1db32b..586515c 100644
--- a/src/com/android/providers/calendar/CalendarDatabaseHelper.java
+++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
@@ -2951,6 +2951,14 @@
return SCHEMA_HTTPS + url.substring(SCHEMA_HTTP.length());
}
+ /**
+ * Duplicates an event and its associated tables (Attendees, Reminders, ExtendedProperties).
+ * <p>
+ * Does not create a duplicate if the Calendar's "canPartiallyUpdate" is 0 or the Event's
+ * "dirty" is 1 (so we don't create more than one duplicate).
+ *
+ * @param id The _id of the event to duplicate.
+ */
protected void duplicateEvent(final long id) {
final SQLiteDatabase db = getWritableDatabase();
final long canPartiallyUpdate = DatabaseUtils.longForQuery(db, "SELECT "
@@ -2985,6 +2993,14 @@
copyEventRelatedTables(db, newId, id);
}
+ /**
+ * Makes a copy of the Attendees, Reminders, and ExtendedProperties rows associated with
+ * a specific event.
+ *
+ * @param db The database.
+ * @param newId The ID of the new event.
+ * @param id The ID of the old event.
+ */
static void copyEventRelatedTables(SQLiteDatabase db, long newId, long id) {
db.execSQL("INSERT INTO " + Tables.REMINDERS
+ " ( " + CalendarContract.Reminders.EVENT_ID + ", "
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 13ac300..dfa0056 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -68,6 +68,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
@@ -108,9 +109,13 @@
private static final int EVENTS_ORIGINAL_ID_INDEX = 3;
private static final int EVENTS_ORIGINAL_SYNC_ID_INDEX = 4;
+ // many tables have _id and event_id; pick a representative version to use as our generic
+ private static final String GENERIC_ID = Attendees._ID;
+ private static final String GENERIC_EVENT_ID = Attendees.EVENT_ID;
+
private static final String[] ID_PROJECTION = new String[] {
- Attendees._ID,
- Attendees.EVENT_ID, // Assume these are the same for each table
+ GENERIC_ID,
+ GENERIC_EVENT_ID,
};
private static final int ID_INDEX = 0;
private static final int EVENT_ID_INDEX = 1;
@@ -166,10 +171,6 @@
" SET " + Events.DIRTY + "=1" +
" WHERE " + Events._ID + "=?";
- // many tables have _id and event_id; pick a representative version to use as our generic
- private static final String GENERIC_ID = Events._ID;
- private static final String GENERIC_EVENT_ID = Attendees.EVENT_ID;
-
protected static final String SQL_WHERE_ID = GENERIC_ID + "=?";
private static final String SQL_WHERE_EVENT_ID = GENERIC_EVENT_ID + "=?";
private static final String SQL_WHERE_ORIGINAL_ID = Events.ORIGINAL_ID + "=?";
@@ -1561,7 +1562,7 @@
}
EventRecurrence excepRecurrence = new EventRecurrence();
- excepRecurrence.parse(origRrule); // TODO: add/use a copy constructor to EventRecurrence
+ excepRecurrence.parse(origRrule); // TODO: add/use a copy constructor to EventRecurrence
excepRecurrence.count -= recurrences.length;
values.put(Events.RRULE, excepRecurrence.toString());
@@ -2042,6 +2043,7 @@
"allDay is true but sec, min, hour were not 0.");
}
}
+ updatedValues.remove(Events.HAS_ALARM); // should not be set by caller
// Insert the row
id = mDbHelper.eventsInsert(updatedValues);
if (id != -1) {
@@ -2130,23 +2132,28 @@
updateEventAttendeeStatus(mDb, values);
break;
case REMINDERS:
- if (!values.containsKey(Reminders.EVENT_ID)) {
+ {
+ Long eventIdObj = values.getAsLong(Reminders.EVENT_ID);
+ if (eventIdObj == null) {
throw new IllegalArgumentException("Reminders values must "
- + "contain an event_id");
+ + "contain a numeric event_id");
}
if (!callerIsSyncAdapter) {
- final Long eventId = values.getAsLong(Reminders.EVENT_ID);
- mDbHelper.duplicateEvent(eventId);
- setEventDirty(eventId);
+ mDbHelper.duplicateEvent(eventIdObj);
+ setEventDirty(eventIdObj);
}
id = mDbHelper.remindersInsert(values);
+ // We know this event has at least one reminder, so make sure "hasAlarm" is 1.
+ setHasAlarm(eventIdObj, 1);
+
// Schedule another event alarm, if necessary
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "insertInternal() changing reminder");
}
mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
break;
+ }
case CALENDAR_ALERTS:
if (!values.containsKey(CalendarAlerts.EVENT_ID)) {
throw new IllegalArgumentException("CalendarAlerts values must "
@@ -2587,6 +2594,23 @@
}
/**
+ * Set the "hasAlarm" column in the database.
+ *
+ * @param eventId The _id of the Event to update.
+ * @param val The value to set it to (0 or 1).
+ */
+ private void setHasAlarm(long eventId, int val) {
+ ContentValues values = new ContentValues();
+ values.put(Events.HAS_ALARM, val);
+ int count = mDb.update(Tables.EVENTS, values, SQL_WHERE_ID,
+ new String[] { String.valueOf(eventId) });
+ if (count != 1) {
+ Log.w(TAG, "setHasAlarm on event " + eventId + " updated " + count +
+ " rows (expected 1)");
+ }
+ }
+
+ /**
* Calculates the "last date" of the event. For a regular event this is the start time
* plus the duration. For a recurring event this is the start date of the last event in
* the recurrence, plus the duration. The event recurs forever, this returns -1. If
@@ -2824,7 +2848,8 @@
if (callerIsSyncAdapter) {
return mDb.delete(Tables.ATTENDEES, selection, selectionArgs);
} else {
- return deleteFromTable(Tables.ATTENDEES, uri, selection, selectionArgs);
+ return deleteFromEventRelatedTable(Tables.ATTENDEES, uri, selection,
+ selectionArgs);
}
}
case ATTENDEES_ID:
@@ -2834,35 +2859,25 @@
return mDb.delete(Tables.ATTENDEES, SQL_WHERE_ID,
new String[] {String.valueOf(id)});
} else {
- return deleteFromTable(Tables.ATTENDEES, uri, null /* selection */,
+ return deleteFromEventRelatedTable(Tables.ATTENDEES, uri, null /* selection */,
null /* selectionArgs */);
}
}
case REMINDERS:
{
- if (callerIsSyncAdapter) {
- return mDb.delete(Tables.REMINDERS, selection, selectionArgs);
- } else {
- return deleteFromTable(Tables.REMINDERS, uri, selection, selectionArgs);
- }
+ return deleteReminders(uri, false, selection, selectionArgs, callerIsSyncAdapter);
}
case REMINDERS_ID:
{
- if (callerIsSyncAdapter) {
- long id = ContentUris.parseId(uri);
- return mDb.delete(Tables.REMINDERS, SQL_WHERE_ID,
- new String[] {String.valueOf(id)});
- } else {
- return deleteFromTable(Tables.REMINDERS, uri, null /* selection */,
- null /* selectionArgs */);
- }
+ return deleteReminders(uri, true, null /*selection*/, null /*selectionArgs*/,
+ callerIsSyncAdapter);
}
case EXTENDED_PROPERTIES:
{
if (callerIsSyncAdapter) {
return mDb.delete(Tables.EXTENDED_PROPERTIES, selection, selectionArgs);
} else {
- return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, selection,
+ return deleteFromEventRelatedTable(Tables.EXTENDED_PROPERTIES, uri, selection,
selectionArgs);
}
}
@@ -2873,8 +2888,8 @@
return mDb.delete(Tables.EXTENDED_PROPERTIES, SQL_WHERE_ID,
new String[] {String.valueOf(id)});
} else {
- return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, null /* selection */,
- null /* selectionArgs */);
+ return deleteFromEventRelatedTable(Tables.EXTENDED_PROPERTIES, uri,
+ null /* selection */, null /* selectionArgs */);
}
}
case CALENDAR_ALERTS:
@@ -2882,7 +2897,8 @@
if (callerIsSyncAdapter) {
return mDb.delete(Tables.CALENDAR_ALERTS, selection, selectionArgs);
} else {
- return deleteFromTable(Tables.CALENDAR_ALERTS, uri, selection, selectionArgs);
+ return deleteFromEventRelatedTable(Tables.CALENDAR_ALERTS, uri, selection,
+ selectionArgs);
}
}
case CALENDAR_ALERTS_ID:
@@ -3007,27 +3023,49 @@
}
/**
- * Delete rows from a table and mark corresponding events as dirty.
+ * Delete rows from an Event-related table (e.g. Attendees) and mark corresponding events
+ * as dirty.
+ *
* @param table The table to delete from
* @param uri The URI specifying the rows
* @param selection for the query
* @param selectionArgs for the query
*/
- private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) {
- // Note that the query will return data according to the access restrictions,
- // so we don't need to worry about deleting data we don't have permission to read.
- final Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
- final ContentValues values = new ContentValues();
- values.put(Events.DIRTY, "1");
+ private int deleteFromEventRelatedTable(String table, Uri uri, String selection,
+ String[] selectionArgs) {
+ if (table.equals(Tables.EVENTS)) {
+ throw new IllegalArgumentException("Don't delete Events with this method "
+ + "(use deleteEventInternal)");
+ }
+
+ ContentValues dirtyValues = new ContentValues();
+ dirtyValues.put(Events.DIRTY, "1");
+
+ /*
+ * Re-issue the delete URI as a query. Note that, if this is a by-ID request, the ID
+ * will be in the URI, not selection/selectionArgs.
+ *
+ * Note that the query will return data according to the access restrictions,
+ * so we don't need to worry about deleting data we don't have permission to read.
+ */
+ Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, GENERIC_EVENT_ID);
int count = 0;
try {
- while(c.moveToNext()) {
- final long id = c.getLong(ID_INDEX);
- final long event_id = c.getLong(EVENT_ID_INDEX);
- mDbHelper.duplicateEvent(event_id);
+ long prevEventId = -1;
+ while (c.moveToNext()) {
+ long id = c.getLong(ID_INDEX);
+ long eventId = c.getLong(EVENT_ID_INDEX);
+ // Duplicate the event. As a minor optimization, don't try to duplicate an
+ // event that we just duplicated on the previous iteration.
+ if (eventId != prevEventId) {
+ mDbHelper.duplicateEvent(eventId);
+ prevEventId = eventId;
+ }
mDb.delete(table, SQL_WHERE_ID, new String[]{String.valueOf(id)});
- mDb.update(Tables.EVENTS, values, SQL_WHERE_ID,
- new String[] {String.valueOf(event_id)});
+ if (eventId != prevEventId) {
+ mDb.update(Tables.EVENTS, dirtyValues, SQL_WHERE_ID,
+ new String[] { String.valueOf(eventId)} );
+ }
count++;
}
} finally {
@@ -3037,6 +3075,103 @@
}
/**
+ * Deletes rows from the Reminders table and marks the corresponding events as dirty.
+ * Ensures the hasAlarm column in the Event is updated.
+ *
+ * @return The number of rows deleted.
+ */
+ private int deleteReminders(Uri uri, boolean byId, String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter) {
+ /*
+ * If this is a by-ID URI, make sure we have a good ID. Also, confirm that the
+ * selection is null, since we will be ignoring it.
+ */
+ long rowId = -1;
+ if (byId) {
+ if (!TextUtils.isEmpty(selection)) {
+ throw new UnsupportedOperationException("Selection not allowed for " + uri);
+ }
+ rowId = ContentUris.parseId(uri);
+ if (rowId < 0) {
+ throw new IllegalArgumentException("ID expected but not found in " + uri);
+ }
+ }
+
+ /*
+ * Determine the set of events affected by this operation. There can be multiple
+ * reminders with the same event_id, so to avoid beating up the database with "how many
+ * reminders are left" and "duplicate this event" requests, we want to generate a list
+ * of affected event IDs and work off that.
+ *
+ * TODO: use GROUP BY to reduce the number of rows returned in the cursor. (The content
+ * provider query() doesn't take it as an argument.)
+ */
+ HashSet<Long> eventIdSet = new HashSet<Long>();
+ Cursor c = query(uri, new String[] { Attendees.EVENT_ID }, selection, selectionArgs, null);
+ try {
+ while (c.moveToNext()) {
+ eventIdSet.add(c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+
+ /*
+ * If this isn't a sync adapter, duplicate each event (along with its associated tables),
+ * and mark each as "dirty". This is for the benefit of partial-update sync.
+ */
+ if (!callerIsSyncAdapter) {
+ ContentValues dirtyValues = new ContentValues();
+ dirtyValues.put(Events.DIRTY, "1");
+
+ Iterator<Long> iter = eventIdSet.iterator();
+ while (iter.hasNext()) {
+ long eventId = iter.next();
+ mDbHelper.duplicateEvent(eventId);
+ mDb.update(Tables.EVENTS, dirtyValues, SQL_WHERE_ID,
+ new String[] { String.valueOf(eventId) });
+ }
+ }
+
+ /*
+ * Issue the original deletion request. If we were called with a by-ID URI, generate
+ * a selection.
+ */
+ if (byId) {
+ selection = SQL_WHERE_ID;
+ selectionArgs = new String[] { String.valueOf(rowId) };
+ }
+ int delCount = mDb.delete(Tables.REMINDERS, selection, selectionArgs);
+
+ /*
+ * For each event, set "hasAlarm" to zero if we've deleted the last of the reminders.
+ * (If the event still has reminders, hasAlarm should already be 1.) Because we're
+ * executing in an exclusive transaction there's no risk of racing against other
+ * database updates.
+ */
+ ContentValues noAlarmValues = new ContentValues();
+ noAlarmValues.put(Events.HAS_ALARM, 0);
+ Iterator<Long> iter = eventIdSet.iterator();
+ while (iter.hasNext()) {
+ long eventId = iter.next();
+
+ // Count up the number of reminders still associated with this event.
+ Cursor reminders = mDb.query(Tables.REMINDERS, new String[] { GENERIC_ID },
+ SQL_WHERE_EVENT_ID, new String[] { String.valueOf(eventId) },
+ null, null, null);
+ int reminderCount = reminders.getCount();
+ reminders.close();
+
+ if (reminderCount == 0) {
+ mDb.update(Tables.EVENTS, noAlarmValues, SQL_WHERE_ID,
+ new String[] { String.valueOf(eventId) });
+ }
+ }
+
+ return delCount;
+ }
+
+ /**
* Update rows in a table and, if this is a non-sync-adapter update, mark the corresponding
* events as dirty.
* <p>
@@ -3232,6 +3367,12 @@
private int handleUpdateEvents(Cursor cursor, ContentValues updateValues,
boolean callerIsSyncAdapter) {
/*
+ * This field is considered read-only. It should not be modified by applications or
+ * by the sync adapter.
+ */
+ updateValues.remove(Events.HAS_ALARM);
+
+ /*
* For a single event, we can just load the event, merge modValues in, perform any
* fix-ups (putting changes into modValues), check validity, and then update(). We have
* to be careful that our fix-ups don't confuse the sync adapter.