| /* | |
| * Copyright (C) 2015 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.server.notification; | |
| import android.content.ContentResolver; | |
| import android.content.ContentUris; | |
| import android.content.Context; | |
| import android.database.ContentObserver; | |
| import android.database.Cursor; | |
| import android.net.Uri; | |
| import android.provider.BaseColumns; | |
| import android.provider.CalendarContract.Attendees; | |
| import android.provider.CalendarContract.Calendars; | |
| import android.provider.CalendarContract.Events; | |
| import android.provider.CalendarContract.Instances; | |
| import android.service.notification.ZenModeConfig.EventInfo; | |
| import android.util.ArraySet; | |
| import android.util.Log; | |
| import java.io.PrintWriter; | |
| import java.util.Date; | |
| import java.util.Objects; | |
| public class CalendarTracker { | |
| private static final String TAG = "ConditionProviders.CT"; | |
| private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG); | |
| private static final boolean DEBUG_ATTENDEES = false; | |
| private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000; | |
| private static final String[] INSTANCE_PROJECTION = { | |
| Instances.BEGIN, | |
| Instances.END, | |
| Instances.TITLE, | |
| Instances.VISIBLE, | |
| Instances.EVENT_ID, | |
| Instances.CALENDAR_DISPLAY_NAME, | |
| Instances.OWNER_ACCOUNT, | |
| Instances.CALENDAR_ID, | |
| Instances.AVAILABILITY, | |
| }; | |
| private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC"; | |
| private static final String[] ATTENDEE_PROJECTION = { | |
| Attendees.EVENT_ID, | |
| Attendees.ATTENDEE_EMAIL, | |
| Attendees.ATTENDEE_STATUS, | |
| }; | |
| private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND " | |
| + Attendees.ATTENDEE_EMAIL + " = ?"; | |
| private final Context mSystemContext; | |
| private final Context mUserContext; | |
| private Callback mCallback; | |
| private boolean mRegistered; | |
| public CalendarTracker(Context systemContext, Context userContext) { | |
| mSystemContext = systemContext; | |
| mUserContext = userContext; | |
| } | |
| public void setCallback(Callback callback) { | |
| if (mCallback == callback) return; | |
| mCallback = callback; | |
| setRegistered(mCallback != null); | |
| } | |
| public void dump(String prefix, PrintWriter pw) { | |
| pw.print(prefix); pw.print("mCallback="); pw.println(mCallback); | |
| pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered); | |
| pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId()); | |
| } | |
| private ArraySet<Long> getPrimaryCalendars() { | |
| final long start = System.currentTimeMillis(); | |
| final ArraySet<Long> rt = new ArraySet<>(); | |
| final String primary = "\"primary\""; | |
| final String[] projection = { Calendars._ID, | |
| "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + primary }; | |
| final String selection = primary + " = 1"; | |
| Cursor cursor = null; | |
| try { | |
| cursor = mUserContext.getContentResolver().query(Calendars.CONTENT_URI, projection, | |
| selection, null, null); | |
| while (cursor != null && cursor.moveToNext()) { | |
| rt.add(cursor.getLong(0)); | |
| } | |
| } finally { | |
| if (cursor != null) { | |
| cursor.close(); | |
| } | |
| } | |
| if (DEBUG) Log.d(TAG, "getPrimaryCalendars took " + (System.currentTimeMillis() - start)); | |
| return rt; | |
| } | |
| public CheckEventResult checkEvent(EventInfo filter, long time) { | |
| final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon(); | |
| ContentUris.appendId(uriBuilder, time); | |
| ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD); | |
| final Uri uri = uriBuilder.build(); | |
| final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION, | |
| null, null, INSTANCE_ORDER_BY); | |
| final CheckEventResult result = new CheckEventResult(); | |
| result.recheckAt = time + EVENT_CHECK_LOOKAHEAD; | |
| try { | |
| final ArraySet<Long> primaryCalendars = getPrimaryCalendars(); | |
| while (cursor != null && cursor.moveToNext()) { | |
| final long begin = cursor.getLong(0); | |
| final long end = cursor.getLong(1); | |
| final String title = cursor.getString(2); | |
| final boolean calendarVisible = cursor.getInt(3) == 1; | |
| final int eventId = cursor.getInt(4); | |
| final String name = cursor.getString(5); | |
| final String owner = cursor.getString(6); | |
| final long calendarId = cursor.getLong(7); | |
| final int availability = cursor.getInt(8); | |
| final boolean calendarPrimary = primaryCalendars.contains(calendarId); | |
| if (DEBUG) Log.d(TAG, String.format( | |
| "%s %s-%s v=%s a=%s eid=%s n=%s o=%s cid=%s p=%s", | |
| title, | |
| new Date(begin), new Date(end), calendarVisible, | |
| availabilityToString(availability), eventId, name, owner, calendarId, | |
| calendarPrimary)); | |
| final boolean meetsTime = time >= begin && time < end; | |
| final boolean meetsCalendar = calendarVisible && calendarPrimary | |
| && (filter.calendar == null || Objects.equals(filter.calendar, owner) | |
| || Objects.equals(filter.calendar, name)); | |
| final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE; | |
| if (meetsCalendar && meetsAvailability) { | |
| if (DEBUG) Log.d(TAG, " MEETS CALENDAR & AVAILABILITY"); | |
| final boolean meetsAttendee = meetsAttendee(filter, eventId, owner); | |
| if (meetsAttendee) { | |
| if (DEBUG) Log.d(TAG, " MEETS ATTENDEE"); | |
| if (meetsTime) { | |
| if (DEBUG) Log.d(TAG, " MEETS TIME"); | |
| result.inEvent = true; | |
| } | |
| if (begin > time && begin < result.recheckAt) { | |
| result.recheckAt = begin; | |
| } else if (end > time && end < result.recheckAt) { | |
| result.recheckAt = end; | |
| } | |
| } | |
| } | |
| } | |
| } finally { | |
| if (cursor != null) { | |
| cursor.close(); | |
| } | |
| } | |
| return result; | |
| } | |
| private boolean meetsAttendee(EventInfo filter, int eventId, String email) { | |
| final long start = System.currentTimeMillis(); | |
| String selection = ATTENDEE_SELECTION; | |
| String[] selectionArgs = { Integer.toString(eventId), email }; | |
| if (DEBUG_ATTENDEES) { | |
| selection = null; | |
| selectionArgs = null; | |
| } | |
| final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI, | |
| ATTENDEE_PROJECTION, selection, selectionArgs, null); | |
| try { | |
| if (cursor == null || cursor.getCount() == 0) { | |
| if (DEBUG) Log.d(TAG, "No attendees found"); | |
| return true; | |
| } | |
| boolean rt = false; | |
| while (cursor != null && cursor.moveToNext()) { | |
| final long rowEventId = cursor.getLong(0); | |
| final String rowEmail = cursor.getString(1); | |
| final int status = cursor.getInt(2); | |
| final boolean meetsReply = meetsReply(filter.reply, status); | |
| if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format( | |
| "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") + | |
| String.format("status=%s, meetsReply=%s", | |
| attendeeStatusToString(status), meetsReply)); | |
| final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email) | |
| && meetsReply; | |
| rt |= eventMeets; | |
| } | |
| return rt; | |
| } finally { | |
| if (cursor != null) { | |
| cursor.close(); | |
| } | |
| if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start)); | |
| } | |
| } | |
| private void setRegistered(boolean registered) { | |
| if (mRegistered == registered) return; | |
| final ContentResolver cr = mSystemContext.getContentResolver(); | |
| final int userId = mUserContext.getUserId(); | |
| if (mRegistered) { | |
| if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId); | |
| cr.unregisterContentObserver(mObserver); | |
| } | |
| mRegistered = registered; | |
| if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId); | |
| if (mRegistered) { | |
| if (DEBUG) Log.d(TAG, "register content observer u=" + userId); | |
| cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId); | |
| cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId); | |
| cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId); | |
| } | |
| } | |
| private static String attendeeStatusToString(int status) { | |
| switch (status) { | |
| case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE"; | |
| case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED"; | |
| case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED"; | |
| case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED"; | |
| case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE"; | |
| default: return "ATTENDEE_STATUS_UNKNOWN_" + status; | |
| } | |
| } | |
| private static String availabilityToString(int availability) { | |
| switch (availability) { | |
| case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY"; | |
| case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE"; | |
| case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE"; | |
| default: return "AVAILABILITY_UNKNOWN_" + availability; | |
| } | |
| } | |
| private static boolean meetsReply(int reply, int attendeeStatus) { | |
| switch (reply) { | |
| case EventInfo.REPLY_YES: | |
| return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED; | |
| case EventInfo.REPLY_YES_OR_MAYBE: | |
| return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED | |
| || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE; | |
| case EventInfo.REPLY_ANY_EXCEPT_NO: | |
| return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED; | |
| default: | |
| return false; | |
| } | |
| } | |
| private final ContentObserver mObserver = new ContentObserver(null) { | |
| @Override | |
| public void onChange(boolean selfChange, Uri u) { | |
| if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u | |
| + " u=" + mUserContext.getUserId()); | |
| mCallback.onChanged(); | |
| } | |
| @Override | |
| public void onChange(boolean selfChange) { | |
| if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange); | |
| } | |
| }; | |
| public static class CheckEventResult { | |
| public boolean inEvent; | |
| public long recheckAt; | |
| } | |
| public interface Callback { | |
| void onChanged(); | |
| } | |
| } |