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