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