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; 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 = { 46 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, 55 }; 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, 63 }; 64 65 private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND " 66 + Attendees.ATTENDEE_EMAIL + " = ?"; 67 68 private final Context mSystemContext; 69 private final Context mUserContext; 70 71 private Callback mCallback; 72 private boolean mRegistered; 73 74 public CalendarTracker(Context systemContext, Context userContext) { 75 mSystemContext = systemContext; 76 mUserContext = userContext; 77 } 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); 88 pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId()); 89 } 90 91 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 } 113 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(); 119 final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION, 120 null, null, INSTANCE_ORDER_BY); 121 final CheckEventResult result = new CheckEventResult(); 122 result.recheckAt = time + EVENT_CHECK_LOOKAHEAD; 123 try { 124 final ArraySet<Long> primaryCalendars = getPrimaryCalendars(); 125 while (cursor != null && cursor.moveToNext()) { 126 final long begin = cursor.getLong(0); 127 final long end = cursor.getLong(1); 128 final String title = cursor.getString(2); 129 final boolean calendarVisible = cursor.getInt(3) == 1; 130 final int eventId = cursor.getInt(4); 131 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); 135 final boolean calendarPrimary = primaryCalendars.contains(calendarId); 136 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", 138 title, 139 new Date(begin), new Date(end), calendarVisible, 140 availabilityToString(availability), eventId, name, owner, calendarId, 141 calendarPrimary)); 142 final boolean meetsTime = time >= begin && time < end; 143 final boolean meetsCalendar = calendarVisible && calendarPrimary 144 && (filter.calendar == null || Objects.equals(filter.calendar, owner) 145 || Objects.equals(filter.calendar, name)); 146 final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE; 147 if (meetsCalendar && meetsAvailability) { 148 if (DEBUG) Log.d(TAG, " MEETS CALENDAR & AVAILABILITY"); 149 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 { 165 if (cursor != null) { 166 cursor.close(); 167 } 168 } 169 return result; 170 } 171 172 private boolean meetsAttendee(EventInfo filter, int eventId, String email) { 173 final long start = System.currentTimeMillis(); 174 String selection = ATTENDEE_SELECTION; 175 String[] selectionArgs = { Integer.toString(eventId), email }; 176 if (DEBUG_ATTENDEES) { 177 selection = null; 178 selectionArgs = null; 179 } 180 final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI, 181 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); 192 final boolean meetsReply = meetsReply(filter.reply, status); 193 if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format( 194 "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") + 195 String.format("status=%s, meetsReply=%s", 196 attendeeStatusToString(status), meetsReply)); 197 final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email) 198 && meetsReply; 199 rt |= eventMeets; 200 } 201 return rt; 202 } finally { 203 cursor.close(); 204 if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start)); 205 } 206 } 207 208 private void setRegistered(boolean registered) { 209 if (mRegistered == registered) return; 210 final ContentResolver cr = mSystemContext.getContentResolver(); 211 final int userId = mUserContext.getUserId(); 212 if (mRegistered) { 213 if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId); 214 cr.unregisterContentObserver(mObserver); 215 } 216 mRegistered = registered; 217 if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId); 218 if (mRegistered) { 219 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); 223 } 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 237 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 246 private static boolean meetsReply(int reply, int attendeeStatus) { 247 switch (reply) { 248 case EventInfo.REPLY_YES: 249 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED; 250 case EventInfo.REPLY_YES_OR_MAYBE: 251 return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED 252 || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE; 253 case EventInfo.REPLY_ANY_EXCEPT_NO: 254 return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED; 255 default: 256 return false; 257 } 258 } 259 260 private final ContentObserver mObserver = new ContentObserver(null) { 261 @Override 262 public void onChange(boolean selfChange, Uri u) { 263 if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u 264 + " u=" + mUserContext.getUserId()); 265 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} 284