summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/calendar/CloudNotificationBackplane.java30
-rw-r--r--src/com/android/calendar/ExtensionsFactory.java39
-rw-r--r--src/com/android/calendar/alerts/AlertReceiver.java4
-rw-r--r--src/com/android/calendar/alerts/AlertService.java2
-rw-r--r--src/com/android/calendar/alerts/AlertUtils.java1
-rw-r--r--src/com/android/calendar/alerts/DismissAlarmsService.java16
-rw-r--r--src/com/android/calendar/alerts/GlobalDismissManager.java390
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);
+ }
+}