blob: 4001b908e3b515f3edb1cb2e45b8ac139af42ce9 [file] [log] [blame]
John Spurlock2f096ed2015-05-04 11:58:26 -04001/*
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
17package com.android.server.notification;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.database.ContentObserver;
23import android.database.Cursor;
24import android.net.Uri;
25import android.provider.BaseColumns;
26import android.provider.CalendarContract.Attendees;
John Spurlock1b8b22b2015-05-20 09:47:13 -040027import android.provider.CalendarContract.Calendars;
28import android.provider.CalendarContract.Events;
John Spurlock2f096ed2015-05-04 11:58:26 -040029import android.provider.CalendarContract.Instances;
30import android.service.notification.ZenModeConfig.EventInfo;
John Spurlock1b8b22b2015-05-20 09:47:13 -040031import android.util.ArraySet;
John Spurlock2f096ed2015-05-04 11:58:26 -040032import android.util.Log;
Julia Reynolds9b7716762018-04-12 14:16:09 -040033import android.util.Slog;
John Spurlock2f096ed2015-05-04 11:58:26 -040034
35import java.io.PrintWriter;
36import java.util.Date;
37import java.util.Objects;
38
39public 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 = {
Julia Reynoldsa9864d22016-04-19 16:08:15 -040047 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,
John Spurlock2f096ed2015-05-04 11:58:26 -040056 };
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,
John Spurlock2f096ed2015-05-04 11:58:26 -040064 };
65
66 private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
67 + Attendees.ATTENDEE_EMAIL + " = ?";
68
John Spurlock1b8b22b2015-05-20 09:47:13 -040069 private final Context mSystemContext;
70 private final Context mUserContext;
John Spurlock2f096ed2015-05-04 11:58:26 -040071
72 private Callback mCallback;
73 private boolean mRegistered;
74
John Spurlock1b8b22b2015-05-20 09:47:13 -040075 public CalendarTracker(Context systemContext, Context userContext) {
76 mSystemContext = systemContext;
77 mUserContext = userContext;
John Spurlock2f096ed2015-05-04 11:58:26 -040078 }
79
80 public void setCallback(Callback callback) {
81 if (mCallback == callback) return;
82 mCallback = callback;
83 setRegistered(mCallback != null);
84 }
85
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);
John Spurlock1b8b22b2015-05-20 09:47:13 -040089 pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId());
John Spurlock2f096ed2015-05-04 11:58:26 -040090 }
91
John Spurlock1b8b22b2015-05-20 09:47:13 -040092 private ArraySet<Long> getPrimaryCalendars() {
93 final long start = System.currentTimeMillis();
94 final ArraySet<Long> rt = new ArraySet<>();
95 final String primary = "\"primary\"";
96 final String[] projection = { Calendars._ID,
97 "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + primary };
98 final String selection = primary + " = 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 } finally {
107 if (cursor != null) {
108 cursor.close();
109 }
110 }
111 if (DEBUG) Log.d(TAG, "getPrimaryCalendars took " + (System.currentTimeMillis() - start));
112 return rt;
113 }
John Spurlock2f096ed2015-05-04 11:58:26 -0400114
115 public CheckEventResult checkEvent(EventInfo filter, long time) {
116 final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
117 ContentUris.appendId(uriBuilder, time);
118 ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
119 final Uri uri = uriBuilder.build();
John Spurlock1b8b22b2015-05-20 09:47:13 -0400120 final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION,
121 null, null, INSTANCE_ORDER_BY);
John Spurlock2f096ed2015-05-04 11:58:26 -0400122 final CheckEventResult result = new CheckEventResult();
123 result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
124 try {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400125 final ArraySet<Long> primaryCalendars = getPrimaryCalendars();
126 while (cursor != null && cursor.moveToNext()) {
John Spurlock2f096ed2015-05-04 11:58:26 -0400127 final long begin = cursor.getLong(0);
128 final long end = cursor.getLong(1);
129 final String title = cursor.getString(2);
John Spurlock1b8b22b2015-05-20 09:47:13 -0400130 final boolean calendarVisible = cursor.getInt(3) == 1;
John Spurlock2f096ed2015-05-04 11:58:26 -0400131 final int eventId = cursor.getInt(4);
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400132 final String name = cursor.getString(5);
133 final String owner = cursor.getString(6);
134 final long calendarId = cursor.getLong(7);
135 final int availability = cursor.getInt(8);
John Spurlock1b8b22b2015-05-20 09:47:13 -0400136 final boolean calendarPrimary = primaryCalendars.contains(calendarId);
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400137 if (DEBUG) Log.d(TAG, String.format(
138 "%s %s-%s v=%s a=%s eid=%s n=%s o=%s cid=%s p=%s",
John Spurlock1b8b22b2015-05-20 09:47:13 -0400139 title,
140 new Date(begin), new Date(end), calendarVisible,
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400141 availabilityToString(availability), eventId, name, owner, calendarId,
John Spurlock1b8b22b2015-05-20 09:47:13 -0400142 calendarPrimary));
John Spurlock2f096ed2015-05-04 11:58:26 -0400143 final boolean meetsTime = time >= begin && time < end;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400144 final boolean meetsCalendar = calendarVisible && calendarPrimary
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400145 && (filter.calendar == null || Objects.equals(filter.calendar, owner)
146 || Objects.equals(filter.calendar, name));
John Spurlock1b8b22b2015-05-20 09:47:13 -0400147 final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE;
148 if (meetsCalendar && meetsAvailability) {
149 if (DEBUG) Log.d(TAG, " MEETS CALENDAR & AVAILABILITY");
John Spurlock2f096ed2015-05-04 11:58:26 -0400150 final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
151 if (meetsAttendee) {
152 if (DEBUG) Log.d(TAG, " MEETS ATTENDEE");
153 if (meetsTime) {
154 if (DEBUG) Log.d(TAG, " MEETS TIME");
155 result.inEvent = true;
156 }
157 if (begin > time && begin < result.recheckAt) {
158 result.recheckAt = begin;
159 } else if (end > time && end < result.recheckAt) {
160 result.recheckAt = end;
161 }
162 }
163 }
164 }
Julia Reynolds9b7716762018-04-12 14:16:09 -0400165 } catch (Exception e) {
166 Slog.w(TAG, "error reading calendar", e);
John Spurlock2f096ed2015-05-04 11:58:26 -0400167 } finally {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400168 if (cursor != null) {
169 cursor.close();
170 }
John Spurlock2f096ed2015-05-04 11:58:26 -0400171 }
172 return result;
173 }
174
175 private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400176 final long start = System.currentTimeMillis();
John Spurlock2f096ed2015-05-04 11:58:26 -0400177 String selection = ATTENDEE_SELECTION;
178 String[] selectionArgs = { Integer.toString(eventId), email };
179 if (DEBUG_ATTENDEES) {
180 selection = null;
181 selectionArgs = null;
182 }
John Spurlock1b8b22b2015-05-20 09:47:13 -0400183 final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI,
John Spurlock2f096ed2015-05-04 11:58:26 -0400184 ATTENDEE_PROJECTION, selection, selectionArgs, null);
185 try {
Julia Reynolds9bbe0bb2017-01-05 11:08:58 -0500186 if (cursor == null || cursor.getCount() == 0) {
John Spurlock2f096ed2015-05-04 11:58:26 -0400187 if (DEBUG) Log.d(TAG, "No attendees found");
188 return true;
189 }
190 boolean rt = false;
Julia Reynolds9bbe0bb2017-01-05 11:08:58 -0500191 while (cursor != null && cursor.moveToNext()) {
John Spurlock2f096ed2015-05-04 11:58:26 -0400192 final long rowEventId = cursor.getLong(0);
193 final String rowEmail = cursor.getString(1);
194 final int status = cursor.getInt(2);
John Spurlock2f096ed2015-05-04 11:58:26 -0400195 final boolean meetsReply = meetsReply(filter.reply, status);
John Spurlock2f096ed2015-05-04 11:58:26 -0400196 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
197 "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
John Spurlockd39af2d2015-05-05 09:49:32 -0400198 String.format("status=%s, meetsReply=%s",
199 attendeeStatusToString(status), meetsReply));
John Spurlock2f096ed2015-05-04 11:58:26 -0400200 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
John Spurlockd39af2d2015-05-05 09:49:32 -0400201 && meetsReply;
John Spurlock2f096ed2015-05-04 11:58:26 -0400202 rt |= eventMeets;
203 }
204 return rt;
205 } finally {
Julia Reynolds9bbe0bb2017-01-05 11:08:58 -0500206 if (cursor != null) {
207 cursor.close();
208 }
John Spurlock1b8b22b2015-05-20 09:47:13 -0400209 if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start));
John Spurlock2f096ed2015-05-04 11:58:26 -0400210 }
211 }
212
213 private void setRegistered(boolean registered) {
214 if (mRegistered == registered) return;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400215 final ContentResolver cr = mSystemContext.getContentResolver();
216 final int userId = mUserContext.getUserId();
John Spurlock2f096ed2015-05-04 11:58:26 -0400217 if (mRegistered) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400218 if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400219 cr.unregisterContentObserver(mObserver);
220 }
221 mRegistered = registered;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400222 if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400223 if (mRegistered) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400224 if (DEBUG) Log.d(TAG, "register content observer u=" + userId);
225 cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId);
226 cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId);
227 cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400228 }
229 }
230
231 private static String attendeeStatusToString(int status) {
232 switch (status) {
233 case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
234 case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
235 case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
236 case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
237 case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
238 default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
239 }
240 }
241
John Spurlock028a5392015-05-06 11:36:19 -0400242 private static String availabilityToString(int availability) {
243 switch (availability) {
244 case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY";
245 case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE";
246 case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE";
247 default: return "AVAILABILITY_UNKNOWN_" + availability;
248 }
249 }
250
John Spurlock2f096ed2015-05-04 11:58:26 -0400251 private static boolean meetsReply(int reply, int attendeeStatus) {
252 switch (reply) {
253 case EventInfo.REPLY_YES:
254 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
John Spurlockd39af2d2015-05-05 09:49:32 -0400255 case EventInfo.REPLY_YES_OR_MAYBE:
256 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED
257 || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE;
John Spurlock2f096ed2015-05-04 11:58:26 -0400258 case EventInfo.REPLY_ANY_EXCEPT_NO:
259 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
John Spurlockd39af2d2015-05-05 09:49:32 -0400260 default:
261 return false;
John Spurlock2f096ed2015-05-04 11:58:26 -0400262 }
263 }
264
265 private final ContentObserver mObserver = new ContentObserver(null) {
266 @Override
267 public void onChange(boolean selfChange, Uri u) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400268 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u
269 + " u=" + mUserContext.getUserId());
John Spurlock2f096ed2015-05-04 11:58:26 -0400270 mCallback.onChanged();
271 }
272
273 @Override
274 public void onChange(boolean selfChange) {
275 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
276 }
277 };
278
279 public static class CheckEventResult {
280 public boolean inEvent;
281 public long recheckAt;
282 }
283
284 public interface Callback {
285 void onChanged();
286 }
287
288}