diff options
author | Paul Sliwowski <psliwowski@google.com> | 2013-06-17 18:06:51 -0700 |
---|---|---|
committer | Michael Bestas <mkbestas@lineageos.org> | 2020-06-24 23:14:05 +0300 |
commit | b09b3452e08935be5b4ebb4671f8817a2619350f (patch) | |
tree | 5bff7c665471688bfe52da4425107c58c4dfee74 | |
parent | c3af36b95e87d25eba27370b68c7d1fcea97622b (diff) | |
download | android_packages_apps_Etar-b09b3452e08935be5b4ebb4671f8817a2619350f.tar.gz android_packages_apps_Etar-b09b3452e08935be5b4ebb4671f8817a2619350f.tar.bz2 android_packages_apps_Etar-b09b3452e08935be5b4ebb4671f8817a2619350f.zip |
Add in-memory cache to dismiss alerts that we haven't seen yet.
Bug: 9018194
This adds an in-memory cache of alerts that we haven't seen
yet, so that we can dismiss them once the calendar data
provider syncs.
Change-Id: I9490f478681783200a5cb8309a58243723bda4c1
-rw-r--r-- | src/com/android/calendar/alerts/AlertService.java | 11 | ||||
-rw-r--r-- | src/com/android/calendar/alerts/GlobalDismissManager.java | 379 |
2 files changed, 281 insertions, 109 deletions
diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java index dabf693d..a3b53d61 100644 --- a/src/com/android/calendar/alerts/AlertService.java +++ b/src/com/android/calendar/alerts/AlertService.java @@ -184,6 +184,8 @@ public class AlertService extends Service { return true; } + // Sync CalendarAlerts with global dismiss cache before query it + GlobalDismissManager.syncReceiverDismissCache(context); Cursor alertCursor = cr.query(CalendarAlerts.CONTENT_URI, ALERT_PROJECTION, (ACTIVE_ALERTS_SELECTION + currentTime), ACTIVE_ALERTS_SELECTION_ARGS, ACTIVE_ALERTS_SORT); @@ -655,7 +657,7 @@ public class AlertService extends Service { lowPriorityEvents.add(newInfo); } } - // TODO(cwren) add beginTime/startTime + // TODO(psliwowski): move this to account synchronization GlobalDismissManager.processEventIds(context, eventIds.keySet()); } finally { if (alertCursor != null) { @@ -875,6 +877,13 @@ public class AlertService extends Service { } } + // If we dismissed a notification for a new event, then we need to sync the cache when + // an ACTION_PROVIDER_CHANGED event has been sent. Unfortunately, the data provider + // has a delay of CalendarProvider2.SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS (ie. 30 sec.) + // until it notifies us that the sync adapter has finished. + // TODO(psliwowski): Find a quicker way to be notified when the data provider has the + // syncId for event. + GlobalDismissManager.syncSenderDismissCache(this); updateAlertNotification(this); } else if (action.equals(Intent.ACTION_TIME_CHANGED)) { doTimeChanged(); diff --git a/src/com/android/calendar/alerts/GlobalDismissManager.java b/src/com/android/calendar/alerts/GlobalDismissManager.java index 051b7014..89a06919 100644 --- a/src/com/android/calendar/alerts/GlobalDismissManager.java +++ b/src/com/android/calendar/alerts/GlobalDismissManager.java @@ -42,6 +42,7 @@ import com.android.calendar.ExtensionsFactory; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -53,12 +54,131 @@ import ws.xsoh.etar.R; * Utilities for managing notification dismissal across devices. */ public class GlobalDismissManager extends BroadcastReceiver { + private static class GlobalDismissId { + public final String mAccountName; + public final String mSyncId; + public final long mStartTime; + + private GlobalDismissId(String accountName, String syncId, long startTime) { + // TODO(psliwowski): Add guava library to use Preconditions class + if (accountName == null) { + throw new IllegalArgumentException("Account Name can not be set to null"); + } else if (syncId == null) { + throw new IllegalArgumentException("SyncId can not be set to null"); + } + mAccountName = accountName; + mSyncId = syncId; + mStartTime = startTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + GlobalDismissId that = (GlobalDismissId) o; + + if (mStartTime != that.mStartTime) { + return false; + } + if (!mAccountName.equals(that.mAccountName)) { + return false; + } + if (!mSyncId.equals(that.mSyncId)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = mAccountName.hashCode(); + result = 31 * result + mSyncId.hashCode(); + result = 31 * result + (int) (mStartTime ^ (mStartTime >>> 32)); + return result; + } + } + + public static class LocalDismissId { + public final String mAccountType; + public final String mAccountName; + public final long mEventId; + public final long mStartTime; + + public LocalDismissId(String accountType, String accountName, long eventId, + long startTime) { + if (accountType == null) { + throw new IllegalArgumentException("Account Type can not be null"); + } else if (accountName == null) { + throw new IllegalArgumentException("Account Name can not be null"); + } + + mAccountType = accountType; + mAccountName = accountName; + mEventId = eventId; + mStartTime = startTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LocalDismissId that = (LocalDismissId) o; + + if (mEventId != that.mEventId) { + return false; + } + if (mStartTime != that.mStartTime) { + return false; + } + if (!mAccountName.equals(that.mAccountName)) { + return false; + } + if (!mAccountType.equals(that.mAccountType)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = mAccountType.hashCode(); + result = 31 * result + mAccountName.hashCode(); + result = 31 * result + (int) (mEventId ^ (mEventId >>> 32)); + result = 31 * result + (int) (mStartTime ^ (mStartTime >>> 32)); + return result; + } + } + + public static class AlarmId { + public long mEventId; + public long mStart; + + public AlarmId(long id, long start) { + mEventId = id; + mStart = start; + } + } + + private static final long TIME_TO_LIVE = 1 * 60 * 60 * 1000; // 1 hour + public static final String KEY_PREFIX = "com.android.calendar.alerts."; public static final String SYNC_ID = KEY_PREFIX + "sync_id"; public static final String START_TIME = KEY_PREFIX + "start_time"; public static final String ACCOUNT_NAME = KEY_PREFIX + "account_name"; public static final String DISMISS_INTENT = KEY_PREFIX + "DISMISS"; - protected static final long FOUR_WEEKS = 60 * 60 * 24 * 7 * 4; + static final String[] EVENT_PROJECTION = new String[] { Events._ID, Events.CALENDAR_ID @@ -77,6 +197,12 @@ public class GlobalDismissManager extends BroadcastReceiver { private static final String GLOBAL_DISMISS_MANAGER_PREFS = "com.android.calendar.alerts.GDM"; private static final String ACCOUNT_KEY = "known_accounts"; + // TODO(psliwowski): Look into persisting these like AlertUtils.ALERTS_SHARED_PREFS_NAME + private static HashMap<GlobalDismissId, Long> sReceiverDismissCache = + new HashMap<GlobalDismissId, Long>(); + private static HashMap<LocalDismissId, Long> sSenderDismissCache = + new HashMap<LocalDismissId, Long>(); + /** * Look for unknown accounts in a set of events and associate with them. * Returns immediately, processing happens in the background. @@ -85,7 +211,7 @@ public class GlobalDismissManager extends BroadcastReceiver { * @param eventIds IDs for events that have posted notifications that may be * dismissed. */ - public static void processEventIds(final Context context, final Set<Long> eventIds) { + public static void processEventIds(Context context, Set<Long> eventIds) { final String senderId = context.getResources().getString(R.string.notification_sender_id); if (senderId == null || senderId.isEmpty()) { Log.i(TAG, "no sender configured"); @@ -149,25 +275,84 @@ public class GlobalDismissManager extends BroadcastReceiver { } /** - * Globally dismiss notifications that are backed by the same events. - * - * @param context application context - * @param alarmIds Unique identifiers for events that have been dismissed by the user. - * @return true if notification_sender_id is available + * Some events don't have a global sync_id when they are dismissed. We need to wait + * until the data provider is updated before we can send the global dismiss message. */ - public static void dismissGlobally(final Context context, final List<AlarmId> alarmIds) { + public static void syncSenderDismissCache(Context context) { final String senderId = context.getResources().getString(R.string.notification_sender_id); if ("".equals(senderId)) { Log.i(TAG, "no sender configured"); return; } + CloudNotificationBackplane cnb = ExtensionsFactory.getCloudNotificationBackplane(); + if (!cnb.open(context)) { + Log.i(TAG, "Unable to open could notification backplane"); + + } + + long currentTime = System.currentTimeMillis(); + ContentResolver resolver = context.getContentResolver(); + synchronized (sSenderDismissCache) { + Iterator<Map.Entry<LocalDismissId, Long>> it = + sSenderDismissCache.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<LocalDismissId, Long> entry = it.next(); + LocalDismissId dismissId = entry.getKey(); + + Uri uri = asSync(Events.CONTENT_URI, dismissId.mAccountType, + dismissId.mAccountName); + Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION, + Events._ID + " = " + dismissId.mEventId, null, null); + try { + cursor.moveToPosition(-1); + int sync_id_idx = cursor.getColumnIndex(Events._SYNC_ID); + if (sync_id_idx != -1) { + while (cursor.moveToNext()) { + String syncId = cursor.getString(sync_id_idx); + if (syncId != null) { + Bundle data = new Bundle(); + long startTime = dismissId.mStartTime; + String accountName = dismissId.mAccountName; + data.putString(SYNC_ID, syncId); + data.putString(START_TIME, Long.toString(startTime)); + data.putString(ACCOUNT_NAME, accountName); + try { + cnb.send(accountName, syncId + ":" + startTime, data); + it.remove(); + } catch (IOException e) { + // If we couldn't send, then leave dismissal in cache + } + } + } + } + } finally { + cursor.close(); + } + + // Remove old dismissals from cache after a certain time period + if (currentTime - entry.getValue() > TIME_TO_LIVE) { + it.remove(); + } + } + } + + cnb.close(); + } + + /** + * Globally dismiss notifications that are backed by the same events. + * + * @param context application context + * @param alarmIds Unique identifiers for events that have been dismissed by the user. + * @return true if notification_sender_id is available + */ + public static void dismissGlobally(Context context, List<AlarmId> alarmIds) { Set<Long> eventIds = new HashSet<Long>(alarmIds.size()); for (AlarmId alarmId: alarmIds) { eventIds.add(alarmId.mEventId); } // find the mapping between calendars and events Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds); - if (eventsToCalendars.isEmpty()) { Log.d(TAG, "found no calendars for events"); return; @@ -179,62 +364,24 @@ public class GlobalDismissManager extends BroadcastReceiver { // find the accounts associated with those calendars Map<Long, Pair<String, String>> calendarsToAccounts = lookupCalendarToAccountMap(context, calendars); - if (calendarsToAccounts.isEmpty()) { Log.d(TAG, "found no accounts for calendars"); return; } - // TODO group by account to reduce queries - Map<String, String> syncIdToAccount = new HashMap<String, String>(); - Map<Long, String> eventIdToSyncId = new HashMap<Long, String>(); - ContentResolver resolver = context.getContentResolver(); - for (Long eventId : eventsToCalendars.keySet()) { - Long calendar = eventsToCalendars.get(eventId); + long currentTime = System.currentTimeMillis(); + for (AlarmId alarmId : alarmIds) { + Long calendar = eventsToCalendars.get(alarmId.mEventId); Pair<String, String> account = calendarsToAccounts.get(calendar); if (GOOGLE_ACCOUNT_TYPE.equals(account.first)) { - Uri uri = asSync(Events.CONTENT_URI, account.first, account.second); - Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION, - Events._ID + " = " + eventId, null, null); - try { - cursor.moveToPosition(-1); - int sync_id_idx = cursor.getColumnIndex(Events._SYNC_ID); - if (sync_id_idx != -1) { - while (cursor.moveToNext()) { - String syncId = cursor.getString(sync_id_idx); - syncIdToAccount.put(syncId, account.second); - eventIdToSyncId.put(eventId, syncId); - } - } - } finally { - cursor.close(); - } - } - } - - if (syncIdToAccount.isEmpty()) { - Log.d(TAG, "found no syncIds for events"); - return; - } - - // TODO group by account to reduce packets - CloudNotificationBackplane cnb = ExtensionsFactory.getCloudNotificationBackplane(); - if (cnb.open(context)) { - for (AlarmId alarmId: alarmIds) { - String syncId = eventIdToSyncId.get(alarmId.mEventId); - String account = syncIdToAccount.get(syncId); - Bundle data = new Bundle(); - data.putString(SYNC_ID, syncId); - data.putString(START_TIME, Long.toString(alarmId.mStart)); - data.putString(ACCOUNT_NAME, account); - try { - cnb.send(account, syncId + ":" + alarmId.mStart, data); - } catch (IOException e) { - // TODO save a note to try again later + LocalDismissId dismissId = new LocalDismissId(account.first, account.second, + alarmId.mEventId, alarmId.mStart); + synchronized (sSenderDismissCache) { + sSenderDismissCache.put(dismissId, currentTime); } } - cnb.close(); } + syncSenderDismissCache(context); } private static Uri asSync(Uri uri, String accountType, String account) { @@ -274,8 +421,7 @@ public class GlobalDismissManager extends BroadcastReceiver { * @param eventIds Event row IDs to query. * @return a map from event to calendar */ - private static Map<Long, Long> lookupEventToCalendarMap(final Context context, - final Set<Long> eventIds) { + private static Map<Long, Long> lookupEventToCalendarMap(Context context, Set<Long> eventIds) { Map<Long, Long> eventsToCalendars = new HashMap<Long, Long>(); ContentResolver resolver = context.getContentResolver(); String eventSelection = buildMultipleIdQuery(eventIds, Events._ID); @@ -309,7 +455,7 @@ public class GlobalDismissManager extends BroadcastReceiver { * @param calendars Calendar row IDs to query. * @return a map from Calendar to a pair (account type, account name) */ - private static Map<Long, Pair<String, String>> lookupCalendarToAccountMap(final Context context, + private static Map<Long, Pair<String, String>> lookupCalendarToAccountMap(Context context, Set<Long> calendars) { Map<Long, Pair<String, String>> calendarsToAccounts = new HashMap<Long, Pair<String, String>>(); @@ -334,7 +480,9 @@ public class GlobalDismissManager extends BroadcastReceiver { Long id = calendarCursor.getLong(calendar_id_idx); String name = calendarCursor.getString(account_name_idx); String type = calendarCursor.getString(account_type_idx); - calendarsToAccounts.put(id, new Pair<String, String>(type, name)); + if (name != null && type != null) { + calendarsToAccounts.put(id, new Pair<String, String>(type, name)); + } } } } finally { @@ -343,67 +491,82 @@ public class GlobalDismissManager extends BroadcastReceiver { return calendarsToAccounts; } - @SuppressWarnings("unchecked") + /** + * We can get global dismisses for events we don't know exists yet, so sync our cache + * with the data provider whenever it updates. + */ + public static void syncReceiverDismissCache(Context context) { + ContentResolver resolver = context.getContentResolver(); + long currentTime = System.currentTimeMillis(); + synchronized (sReceiverDismissCache) { + Iterator<Map.Entry<GlobalDismissId, Long>> it = + sReceiverDismissCache.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<GlobalDismissId, Long> entry = it.next(); + GlobalDismissId globalDismissId = entry.getKey(); + Uri uri = GlobalDismissManager.asSync(Events.CONTENT_URI, + GlobalDismissManager.GOOGLE_ACCOUNT_TYPE, globalDismissId.mAccountName); + Cursor cursor = resolver.query(uri, GlobalDismissManager.EVENT_SYNC_PROJECTION, + Events._SYNC_ID + " = '" + globalDismissId.mSyncId + "'", + null, null); + try { + int event_id_idx = cursor.getColumnIndex(Events._ID); + cursor.moveToFirst(); + if (event_id_idx != -1 && !cursor.isAfterLast()) { + long eventId = cursor.getLong(event_id_idx); + ContentValues values = new ContentValues(); + String selection = "(" + CalendarAlerts.STATE + "=" + + CalendarAlerts.STATE_FIRED + " OR " + + CalendarAlerts.STATE + "=" + + CalendarAlerts.STATE_SCHEDULED + ") AND " + + CalendarAlerts.EVENT_ID + "=" + eventId + " AND " + + CalendarAlerts.BEGIN + "=" + globalDismissId.mStartTime; + values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); + int rows = resolver.update(CalendarAlerts.CONTENT_URI, values, + selection, null); + if (rows > 0) { + it.remove(); + } + } + } finally { + cursor.close(); + } + + if (currentTime - entry.getValue() > TIME_TO_LIVE) { + it.remove(); + } + } + } + } + @Override + @SuppressWarnings("unchecked") public void onReceive(Context context, Intent intent) { new AsyncTask<Pair<Context, Intent>, Void, Void>() { @Override protected Void doInBackground(Pair<Context, Intent>... params) { Context context = params[0].first; Intent intent = params[0].second; - boolean updated = false; - if (intent.hasExtra(SYNC_ID) && intent.hasExtra(ACCOUNT_NAME)) { - String syncId = intent.getStringExtra(SYNC_ID); - long startTime = Long.parseLong(intent.getStringExtra(START_TIME)); - ContentResolver resolver = context.getContentResolver(); - - Uri uri = asSync(Events.CONTENT_URI, GOOGLE_ACCOUNT_TYPE, - intent.getStringExtra(ACCOUNT_NAME)); - Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION, - Events._SYNC_ID + " = '" + syncId + "'", null, null); - try { - int event_id_idx = cursor.getColumnIndex(Events._ID); - cursor.moveToFirst(); - if (event_id_idx != -1 && !cursor.isAfterLast()) { - long eventId = cursor.getLong(event_id_idx); - ContentValues values = new ContentValues(); - String selection = CalendarAlerts.STATE + "=" + - CalendarAlerts.STATE_FIRED + " AND " + - CalendarAlerts.EVENT_ID + "=" + eventId + " AND " + - CalendarAlerts.BEGIN + "=" + startTime; - values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); - if (Build.VERSION.SDK_INT >= 23 && ContextCompat.checkSelfPermission(context, - Manifest.permission.WRITE_CALENDAR) - != PackageManager.PERMISSION_GRANTED) { - //If permission is not granted then just return. - Log.d(TAG, "Manifest.permission.READ_CALENDAR is not granted"); - return null; - } - int rows = resolver.update(CalendarAlerts.CONTENT_URI, values, - selection, null); - updated = rows > 0; - } - } finally { - cursor.close(); - } + if (Build.VERSION.SDK_INT >= 23 && ContextCompat.checkSelfPermission(context, + Manifest.permission.WRITE_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + //If permission is not granted then just return. + Log.d(TAG, "Manifest.permission.READ_CALENDAR is not granted"); + return null; } - - if (updated) { - Log.d(TAG, "updating alarm state"); + if (intent.hasExtra(SYNC_ID) && intent.hasExtra(ACCOUNT_NAME) + && intent.hasExtra(START_TIME)) { + synchronized (sReceiverDismissCache) { + sReceiverDismissCache.put(new GlobalDismissId( + intent.getStringExtra(ACCOUNT_NAME), + intent.getStringExtra(SYNC_ID), + Long.parseLong(intent.getStringExtra(START_TIME)) + ), System.currentTimeMillis()); + } AlertService.updateAlertNotification(context); } return null; } }.execute(new Pair<Context, Intent>(context, intent)); } - - public static class AlarmId { - public long mEventId; - public long mStart; - - public AlarmId(long id, long start) { - mEventId = id; - mStart = start; - } - } } |