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 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.Calendars; 28import android.provider.CalendarContract.Events; 29import android.provider.CalendarContract.Instances; 30import android.service.notification.ZenModeConfig.EventInfo; 31import android.util.ArraySet; 32import android.util.Log; 33import android.util.Slog; 34 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 = { 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 75 public CalendarTracker(Context systemContext, Context userContext) { 76 mSystemContext = systemContext; 77 mUserContext = userContext; 78 } 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); 89 pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId()); 90 } 91 92 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 } 114 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(); 120 final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION, 121 null, null, INSTANCE_ORDER_BY); 122 final CheckEventResult result = new CheckEventResult(); 123 result.recheckAt = time + EVENT_CHECK_LOOKAHEAD; 124 try { 125 final ArraySet<Long> primaryCalendars = getPrimaryCalendars(); 126 while (cursor != null && cursor.moveToNext()) { 127 final long begin = cursor.getLong(0); 128 final long end = cursor.getLong(1); 129 final String title = cursor.getString(2); 130 final boolean calendarVisible = cursor.getInt(3) == 1; 131 final int eventId = cursor.getInt(4); 132 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); 136 final boolean calendarPrimary = primaryCalendars.contains(calendarId); 137 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", 139 title, 140 new Date(begin), new Date(end), calendarVisible, 141 availabilityToString(availability), eventId, name, owner, calendarId, 142 calendarPrimary)); 143 final boolean meetsTime = time >= begin && time < end; 144 final boolean meetsCalendar = calendarVisible && calendarPrimary 145 && (filter.calendar == null || Objects.equals(filter.calendar, owner) 146 || Objects.equals(filter.calendar, name)); 147 final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE; 148 if (meetsCalendar && meetsAvailability) { 149 if (DEBUG) Log.d(TAG, " MEETS CALENDAR & AVAILABILITY"); 150 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 } 165 } catch (Exception e) { 166 Slog.w(TAG, "error reading calendar", e); 167 } finally { 168 if (cursor != null) { 169 cursor.close(); 170 } 171 } 172 return result; 173 } 174 175 private boolean meetsAttendee(EventInfo filter, int eventId, String email) { 176 final long start = System.currentTimeMillis(); 177 String selection = ATTENDEE_SELECTION; 178 String[] selectionArgs = { Integer.toString(eventId), email }; 179 if (DEBUG_ATTENDEES) { 180 selection = null; 181 selectionArgs = null; 182 } 183 final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI, 184 ATTENDEE_PROJECTION, selection, selectionArgs, null); 185 try { 186 if (cursor == null || cursor.getCount() == 0) { 187 if (DEBUG) Log.d(TAG, "No attendees found"); 188 return true; 189 } 190 boolean rt = false; 191 while (cursor != null && cursor.moveToNext()) { 192 final long rowEventId = cursor.getLong(0); 193 final String rowEmail = cursor.getString(1); 194 final int status = cursor.getInt(2); 195 final boolean meetsReply = meetsReply(filter.reply, status); 196 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format( 197 "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") + 198 String.format("status=%s, meetsReply=%s", 199 attendeeStatusToString(status), meetsReply)); 200 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email) 201 && meetsReply; 202 rt |= eventMeets; 203 } 204 return rt; 205 } finally { 206 if (cursor != null) { 207 cursor.close(); 208 } 209 if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start)); 210 } 211 } 212 213 private void setRegistered(boolean registered) { 214 if (mRegistered == registered) return; 215 final ContentResolver cr = mSystemContext.getContentResolver(); 216 final int userId = mUserContext.getUserId(); 217 if (mRegistered) { 218 if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId); 219 cr.unregisterContentObserver(mObserver); 220 } 221 mRegistered = registered; 222 if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId); 223 if (mRegistered) { 224 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); 228 } 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 242 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 251 private static boolean meetsReply(int reply, int attendeeStatus) { 252 switch (reply) { 253 case EventInfo.REPLY_YES: 254 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED; 255 case EventInfo.REPLY_YES_OR_MAYBE: 256 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED 257 || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE; 258 case EventInfo.REPLY_ANY_EXCEPT_NO: 259 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED; 260 default: 261 return false; 262 } 263 } 264 265 private final ContentObserver mObserver = new ContentObserver(null) { 266 @Override 267 public void onChange(boolean selfChange, Uri u) { 268 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u 269 + " u=" + mUserContext.getUserId()); 270 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} 289