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