blob: 8734d976db2e0426a55aa156d17df52474fb04bb [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;
27import android.provider.CalendarContract.Instances;
28import android.service.notification.ZenModeConfig.EventInfo;
29import android.util.Log;
30
31import java.io.PrintWriter;
32import java.util.Date;
33import java.util.Objects;
34
35public class CalendarTracker {
36 private static final String TAG = "ConditionProviders.CT";
37 private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
38 private static final boolean DEBUG_ATTENDEES = false;
39
40 private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000;
41
42 private static final String[] INSTANCE_PROJECTION = {
43 Instances.BEGIN,
44 Instances.END,
45 Instances.TITLE,
46 Instances.VISIBLE,
47 Instances.EVENT_ID,
48 Instances.OWNER_ACCOUNT,
49 Instances.CALENDAR_ID,
50 };
51
52 private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC";
53
54 private static final String[] ATTENDEE_PROJECTION = {
55 Attendees.EVENT_ID,
56 Attendees.ATTENDEE_EMAIL,
57 Attendees.ATTENDEE_STATUS,
58 Attendees.ATTENDEE_TYPE,
59 };
60
61 private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
62 + Attendees.ATTENDEE_EMAIL + " = ?";
63
64 private final Context mContext;
65
66 private Callback mCallback;
67 private boolean mRegistered;
68
69 public CalendarTracker(Context context) {
70 mContext = context;
71 }
72
73 public void setCallback(Callback callback) {
74 if (mCallback == callback) return;
75 mCallback = callback;
76 setRegistered(mCallback != null);
77 }
78
79 public void dump(String prefix, PrintWriter pw) {
80 pw.print(prefix); pw.print("mCallback="); pw.println(mCallback);
81 pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered);
82 }
83
84 public void dumpContent(Uri uri) {
85 Log.d(TAG, "dumpContent: " + uri);
86 final Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null);
87 try {
88 int r = 0;
89 while (cursor.moveToNext()) {
90 Log.d(TAG, "Row " + (++r) + ": id="
91 + cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)));
92 for (int i = 0; i < cursor.getColumnCount(); i++) {
93 final String name = cursor.getColumnName(i);
94 final int type = cursor.getType(i);
95 Object o = null;
96 String typeName = null;
97 switch (type) {
98 case Cursor.FIELD_TYPE_INTEGER:
99 o = cursor.getLong(i);
100 typeName = "INTEGER";
101 break;
102 case Cursor.FIELD_TYPE_STRING:
103 o = cursor.getString(i);
104 typeName = "STRING";
105 break;
106 case Cursor.FIELD_TYPE_NULL:
107 o = null;
108 typeName = "NULL";
109 break;
110 default:
111 throw new UnsupportedOperationException("type: " + type);
112 }
113 if (name.equals(BaseColumns._ID)
114 || name.toLowerCase().contains("sync")
115 || o == null) {
116 continue;
117 }
118 Log.d(TAG, " " + name + "(" + typeName + ")=" + o);
119 }
120 }
121 Log.d(TAG, " " + uri + " " + r + " rows");
122 } finally {
123 cursor.close();
124 }
125 }
126
127
128
129 public CheckEventResult checkEvent(EventInfo filter, long time) {
130 final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
131 ContentUris.appendId(uriBuilder, time);
132 ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
133 final Uri uri = uriBuilder.build();
134 final Cursor cursor = mContext.getContentResolver().query(uri, INSTANCE_PROJECTION, null,
135 null, INSTANCE_ORDER_BY);
136 final CheckEventResult result = new CheckEventResult();
137 result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
138 try {
139 while (cursor.moveToNext()) {
140 final long begin = cursor.getLong(0);
141 final long end = cursor.getLong(1);
142 final String title = cursor.getString(2);
143 final boolean visible = cursor.getInt(3) == 1;
144 final int eventId = cursor.getInt(4);
145 final String owner = cursor.getString(5);
146 final long calendarId = cursor.getLong(6);
147 if (DEBUG) Log.d(TAG, String.format("%s %s-%s v=%s eid=%s o=%s cid=%s", title,
148 new Date(begin), new Date(end), visible, eventId, owner, calendarId));
149 final boolean meetsTime = time >= begin && time < end;
150 final boolean meetsCalendar = visible
151 && (filter.calendar == 0 || filter.calendar == calendarId);
152 if (meetsCalendar) {
153 if (DEBUG) Log.d(TAG, " MEETS CALENDAR");
154 final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
155 if (meetsAttendee) {
156 if (DEBUG) Log.d(TAG, " MEETS ATTENDEE");
157 if (meetsTime) {
158 if (DEBUG) Log.d(TAG, " MEETS TIME");
159 result.inEvent = true;
160 }
161 if (begin > time && begin < result.recheckAt) {
162 result.recheckAt = begin;
163 } else if (end > time && end < result.recheckAt) {
164 result.recheckAt = end;
165 }
166 }
167 }
168 }
169 } finally {
170 cursor.close();
171 }
172 return result;
173 }
174
175 private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
176 String selection = ATTENDEE_SELECTION;
177 String[] selectionArgs = { Integer.toString(eventId), email };
178 if (DEBUG_ATTENDEES) {
179 selection = null;
180 selectionArgs = null;
181 }
182 final Cursor cursor = mContext.getContentResolver().query(Attendees.CONTENT_URI,
183 ATTENDEE_PROJECTION, selection, selectionArgs, null);
184 try {
185 if (cursor.getCount() == 0) {
186 if (DEBUG) Log.d(TAG, "No attendees found");
187 return true;
188 }
189 boolean rt = false;
190 while (cursor.moveToNext()) {
191 final long rowEventId = cursor.getLong(0);
192 final String rowEmail = cursor.getString(1);
193 final int status = cursor.getInt(2);
194 final int type = cursor.getInt(3);
195 final boolean meetsReply = meetsReply(filter.reply, status);
196 final boolean meetsAttendance = meetsAttendance(filter.attendance, type);
197 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
198 "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
199 String.format("status=%s, type=%s, meetsReply=%s, meetsAttendance=%s",
200 attendeeStatusToString(status), attendeeTypeToString(type), meetsReply,
201 meetsAttendance));
202 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
203 && meetsReply && meetsAttendance;
204 rt |= eventMeets;
205 }
206 return rt;
207 } finally {
208 cursor.close();
209 }
210 }
211
212 private void setRegistered(boolean registered) {
213 if (mRegistered == registered) return;
214 final ContentResolver cr = mContext.getContentResolver();
215 if (mRegistered) {
216 cr.unregisterContentObserver(mObserver);
217 }
218 mRegistered = registered;
219 if (mRegistered) {
220 cr.registerContentObserver(Instances.CONTENT_URI, false, mObserver);
221 }
222 }
223
224 private static String attendeeStatusToString(int status) {
225 switch (status) {
226 case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
227 case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
228 case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
229 case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
230 case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
231 default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
232 }
233 }
234
235 private static String attendeeTypeToString(int type) {
236 switch (type) {
237 case Attendees.TYPE_NONE: return "TYPE_NONE";
238 case Attendees.TYPE_REQUIRED: return "TYPE_REQUIRED";
239 case Attendees.TYPE_OPTIONAL: return "TYPE_OPTIONAL";
240 case Attendees.TYPE_RESOURCE: return "TYPE_RESOURCE";
241 default: return "TYPE_" + type;
242 }
243 }
244
245 private static boolean meetsAttendance(int attendance, int attendeeType) {
246 switch (attendance) {
247 case EventInfo.ATTENDANCE_OPTIONAL:
248 return attendeeType == Attendees.TYPE_OPTIONAL;
249 case EventInfo.ATTENDANCE_REQUIRED:
250 return attendeeType == Attendees.TYPE_REQUIRED;
251 default: // EventInfo.ATTENDANCE_REQUIRED_OR_OPTIONAL
252 return true;
253 }
254 }
255
256 private static boolean meetsReply(int reply, int attendeeStatus) {
257 switch (reply) {
258 case EventInfo.REPLY_YES:
259 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
260 case EventInfo.REPLY_ANY_EXCEPT_NO:
261 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
262 default: // EventInfo.REPLY_ANY
263 return true;
264 }
265 }
266
267 private final ContentObserver mObserver = new ContentObserver(null) {
268 @Override
269 public void onChange(boolean selfChange, Uri u) {
270 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u);
271 mCallback.onChanged();
272 }
273
274 @Override
275 public void onChange(boolean selfChange) {
276 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
277 }
278 };
279
280 public static class CheckEventResult {
281 public boolean inEvent;
282 public long recheckAt;
283 }
284
285 public interface Callback {
286 void onChanged();
287 }
288
289}