Commit fec5bff4 authored by Chris Wren's avatar Chris Wren
Browse files

add global dismiss to calendar, open source part

Change-Id: If179adc814a1da977712c9dc804e81a2cc726bce
parent 268fe32f
......@@ -185,6 +185,9 @@
</intent-filter>
</receiver>
<receiver android:name=".alerts.GlobalDismissManager"
android:exported="false" />
<service android:name=".alerts.AlertService" />
<service android:name=".alerts.DismissAlarmsService" />
......
......@@ -710,4 +710,6 @@
<!-- Description of the selected marker for accessibility support [CHAR LIMIT = NONE]-->
<string name="acessibility_recurrence_choose_end_date_description">change end date</string>
<!-- Do Not Translate. Sender identity for global notification synchronization. -->
<string name="notification_sender_id"></string>
</resources>
/*
* 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, String collapseKey,
long timeToLive, Bundle data) throws IOException;
public void close();
}
......@@ -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,40 @@ 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, String collapseKey,
long timeToLive, Bundle data) {
}
@Override
public void close() {
}
};
}
return cnb;
}
}
......@@ -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);
......
......@@ -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();
......
......@@ -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
......
......@@ -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);
......
/*
* 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.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 SYNC_ID = "sync_id";
public static final String START_TIME = "start_time";
public static final String ACCOUNT_NAME = "account_name"; // redundant?
public static final String DISMISS_INTENT = "com.android.calendar.alerts.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.putLong(START_TIME, alarmId.mStart);
data.putString(ACCOUNT_NAME, account);
try {
cnb.send(account,
syncId + ":" + alarmId.mStart,
syncId, FOUR_WEEKS, 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 = intent.getLongExtra(START_TIME, 0L);
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);
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment