/*
|
* 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.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 android.util.Slog;
|
|
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> getCalendarsWithAccess() {
|
final long start = System.currentTimeMillis();
|
final ArraySet<Long> rt = new ArraySet<>();
|
final String[] projection = { Calendars._ID };
|
final String selection = Calendars.CALENDAR_ACCESS_LEVEL + " >= "
|
+ Calendars.CAL_ACCESS_CONTRIBUTOR
|
+ " AND " + Calendars.SYNC_EVENTS + " = 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, "getCalendarsWithAccess 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> calendars = getCalendarsWithAccess();
|
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 canAccessCal = calendars.contains(calendarId);
|
if (DEBUG) {
|
Log.d(TAG, String.format("title=%s time=%s-%s vis=%s availability=%s "
|
+ "eventId=%s name=%s owner=%s calId=%s canAccessCal=%s",
|
title, new Date(begin), new Date(end), calendarVisible,
|
availabilityToString(availability), eventId, name, owner, calendarId,
|
canAccessCal));
|
}
|
final boolean meetsTime = time >= begin && time < end;
|
final boolean meetsCalendar = calendarVisible && canAccessCal
|
&& ((filter.calName == null && filter.calendarId == null)
|
|| (Objects.equals(filter.calendarId, calendarId))
|
|| Objects.equals(filter.calName, 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;
|
}
|
}
|
}
|
}
|
} catch (Exception e) {
|
Slog.w(TAG, "error reading calendar", e);
|
} 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();
|
}
|
|
}
|