blob: 5d50f3b7947fef203798c27c7818b8b65aa12e02 [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;
33
34import java.io.PrintWriter;
35import java.util.Date;
36import java.util.Objects;
37
38public class CalendarTracker {
39 private static final String TAG = "ConditionProviders.CT";
40 private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
41 private static final boolean DEBUG_ATTENDEES = false;
42
43 private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000;
44
45 private static final String[] INSTANCE_PROJECTION = {
Julia Reynoldsa9864d22016-04-19 16:08:15 -040046 Instances.BEGIN,
47 Instances.END,
48 Instances.TITLE,
49 Instances.VISIBLE,
50 Instances.EVENT_ID,
51 Instances.CALENDAR_DISPLAY_NAME,
52 Instances.OWNER_ACCOUNT,
53 Instances.CALENDAR_ID,
54 Instances.AVAILABILITY,
John Spurlock2f096ed2015-05-04 11:58:26 -040055 };
56
57 private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC";
58
59 private static final String[] ATTENDEE_PROJECTION = {
60 Attendees.EVENT_ID,
61 Attendees.ATTENDEE_EMAIL,
62 Attendees.ATTENDEE_STATUS,
John Spurlock2f096ed2015-05-04 11:58:26 -040063 };
64
65 private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
66 + Attendees.ATTENDEE_EMAIL + " = ?";
67
John Spurlock1b8b22b2015-05-20 09:47:13 -040068 private final Context mSystemContext;
69 private final Context mUserContext;
John Spurlock2f096ed2015-05-04 11:58:26 -040070
71 private Callback mCallback;
72 private boolean mRegistered;
73
John Spurlock1b8b22b2015-05-20 09:47:13 -040074 public CalendarTracker(Context systemContext, Context userContext) {
75 mSystemContext = systemContext;
76 mUserContext = userContext;
John Spurlock2f096ed2015-05-04 11:58:26 -040077 }
78
79 public void setCallback(Callback callback) {
80 if (mCallback == callback) return;
81 mCallback = callback;
82 setRegistered(mCallback != null);
83 }
84
85 public void dump(String prefix, PrintWriter pw) {
86 pw.print(prefix); pw.print("mCallback="); pw.println(mCallback);
87 pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered);
John Spurlock1b8b22b2015-05-20 09:47:13 -040088 pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId());
John Spurlock2f096ed2015-05-04 11:58:26 -040089 }
90
John Spurlock1b8b22b2015-05-20 09:47:13 -040091 private ArraySet<Long> getPrimaryCalendars() {
92 final long start = System.currentTimeMillis();
93 final ArraySet<Long> rt = new ArraySet<>();
94 final String primary = "\"primary\"";
95 final String[] projection = { Calendars._ID,
96 "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + primary };
97 final String selection = primary + " = 1";
98 Cursor cursor = null;
99 try {
100 cursor = mUserContext.getContentResolver().query(Calendars.CONTENT_URI, projection,
101 selection, null, null);
102 while (cursor != null && cursor.moveToNext()) {
103 rt.add(cursor.getLong(0));
104 }
105 } finally {
106 if (cursor != null) {
107 cursor.close();
108 }
109 }
110 if (DEBUG) Log.d(TAG, "getPrimaryCalendars took " + (System.currentTimeMillis() - start));
111 return rt;
112 }
John Spurlock2f096ed2015-05-04 11:58:26 -0400113
114 public CheckEventResult checkEvent(EventInfo filter, long time) {
115 final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
116 ContentUris.appendId(uriBuilder, time);
117 ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
118 final Uri uri = uriBuilder.build();
John Spurlock1b8b22b2015-05-20 09:47:13 -0400119 final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION,
120 null, null, INSTANCE_ORDER_BY);
John Spurlock2f096ed2015-05-04 11:58:26 -0400121 final CheckEventResult result = new CheckEventResult();
122 result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
123 try {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400124 final ArraySet<Long> primaryCalendars = getPrimaryCalendars();
125 while (cursor != null && cursor.moveToNext()) {
John Spurlock2f096ed2015-05-04 11:58:26 -0400126 final long begin = cursor.getLong(0);
127 final long end = cursor.getLong(1);
128 final String title = cursor.getString(2);
John Spurlock1b8b22b2015-05-20 09:47:13 -0400129 final boolean calendarVisible = cursor.getInt(3) == 1;
John Spurlock2f096ed2015-05-04 11:58:26 -0400130 final int eventId = cursor.getInt(4);
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400131 final String name = cursor.getString(5);
132 final String owner = cursor.getString(6);
133 final long calendarId = cursor.getLong(7);
134 final int availability = cursor.getInt(8);
John Spurlock1b8b22b2015-05-20 09:47:13 -0400135 final boolean calendarPrimary = primaryCalendars.contains(calendarId);
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400136 if (DEBUG) Log.d(TAG, String.format(
137 "%s %s-%s v=%s a=%s eid=%s n=%s o=%s cid=%s p=%s",
John Spurlock1b8b22b2015-05-20 09:47:13 -0400138 title,
139 new Date(begin), new Date(end), calendarVisible,
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400140 availabilityToString(availability), eventId, name, owner, calendarId,
John Spurlock1b8b22b2015-05-20 09:47:13 -0400141 calendarPrimary));
John Spurlock2f096ed2015-05-04 11:58:26 -0400142 final boolean meetsTime = time >= begin && time < end;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400143 final boolean meetsCalendar = calendarVisible && calendarPrimary
Julia Reynoldsa9864d22016-04-19 16:08:15 -0400144 && (filter.calendar == null || Objects.equals(filter.calendar, owner)
145 || Objects.equals(filter.calendar, name));
John Spurlock1b8b22b2015-05-20 09:47:13 -0400146 final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE;
147 if (meetsCalendar && meetsAvailability) {
148 if (DEBUG) Log.d(TAG, " MEETS CALENDAR & AVAILABILITY");
John Spurlock2f096ed2015-05-04 11:58:26 -0400149 final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
150 if (meetsAttendee) {
151 if (DEBUG) Log.d(TAG, " MEETS ATTENDEE");
152 if (meetsTime) {
153 if (DEBUG) Log.d(TAG, " MEETS TIME");
154 result.inEvent = true;
155 }
156 if (begin > time && begin < result.recheckAt) {
157 result.recheckAt = begin;
158 } else if (end > time && end < result.recheckAt) {
159 result.recheckAt = end;
160 }
161 }
162 }
163 }
164 } finally {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400165 if (cursor != null) {
166 cursor.close();
167 }
John Spurlock2f096ed2015-05-04 11:58:26 -0400168 }
169 return result;
170 }
171
172 private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400173 final long start = System.currentTimeMillis();
John Spurlock2f096ed2015-05-04 11:58:26 -0400174 String selection = ATTENDEE_SELECTION;
175 String[] selectionArgs = { Integer.toString(eventId), email };
176 if (DEBUG_ATTENDEES) {
177 selection = null;
178 selectionArgs = null;
179 }
John Spurlock1b8b22b2015-05-20 09:47:13 -0400180 final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI,
John Spurlock2f096ed2015-05-04 11:58:26 -0400181 ATTENDEE_PROJECTION, selection, selectionArgs, null);
182 try {
183 if (cursor.getCount() == 0) {
184 if (DEBUG) Log.d(TAG, "No attendees found");
185 return true;
186 }
187 boolean rt = false;
188 while (cursor.moveToNext()) {
189 final long rowEventId = cursor.getLong(0);
190 final String rowEmail = cursor.getString(1);
191 final int status = cursor.getInt(2);
John Spurlock2f096ed2015-05-04 11:58:26 -0400192 final boolean meetsReply = meetsReply(filter.reply, status);
John Spurlock2f096ed2015-05-04 11:58:26 -0400193 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
194 "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
John Spurlockd39af2d2015-05-05 09:49:32 -0400195 String.format("status=%s, meetsReply=%s",
196 attendeeStatusToString(status), meetsReply));
John Spurlock2f096ed2015-05-04 11:58:26 -0400197 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
John Spurlockd39af2d2015-05-05 09:49:32 -0400198 && meetsReply;
John Spurlock2f096ed2015-05-04 11:58:26 -0400199 rt |= eventMeets;
200 }
201 return rt;
202 } finally {
203 cursor.close();
John Spurlock1b8b22b2015-05-20 09:47:13 -0400204 if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start));
John Spurlock2f096ed2015-05-04 11:58:26 -0400205 }
206 }
207
208 private void setRegistered(boolean registered) {
209 if (mRegistered == registered) return;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400210 final ContentResolver cr = mSystemContext.getContentResolver();
211 final int userId = mUserContext.getUserId();
John Spurlock2f096ed2015-05-04 11:58:26 -0400212 if (mRegistered) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400213 if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400214 cr.unregisterContentObserver(mObserver);
215 }
216 mRegistered = registered;
John Spurlock1b8b22b2015-05-20 09:47:13 -0400217 if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400218 if (mRegistered) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400219 if (DEBUG) Log.d(TAG, "register content observer u=" + userId);
220 cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId);
221 cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId);
222 cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId);
John Spurlock2f096ed2015-05-04 11:58:26 -0400223 }
224 }
225
226 private static String attendeeStatusToString(int status) {
227 switch (status) {
228 case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
229 case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
230 case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
231 case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
232 case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
233 default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
234 }
235 }
236
John Spurlock028a5392015-05-06 11:36:19 -0400237 private static String availabilityToString(int availability) {
238 switch (availability) {
239 case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY";
240 case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE";
241 case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE";
242 default: return "AVAILABILITY_UNKNOWN_" + availability;
243 }
244 }
245
John Spurlock2f096ed2015-05-04 11:58:26 -0400246 private static boolean meetsReply(int reply, int attendeeStatus) {
247 switch (reply) {
248 case EventInfo.REPLY_YES:
249 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
John Spurlockd39af2d2015-05-05 09:49:32 -0400250 case EventInfo.REPLY_YES_OR_MAYBE:
251 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED
252 || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE;
John Spurlock2f096ed2015-05-04 11:58:26 -0400253 case EventInfo.REPLY_ANY_EXCEPT_NO:
254 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
John Spurlockd39af2d2015-05-05 09:49:32 -0400255 default:
256 return false;
John Spurlock2f096ed2015-05-04 11:58:26 -0400257 }
258 }
259
260 private final ContentObserver mObserver = new ContentObserver(null) {
261 @Override
262 public void onChange(boolean selfChange, Uri u) {
John Spurlock1b8b22b2015-05-20 09:47:13 -0400263 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u
264 + " u=" + mUserContext.getUserId());
John Spurlock2f096ed2015-05-04 11:58:26 -0400265 mCallback.onChanged();
266 }
267
268 @Override
269 public void onChange(boolean selfChange) {
270 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
271 }
272 };
273
274 public static class CheckEventResult {
275 public boolean inEvent;
276 public long recheckAt;
277 }
278
279 public interface Callback {
280 void onChanged();
281 }
282
283}