GlobalDismissManager.java revision 5f44e994a6f1dabf5a208f0035d476e71460626b
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.calendar.alerts; 18 19import android.content.BroadcastReceiver; 20import android.content.ContentResolver; 21import android.content.ContentValues; 22import android.content.Context; 23import android.content.Intent; 24import android.content.SharedPreferences; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Bundle; 28import android.provider.CalendarContract.CalendarAlerts; 29import android.provider.CalendarContract.Calendars; 30import android.provider.CalendarContract.Events; 31import android.util.Log; 32import android.util.Pair; 33 34import com.android.calendar.CloudNotificationBackplane; 35import com.android.calendar.ExtensionsFactory; 36import com.android.calendar.R; 37 38import java.io.IOException; 39import java.util.HashMap; 40import java.util.HashSet; 41import java.util.LinkedHashSet; 42import java.util.List; 43import java.util.Map; 44import java.util.Set; 45 46/** 47 * Utilities for managing notification dismissal across devices. 48 */ 49public class GlobalDismissManager extends BroadcastReceiver { 50 private static final String TAG = "GlobalDismissManager"; 51 private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; 52 private static final String GLOBAL_DISMISS_MANAGER_PREFS = "com.android.calendar.alerts.GDM"; 53 private static final String ACCOUNT_KEY = "known_accounts"; 54 protected static final long FOUR_WEEKS = 60 * 60 * 24 * 7 * 4; 55 56 static final String[] EVENT_PROJECTION = new String[] { 57 Events._ID, 58 Events.CALENDAR_ID 59 }; 60 static final String[] EVENT_SYNC_PROJECTION = new String[] { 61 Events._ID, 62 Events._SYNC_ID 63 }; 64 static final String[] CALENDARS_PROJECTION = new String[] { 65 Calendars._ID, 66 Calendars.ACCOUNT_NAME, 67 Calendars.ACCOUNT_TYPE 68 }; 69 70 public static final String KEY_PREFIX = "com.android.calendar.alerts."; 71 public static final String SYNC_ID = KEY_PREFIX + "sync_id"; 72 public static final String START_TIME = KEY_PREFIX + "start_time"; 73 public static final String ACCOUNT_NAME = KEY_PREFIX + "account_name"; 74 public static final String DISMISS_INTENT = KEY_PREFIX + "DISMISS"; 75 76 public static class AlarmId { 77 public long mEventId; 78 public long mStart; 79 public AlarmId(long id, long start) { 80 mEventId = id; 81 mStart = start; 82 } 83 } 84 85 /** 86 * Look for unknown accounts in a set of events and associate with them. 87 * Returns immediately, processing happens in the background. 88 * 89 * @param context application context 90 * @param eventIds IDs for events that have posted notifications that may be 91 * dismissed. 92 */ 93 public static void processEventIds(final Context context, final Set<Long> eventIds) { 94 final String senderId = context.getResources().getString(R.string.notification_sender_id); 95 if (senderId == null || senderId.isEmpty()) { 96 Log.i(TAG, "no sender configured"); 97 return; 98 } 99 Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds); 100 Set<Long> calendars = new LinkedHashSet<Long>(); 101 calendars.addAll(eventsToCalendars.values()); 102 if (calendars.isEmpty()) { 103 Log.d(TAG, "found no calendars for events"); 104 return; 105 } 106 107 Map<Long, Pair<String, String>> calendarsToAccounts = 108 lookupCalendarToAccountMap(context, calendars); 109 110 if (calendarsToAccounts.isEmpty()) { 111 Log.d(TAG, "found no accounts for calendars"); 112 return; 113 } 114 115 // filter out non-google accounts (necessary?) 116 Set<String> accounts = new LinkedHashSet<String>(); 117 for (Pair<String, String> accountPair : calendarsToAccounts.values()) { 118 if (GOOGLE_ACCOUNT_TYPE.equals(accountPair.first)) { 119 accounts.add(accountPair.second); 120 } 121 } 122 123 // filter out accounts we already know about 124 SharedPreferences prefs = 125 context.getSharedPreferences(GLOBAL_DISMISS_MANAGER_PREFS, 126 Context.MODE_PRIVATE); 127 Set<String> existingAccounts = prefs.getStringSet(ACCOUNT_KEY, 128 new HashSet<String>()); 129 accounts.removeAll(existingAccounts); 130 131 if (accounts.isEmpty()) { 132 // nothing to do, we've already registered all the accounts. 133 return; 134 } 135 136 // subscribe to remaining accounts 137 CloudNotificationBackplane cnb = 138 ExtensionsFactory.getCloudNotificationBackplane(); 139 if (cnb.open(context)) { 140 for (String account : accounts) { 141 try { 142 if (cnb.subscribeToGroup(senderId, account, account)) { 143 existingAccounts.add(account); 144 } 145 } catch (IOException e) { 146 // Try again, next time the account triggers and alert. 147 } 148 } 149 cnb.close(); 150 prefs.edit() 151 .putStringSet(ACCOUNT_KEY, existingAccounts) 152 .commit(); 153 } 154 } 155 156 /** 157 * Globally dismiss notifications that are backed by the same events. 158 * 159 * @param context application context 160 * @param alarmIds Unique identifiers for events that have been dismissed by the user. 161 * @return true if notification_sender_id is available 162 */ 163 public static void dismissGlobally(final Context context, final List<AlarmId> alarmIds) { 164 final String senderId = context.getResources().getString(R.string.notification_sender_id); 165 if ("".equals(senderId)) { 166 Log.i(TAG, "no sender configured"); 167 return; 168 } 169 Set<Long> eventIds = new HashSet<Long>(alarmIds.size()); 170 for (AlarmId alarmId: alarmIds) { 171 eventIds.add(alarmId.mEventId); 172 } 173 // find the mapping between calendars and events 174 Map<Long, Long> eventsToCalendars = lookupEventToCalendarMap(context, eventIds); 175 176 if (eventsToCalendars.isEmpty()) { 177 Log.d(TAG, "found no calendars for events"); 178 return; 179 } 180 181 Set<Long> calendars = new LinkedHashSet<Long>(); 182 calendars.addAll(eventsToCalendars.values()); 183 184 // find the accounts associated with those calendars 185 Map<Long, Pair<String, String>> calendarsToAccounts = 186 lookupCalendarToAccountMap(context, calendars); 187 188 if (calendarsToAccounts.isEmpty()) { 189 Log.d(TAG, "found no accounts for calendars"); 190 return; 191 } 192 193 // TODO group by account to reduce queries 194 Map<String, String> syncIdToAccount = new HashMap<String, String>(); 195 Map<Long, String> eventIdToSyncId = new HashMap<Long, String>(); 196 ContentResolver resolver = context.getContentResolver(); 197 for (Long eventId : eventsToCalendars.keySet()) { 198 Long calendar = eventsToCalendars.get(eventId); 199 Pair<String, String> account = calendarsToAccounts.get(calendar); 200 if (GOOGLE_ACCOUNT_TYPE.equals(account.first)) { 201 Uri uri = asSync(Events.CONTENT_URI, account.first, account.second); 202 Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION, 203 Events._ID + " = " + eventId, null, null); 204 try { 205 cursor.moveToPosition(-1); 206 int sync_id_idx = cursor.getColumnIndex(Events._SYNC_ID); 207 if (sync_id_idx != -1) { 208 while (cursor.moveToNext()) { 209 String syncId = cursor.getString(sync_id_idx); 210 syncIdToAccount.put(syncId, account.second); 211 eventIdToSyncId.put(eventId, syncId); 212 } 213 } 214 } finally { 215 cursor.close(); 216 } 217 } 218 } 219 220 if (syncIdToAccount.isEmpty()) { 221 Log.d(TAG, "found no syncIds for events"); 222 return; 223 } 224 225 // TODO group by account to reduce packets 226 CloudNotificationBackplane cnb = ExtensionsFactory.getCloudNotificationBackplane(); 227 if (cnb.open(context)) { 228 for (AlarmId alarmId: alarmIds) { 229 String syncId = eventIdToSyncId.get(alarmId.mEventId); 230 String account = syncIdToAccount.get(syncId); 231 Bundle data = new Bundle(); 232 data.putString(SYNC_ID, syncId); 233 data.putString(START_TIME, Long.toString(alarmId.mStart)); 234 data.putString(ACCOUNT_NAME, account); 235 try { 236 cnb.send(account, syncId + ":" + alarmId.mStart, data); 237 } catch (IOException e) { 238 // TODO save a note to try again later 239 } 240 } 241 cnb.close(); 242 } 243 } 244 245 private static Uri asSync(Uri uri, String accountType, String account) { 246 return uri 247 .buildUpon() 248 .appendQueryParameter( 249 android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true") 250 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 251 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 252 } 253 254 /** 255 * build a selection over a set of row IDs 256 * 257 * @param ids row IDs to select 258 * @param key row name for the table 259 * @return a selection string suitable for a resolver query. 260 */ 261 private static String buildMultipleIdQuery(Set<Long> ids, String key) { 262 StringBuilder selection = new StringBuilder(); 263 boolean first = true; 264 for (Long id : ids) { 265 if (first) { 266 first = false; 267 } else { 268 selection.append(" OR "); 269 } 270 selection.append(key); 271 selection.append("="); 272 selection.append(id); 273 } 274 return selection.toString(); 275 } 276 277 /** 278 * @param context application context 279 * @param eventIds Event row IDs to query. 280 * @return a map from event to calendar 281 */ 282 private static Map<Long, Long> lookupEventToCalendarMap(final Context context, 283 final Set<Long> eventIds) { 284 Map<Long, Long> eventsToCalendars = new HashMap<Long, Long>(); 285 ContentResolver resolver = context.getContentResolver(); 286 String eventSelection = buildMultipleIdQuery(eventIds, Events._ID); 287 Cursor eventCursor = resolver.query(Events.CONTENT_URI, EVENT_PROJECTION, 288 eventSelection, null, null); 289 try { 290 eventCursor.moveToPosition(-1); 291 int calendar_id_idx = eventCursor.getColumnIndex(Events.CALENDAR_ID); 292 int event_id_idx = eventCursor.getColumnIndex(Events._ID); 293 if (calendar_id_idx != -1 && event_id_idx != -1) { 294 while (eventCursor.moveToNext()) { 295 eventsToCalendars.put(eventCursor.getLong(event_id_idx), 296 eventCursor.getLong(calendar_id_idx)); 297 } 298 } 299 } finally { 300 eventCursor.close(); 301 } 302 return eventsToCalendars; 303 } 304 305 /** 306 * @param context application context 307 * @param calendars Calendar row IDs to query. 308 * @return a map from Calendar to a pair (account type, account name) 309 */ 310 private static Map<Long, Pair<String, String>> lookupCalendarToAccountMap(final Context context, 311 Set<Long> calendars) { 312 Map<Long, Pair<String, String>> calendarsToAccounts = 313 new HashMap<Long, Pair<String, String>>(); 314 ; 315 ContentResolver resolver = context.getContentResolver(); 316 String calendarSelection = buildMultipleIdQuery(calendars, Calendars._ID); 317 Cursor calendarCursor = resolver.query(Calendars.CONTENT_URI, CALENDARS_PROJECTION, 318 calendarSelection, null, null); 319 try { 320 calendarCursor.moveToPosition(-1); 321 int calendar_id_idx = calendarCursor.getColumnIndex(Calendars._ID); 322 int account_name_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_NAME); 323 int account_type_idx = calendarCursor.getColumnIndex(Calendars.ACCOUNT_TYPE); 324 if (calendar_id_idx != -1 && account_name_idx != -1 && account_type_idx != -1) { 325 while (calendarCursor.moveToNext()) { 326 Long id = calendarCursor.getLong(calendar_id_idx); 327 String name = calendarCursor.getString(account_name_idx); 328 String type = calendarCursor.getString(account_type_idx); 329 calendarsToAccounts.put(id, new Pair<String, String>(type, name)); 330 } 331 } 332 } finally { 333 calendarCursor.close(); 334 } 335 return calendarsToAccounts; 336 } 337 338 @Override 339 public void onReceive(Context context, Intent intent) { 340 boolean updated = false; 341 if (intent.hasExtra(SYNC_ID) && intent.hasExtra(ACCOUNT_NAME)) { 342 String syncId = intent.getStringExtra(SYNC_ID); 343 long startTime = Long.parseLong(intent.getStringExtra(START_TIME)); 344 ContentResolver resolver = context.getContentResolver(); 345 346 Uri uri = asSync(Events.CONTENT_URI, GOOGLE_ACCOUNT_TYPE, 347 intent.getStringExtra(ACCOUNT_NAME)); 348 Cursor cursor = resolver.query(uri, EVENT_SYNC_PROJECTION, 349 Events._SYNC_ID + " = '" + syncId + "'", null, null); 350 try { 351 int event_id_idx = cursor.getColumnIndex(Events._ID); 352 cursor.moveToFirst(); 353 if (event_id_idx != -1 && !cursor.isAfterLast()) { 354 long eventId = cursor.getLong(event_id_idx); 355 ContentValues values = new ContentValues(); 356 String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.STATE_FIRED + 357 " AND " + CalendarAlerts.EVENT_ID + "=" + eventId + 358 " AND " + CalendarAlerts.BEGIN + "=" + startTime; 359 values.put(CalendarAlerts.STATE, CalendarAlerts.STATE_DISMISSED); 360 if (resolver.update(CalendarAlerts.CONTENT_URI, values, selection, null) > 0) { 361 updated |= true; 362 } 363 } 364 } finally { 365 cursor.close(); 366 } 367 } 368 369 if (updated) { 370 Log.d(TAG, "updating alarm state"); 371 AlertService.updateAlertNotification(context); 372 } 373 } 374} 375