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