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