diff options
Diffstat (limited to 'src/com')
7 files changed, 478 insertions, 4 deletions
diff --git a/src/com/android/calendar/CloudNotificationBackplane.java b/src/com/android/calendar/CloudNotificationBackplane.java new file mode 100644 index 00000000..d9ff0cc0 --- /dev/null +++ b/src/com/android/calendar/CloudNotificationBackplane.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 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; + +import java.io.IOException; + +import android.content.Context; +import android.os.Bundle; + +public interface CloudNotificationBackplane { + public boolean open(Context context); + public boolean subscribeToGroup(String senderId, String account, String groupId) + throws IOException; + public void send(String to, String msgId, Bundle data) throws IOException; + public void close(); +} diff --git a/src/com/android/calendar/ExtensionsFactory.java b/src/com/android/calendar/ExtensionsFactory.java index aaf7b01a..c323e168 100644 --- a/src/com/android/calendar/ExtensionsFactory.java +++ b/src/com/android/calendar/ExtensionsFactory.java @@ -18,6 +18,7 @@ package com.android.calendar; import android.content.Context; import android.content.res.AssetManager; +import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -27,10 +28,12 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; + /* * Skeleton for additional options in the AllInOne menu. */ public class ExtensionsFactory { + private static String TAG = "ExtensionsFactory"; // Config filename for mappings of various class names to their custom @@ -38,6 +41,7 @@ public class ExtensionsFactory { private static String EXTENSIONS_PROPERTIES = "calendar_extensions.properties"; private static String ALL_IN_ONE_MENU_KEY = "AllInOneMenuExtensions"; + private static String CLOUD_NOTIFICATION_KEY = "CloudNotificationChannel"; private static Properties sProperties = new Properties(); private static AllInOneMenuExtensionsInterface sAllInOneMenuExtensions = null; @@ -95,4 +99,39 @@ public class ExtensionsFactory { return sAllInOneMenuExtensions; } + + public static CloudNotificationBackplane getCloudNotificationBackplane() { + CloudNotificationBackplane cnb = null; + + String className = sProperties.getProperty(CLOUD_NOTIFICATION_KEY); + if (className != null) { + cnb = createInstance(className); + } else { + Log.d(TAG, CLOUD_NOTIFICATION_KEY + " not found in properties file."); + } + + if (cnb == null) { + cnb = new CloudNotificationBackplane() { + @Override + public boolean open(Context context) { + return true; + } + + @Override + public boolean subscribeToGroup(String senderId, String account, String groupId) + throws IOException { + return true;} + + @Override + public void send(String to, String msgId, Bundle data) { + } + + @Override + public void close() { + } + }; + } + + return cnb; + } } diff --git a/src/com/android/calendar/alerts/AlertReceiver.java b/src/com/android/calendar/alerts/AlertReceiver.java index e9822b1c..9005d36a 100644 --- a/src/com/android/calendar/alerts/AlertReceiver.java +++ b/src/com/android/calendar/alerts/AlertReceiver.java @@ -105,9 +105,7 @@ public class AlertReceiver extends BroadcastReceiver { } if (DELETE_ALL_ACTION.equals(intent.getAction())) { - /* The user has clicked the "Clear All Notifications" - * buttons so dismiss all Calendar alerts. - */ + // The user has dismissed a digest notification. // TODO Grab a wake lock here? Intent serviceIntent = new Intent(context, DismissAlarmsService.class); context.startService(serviceIntent); diff --git a/src/com/android/calendar/alerts/AlertService.java b/src/com/android/calendar/alerts/AlertService.java index 046af047..fbb5ad13 100644 --- a/src/com/android/calendar/alerts/AlertService.java +++ b/src/com/android/calendar/alerts/AlertService.java @@ -814,6 +814,8 @@ public class AlertService extends Service { lowPriorityEvents.add(newInfo); } } + // TODO(cwren) add beginTime/startTime + GlobalDismissManager.processEventIds(context, eventIds.keySet()); } finally { if (alertCursor != null) { alertCursor.close(); diff --git a/src/com/android/calendar/alerts/AlertUtils.java b/src/com/android/calendar/alerts/AlertUtils.java index 766d8e45..a9a74eee 100644 --- a/src/com/android/calendar/alerts/AlertUtils.java +++ b/src/com/android/calendar/alerts/AlertUtils.java @@ -56,6 +56,7 @@ public class AlertUtils { public static final String EVENT_END_KEY = "eventend"; public static final String NOTIFICATION_ID_KEY = "notificationid"; public static final String EVENT_IDS_KEY = "eventids"; + public static final String EVENT_STARTS_KEY = "starts"; // A flag for using local storage to save alert state instead of the alerts DB table. // This allows the unbundled app to run alongside other calendar apps without eating diff --git a/src/com/android/calendar/alerts/DismissAlarmsService.java b/src/com/android/calendar/alerts/DismissAlarmsService.java index b52ffd5a..64ff2aa4 100644 --- a/src/com/android/calendar/alerts/DismissAlarmsService.java +++ b/src/com/android/calendar/alerts/DismissAlarmsService.java @@ -28,6 +28,10 @@ import android.provider.CalendarContract.CalendarAlerts; import android.support.v4.app.TaskStackBuilder; import com.android.calendar.EventInfoActivity; +import com.android.calendar.alerts.GlobalDismissManager.AlarmId; + +import java.util.LinkedList; +import java.util.List; /** * Service for asynchronously marking fired alarms as dismissed. @@ -55,21 +59,31 @@ public class DismissAlarmsService extends IntentService { long eventEnd = intent.getLongExtra(AlertUtils.EVENT_END_KEY, -1); boolean showEvent = intent.getBooleanExtra(AlertUtils.SHOW_EVENT_KEY, false); long[] eventIds = intent.getLongArrayExtra(AlertUtils.EVENT_IDS_KEY); + long[] eventStarts = intent.getLongArrayExtra(AlertUtils.EVENT_STARTS_KEY); int notificationId = intent.getIntExtra(AlertUtils.NOTIFICATION_ID_KEY, -1); + List<AlarmId> alarmIds = new LinkedList<AlarmId>(); Uri uri = CalendarAlerts.CONTENT_URI; String selection; // Dismiss a specific fired alarm if id is present, otherwise, dismiss all alarms if (eventId != -1) { + alarmIds.add(new AlarmId(eventId, eventStart)); selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED + " AND " + CalendarAlerts.EVENT_ID + "=" + eventId; - } else if (eventIds != null && eventIds.length > 0) { + } else if (eventIds != null && eventIds.length > 0 && + eventStarts != null && eventIds.length == eventStarts.length) { selection = buildMultipleEventsQuery(eventIds); + for (int i = 1; i < eventIds.length; i++) { + alarmIds.add(new AlarmId(eventIds[i], eventStarts[i])); + } } else { + // NOTE: I don't believe that this ever happens. selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED; } + GlobalDismissManager.dismissGlobally(getApplicationContext(), alarmIds); + ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put(PROJECTION[COLUMN_INDEX_STATE], CalendarAlerts.STATE_DISMISSED); diff --git a/src/com/android/calendar/alerts/GlobalDismissManager.java b/src/com/android/calendar/alerts/GlobalDismissManager.java new file mode 100644 index 00000000..f11e6ed6 --- /dev/null +++ b/src/com/android/calendar/alerts/GlobalDismissManager.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2013 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.Activity; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.CalendarContract.CalendarAlerts; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.Events; +import android.util.Log; +import android.util.Pair; + +import com.android.calendar.CloudNotificationBackplane; +import com.android.calendar.ExtensionsFactory; +import com.android.calendar.R; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for managing notification dismissal across devices. + */ +public class GlobalDismissManager extends BroadcastReceiver { + private static final String TAG = "GlobalDismissManager"; + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + private static final String GLOBAL_DISMISS_MANAGER_PREFS = "com.android.calendar.alerts.GDM"; + private static final String ACCOUNT_KEY = "known_accounts"; + protected static final long FOUR_WEEKS = 60 * 60 * 24 * 7 * 4; + + static final String[] EVENT_PROJECTION = new String[] { + Events._ID, + Events.CALENDAR_ID + }; + static final String[] EVENT_SYNC_PROJECTION = new String[] { + Events._ID, + Events._SYNC_ID + }; + static final String[] CALENDARS_PROJECTION = new String[] { + Calendars._ID, + Calendars.ACCOUNT_NAME, + Calendars.ACCOUNT_TYPE + }; + + 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"; + + public static class AlarmId { + public long mEventId; + public long mStart; + public AlarmId(long id, long start) { + mEventId = id; + mStart = start; + } + } + + /** + * Look for unknown accounts in a set of events and associate with them. + * Returns immediately, processing happens in the background. + * + * @param context application context + * @param eventIds IDs for events that have posted notifications that may be + * dismissed. + */ + public static void processEventIds(final Context context, final 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"); + return; + } + new AsyncTask<Void, Void, Void>() { + + @Override + protected Void doInBackground(Void... params) { + + Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds); + Set<Long> calendars = new LinkedHashSet<Long>(); + calendars.addAll(eventsToCalendars.values()); + if (calendars.isEmpty()) { + Log.d(TAG, "foudn no calendars for events"); + return null; + } + + Map<Long, Pair<String, String>> calendarsToAccounts = + lookupCalendarToAccountMap(context, calendars); + + if (calendarsToAccounts.isEmpty()) { + Log.d(TAG, "found no accounts for calendars"); + return null; + } + + // filter out non-google accounts (necessary?) + Set<String> accounts = new LinkedHashSet<String>(); + for (Pair<String, String> accountPair : calendarsToAccounts.values()) { + if (GOOGLE_ACCOUNT_TYPE.equals(accountPair.first)) { + accounts.add(accountPair.second); + } + } + + // filter out accounts we already know about + SharedPreferences prefs = + context.getSharedPreferences(GLOBAL_DISMISS_MANAGER_PREFS, + Context.MODE_PRIVATE); + Set<String> existingAccounts = prefs.getStringSet(ACCOUNT_KEY, + new HashSet<String>()); + accounts.removeAll(existingAccounts); + + if (accounts.isEmpty()) { + return null; + } + + // subscribe to remaining accounts + CloudNotificationBackplane cnb = + ExtensionsFactory.getCloudNotificationBackplane(); + if (cnb.open(context)) { + for (String account : accounts) { + try { + cnb.subscribeToGroup(senderId, account, account); + accounts.add(account); + } catch (IOException e) { + // Try again, next time the account triggers and alert. + } + } + cnb.close(); + prefs.edit() + .putStringSet(ACCOUNT_KEY, accounts) + .commit(); + } + return null; + } + }.execute(); + } + + /** + * 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(final Context context, final List<AlarmId> alarmIds) { + final String senderId = context.getResources().getString(R.string.notification_sender_id); + if ("".equals(senderId)) { + Log.i(TAG, "no sender configured"); + return; + } + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + 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 null; + } + + Set<Long> calendars = new LinkedHashSet<Long>(); + calendars.addAll(eventsToCalendars.values()); + + // 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 null; + } + + // 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); + 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 null; + } + + // 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 + } + } + cnb.close(); + } + return null; + } + }.execute(); + } + + private static Uri asSync(Uri uri, String accountType, String account) { + return uri + .buildUpon() + .appendQueryParameter( + android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(Calendars.ACCOUNT_NAME, account) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); + } + + /** + * build a selection over a set of row IDs + * + * @param ids row IDs to select + * @param key row name for the table + * @return a selection string suitable for a resolver query. + */ + private static String buildMultipleIdQuery(Set<Long> ids, String key) { + StringBuilder selection = new StringBuilder(); + boolean first = true; + for (Long id : ids) { + if (first) { + first = false; + } else { + selection.append(" OR "); + } + selection.append(key); + selection.append("="); + selection.append(id); + } + return selection.toString(); + } + + /** + * @param context application context + * @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) { + Map<Long, Long> eventsToCalendars = new HashMap<Long, Long>(); + ContentResolver resolver = context.getContentResolver(); + String eventSelection = buildMultipleIdQuery(eventIds, Events._ID); + Cursor eventCursor = resolver.query(Events.CONTENT_URI, EVENT_PROJECTION, + eventSelection, null, null); + try { + eventCursor.moveToPosition(-1); + int calendar_id_idx = eventCursor.getColumnIndex(Events.CALENDAR_ID); + int event_id_idx = eventCursor.getColumnIndex(Events._ID); + if (calendar_id_idx != -1 && event_id_idx != -1) { + while (eventCursor.moveToNext()) { + eventsToCalendars.put(eventCursor.getLong(event_id_idx), + eventCursor.getLong(calendar_id_idx)); + } + } + } finally { + eventCursor.close(); + } + return eventsToCalendars; + } + + /** + * @param context application context + * @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, + Set<Long> calendars) { + Map<Long, Pair<String, String>> calendarsToAccounts = + new HashMap<Long, Pair<String, String>>(); + ; + ContentResolver resolver = context.getContentResolver(); + String calendarSelection = buildMultipleIdQuery(calendars, Calendars._ID); + Cursor calendarCursor = resolver.query(Calendars.CONTENT_URI, CALENDARS_PROJECTION, + calendarSelection, null, null); + try { + calendarCursor.moveToPosition(-1); + int calendar_id_idx = calendarCursor.getColumnIndex(Calendars._ID); + int account_name_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_NAME); + int account_type_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_TYPE); + if (calendar_id_idx != -1 && account_name_idx != -1 && account_type_idx != -1) { + while (calendarCursor.moveToNext()) { + 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)); + } + } + } finally { + calendarCursor.close(); + } + return calendarsToAccounts; + } + + @Override + public void onReceive(Context context, Intent intent) { + 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 (resolver.update(CalendarAlerts.CONTENT_URI, values, selection, null) > 0) { + updated |= true; + } + } + } finally { + cursor.close(); + } + } + + if (updated) { + Log.d(TAG, "updating alarm state"); + AlertService.updateAlertNotification(context); + } + + setResultCode(Activity.RESULT_OK); + } +} |