• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.server.notification;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.database.ContentObserver;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteException;
25 import android.net.Uri;
26 import android.provider.CalendarContract.Attendees;
27 import android.provider.CalendarContract.Calendars;
28 import android.provider.CalendarContract.Events;
29 import android.provider.CalendarContract.Instances;
30 import android.service.notification.ZenModeConfig.EventInfo;
31 import android.util.ArraySet;
32 import android.util.Log;
33 import android.util.Slog;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.io.PrintWriter;
38 import java.util.Date;
39 import java.util.Objects;
40 
41 public class CalendarTracker {
42     private static final String TAG = "ConditionProviders.CT";
43     private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
44     private static final boolean DEBUG_ATTENDEES = false;
45 
46     private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000;
47 
48     private static final String[] INSTANCE_PROJECTION = {
49             Instances.BEGIN,
50             Instances.END,
51             Instances.TITLE,
52             Instances.VISIBLE,
53             Instances.EVENT_ID,
54             Instances.CALENDAR_DISPLAY_NAME,
55             Instances.OWNER_ACCOUNT,
56             Instances.CALENDAR_ID,
57             Instances.AVAILABILITY,
58     };
59 
60     private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC";
61 
62     private static final String[] ATTENDEE_PROJECTION = {
63         Attendees.EVENT_ID,
64         Attendees.ATTENDEE_EMAIL,
65         Attendees.ATTENDEE_STATUS,
66     };
67 
68     private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
69             + Attendees.ATTENDEE_EMAIL + " = ?";
70 
71     private final Context mSystemContext;
72     private final Context mUserContext;
73 
74     private Callback mCallback;
75     private boolean mRegistered;
76 
CalendarTracker(Context systemContext, Context userContext)77     public CalendarTracker(Context systemContext, Context userContext) {
78         mSystemContext = systemContext;
79         mUserContext = userContext;
80     }
81 
setCallback(Callback callback)82     public void setCallback(Callback callback) {
83         if (mCallback == callback) return;
84         mCallback = callback;
85         setRegistered(mCallback != null);
86     }
87 
dump(String prefix, PrintWriter pw)88     public void dump(String prefix, PrintWriter pw) {
89         pw.print(prefix); pw.print("mCallback="); pw.println(mCallback);
90         pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered);
91         pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId());
92     }
93 
getCalendarsWithAccess()94     private ArraySet<Long> getCalendarsWithAccess() {
95         final long start = System.currentTimeMillis();
96         final ArraySet<Long> rt = new ArraySet<>();
97         final String[] projection = { Calendars._ID };
98         final String selection = Calendars.CALENDAR_ACCESS_LEVEL + " >= "
99                 + Calendars.CAL_ACCESS_CONTRIBUTOR
100                 + " AND " + Calendars.SYNC_EVENTS + " = 1";
101         Cursor cursor = null;
102         try {
103             cursor = mUserContext.getContentResolver().query(Calendars.CONTENT_URI, projection,
104                     selection, null, null);
105             while (cursor != null && cursor.moveToNext()) {
106                 rt.add(cursor.getLong(0));
107             }
108         } catch (SQLiteException e) {
109             Slog.w(TAG, "error querying calendar content provider", e);
110         } finally {
111             if (cursor != null) {
112                 cursor.close();
113             }
114         }
115         if (DEBUG) {
116             Log.d(TAG, "getCalendarsWithAccess took " + (System.currentTimeMillis() - start));
117         }
118         return rt;
119     }
120 
checkEvent(EventInfo filter, long time)121     public CheckEventResult checkEvent(EventInfo filter, long time) {
122         final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
123         ContentUris.appendId(uriBuilder, time);
124         ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
125         final Uri uri = uriBuilder.build();
126         Cursor cursor = null;
127         final CheckEventResult result = new CheckEventResult();
128         result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
129         try {
130             cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION,
131                     null, null, INSTANCE_ORDER_BY);
132             final ArraySet<Long> calendars = getCalendarsWithAccess();
133             while (cursor != null && cursor.moveToNext()) {
134                 final long begin = cursor.getLong(0);
135                 final long end = cursor.getLong(1);
136                 final String title = cursor.getString(2);
137                 final boolean calendarVisible = cursor.getInt(3) == 1;
138                 final int eventId = cursor.getInt(4);
139                 final String name = cursor.getString(5);
140                 final String owner = cursor.getString(6);
141                 final long calendarId = cursor.getLong(7);
142                 final int availability = cursor.getInt(8);
143                 final boolean canAccessCal = calendars.contains(calendarId);
144                 if (DEBUG) {
145                     Log.d(TAG, String.format("title=%s time=%s-%s vis=%s availability=%s "
146                                     + "eventId=%s name=%s owner=%s calId=%s canAccessCal=%s",
147                             title, new Date(begin), new Date(end), calendarVisible,
148                             availabilityToString(availability), eventId, name, owner, calendarId,
149                             canAccessCal));
150                 }
151                 final boolean meetsTime = time >= begin && time < end;
152                 final boolean meetsCalendar = calendarVisible && canAccessCal
153                         && ((filter.calName == null && filter.calendarId == null)
154                         || (Objects.equals(filter.calendarId, calendarId))
155                         || Objects.equals(filter.calName, name));
156                 final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE;
157                 if (meetsCalendar && meetsAvailability) {
158                     if (DEBUG) Log.d(TAG, "  MEETS CALENDAR & AVAILABILITY");
159                     final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
160                     if (meetsAttendee) {
161                         if (DEBUG) Log.d(TAG, "    MEETS ATTENDEE");
162                         if (meetsTime) {
163                             if (DEBUG) Log.d(TAG, "      MEETS TIME");
164                             result.inEvent = true;
165                         }
166                         if (begin > time && begin < result.recheckAt) {
167                             result.recheckAt = begin;
168                         } else if (end > time && end < result.recheckAt) {
169                             result.recheckAt = end;
170                         }
171                     }
172                 }
173             }
174         } catch (Exception e) {
175             Slog.w(TAG, "error reading calendar", e);
176         } finally {
177             if (cursor != null) {
178                 cursor.close();
179             }
180         }
181         return result;
182     }
183 
184     private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
185         final long start = System.currentTimeMillis();
186         String selection = ATTENDEE_SELECTION;
187         String[] selectionArgs = { Integer.toString(eventId), email };
188         if (DEBUG_ATTENDEES) {
189             selection = null;
190             selectionArgs = null;
191         }
192         Cursor cursor = null;
193         try {
194             cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI,
195                     ATTENDEE_PROJECTION, selection, selectionArgs, null);
196             if (cursor == null || cursor.getCount() == 0) {
197                 if (DEBUG) Log.d(TAG, "No attendees found");
198                 return true;
199             }
200             boolean rt = false;
201             while (cursor != null && cursor.moveToNext()) {
202                 final long rowEventId = cursor.getLong(0);
203                 final String rowEmail = cursor.getString(1);
204                 final int status = cursor.getInt(2);
205                 final boolean meetsReply = meetsReply(filter.reply, status);
206                 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
207                         "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
208                         String.format("status=%s, meetsReply=%s",
209                         attendeeStatusToString(status), meetsReply));
210                 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
211                         && meetsReply;
212                 rt |= eventMeets;
213             }
214             return rt;
215         } catch (SQLiteException e) {
216             Slog.w(TAG, "error querying attendees content provider", e);
217             return false;
218         } finally {
219             if (cursor != null) {
220                 cursor.close();
221             }
222             if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start));
223         }
224     }
225 
226     private void setRegistered(boolean registered) {
227         if (mRegistered == registered) return;
228         final ContentResolver cr = mSystemContext.getContentResolver();
229         final int userId = mUserContext.getUserId();
230         if (mRegistered) {
231             if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId);
232             cr.unregisterContentObserver(mObserver);
233         }
234         mRegistered = registered;
235         if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId);
236         if (mRegistered) {
237             if (DEBUG) Log.d(TAG, "register content observer u=" + userId);
238             cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId);
239             cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId);
240             cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId);
241         }
242     }
243 
244     private static String attendeeStatusToString(int status) {
245         switch (status) {
246             case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
247             case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
248             case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
249             case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
250             case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
251             default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
252         }
253     }
254 
255     private static String availabilityToString(int availability) {
256         switch (availability) {
257             case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY";
258             case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE";
259             case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE";
260             default: return "AVAILABILITY_UNKNOWN_" + availability;
261         }
262     }
263 
264     private static boolean meetsReply(int reply, int attendeeStatus) {
265         switch (reply) {
266             case EventInfo.REPLY_YES:
267                 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
268             case EventInfo.REPLY_YES_OR_MAYBE:
269                 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED
270                         || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE;
271             case EventInfo.REPLY_ANY_EXCEPT_NO:
272                 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
273             default:
274                 return false;
275         }
276     }
277 
278     private final ContentObserver mObserver = new ContentObserver(null) {
279         @Override
280         public void onChange(boolean selfChange, Uri u) {
281             if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u
282                     + " u=" + mUserContext.getUserId());
283             mCallback.onChanged();
284         }
285 
286         @Override
287         public void onChange(boolean selfChange) {
288             if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
289         }
290     };
291 
292     public static class CheckEventResult {
293         public boolean inEvent;
294         public long recheckAt;
295     }
296 
297     public interface Callback {
298         void onChanged();
299     }
300 
301     @VisibleForTesting // (otherwise = NONE)
302     public int getUserId() {
303         return mUserContext.getUserId();
304     }
305 }
306