Merge "Zen: Implement calendar event system condition provider." into mnc-dev

This commit is contained in:
John Spurlock
2015-05-04 19:07:29 +00:00
committed by Android (Google) Code Review
6 changed files with 430 additions and 32 deletions

View File

@@ -0,0 +1,289 @@
/*
* 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.Instances;
import android.service.notification.ZenModeConfig.EventInfo;
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.OWNER_ACCOUNT,
Instances.CALENDAR_ID,
};
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,
Attendees.ATTENDEE_TYPE,
};
private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
+ Attendees.ATTENDEE_EMAIL + " = ?";
private final Context mContext;
private Callback mCallback;
private boolean mRegistered;
public CalendarTracker(Context context) {
mContext = context;
}
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);
}
public void dumpContent(Uri uri) {
Log.d(TAG, "dumpContent: " + uri);
final Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null);
try {
int r = 0;
while (cursor.moveToNext()) {
Log.d(TAG, "Row " + (++r) + ": id="
+ cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)));
for (int i = 0; i < cursor.getColumnCount(); i++) {
final String name = cursor.getColumnName(i);
final int type = cursor.getType(i);
Object o = null;
String typeName = null;
switch (type) {
case Cursor.FIELD_TYPE_INTEGER:
o = cursor.getLong(i);
typeName = "INTEGER";
break;
case Cursor.FIELD_TYPE_STRING:
o = cursor.getString(i);
typeName = "STRING";
break;
case Cursor.FIELD_TYPE_NULL:
o = null;
typeName = "NULL";
break;
default:
throw new UnsupportedOperationException("type: " + type);
}
if (name.equals(BaseColumns._ID)
|| name.toLowerCase().contains("sync")
|| o == null) {
continue;
}
Log.d(TAG, " " + name + "(" + typeName + ")=" + o);
}
}
Log.d(TAG, " " + uri + " " + r + " rows");
} finally {
cursor.close();
}
}
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 = mContext.getContentResolver().query(uri, INSTANCE_PROJECTION, null,
null, INSTANCE_ORDER_BY);
final CheckEventResult result = new CheckEventResult();
result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
try {
while (cursor.moveToNext()) {
final long begin = cursor.getLong(0);
final long end = cursor.getLong(1);
final String title = cursor.getString(2);
final boolean visible = cursor.getInt(3) == 1;
final int eventId = cursor.getInt(4);
final String owner = cursor.getString(5);
final long calendarId = cursor.getLong(6);
if (DEBUG) Log.d(TAG, String.format("%s %s-%s v=%s eid=%s o=%s cid=%s", title,
new Date(begin), new Date(end), visible, eventId, owner, calendarId));
final boolean meetsTime = time >= begin && time < end;
final boolean meetsCalendar = visible
&& (filter.calendar == 0 || filter.calendar == calendarId);
if (meetsCalendar) {
if (DEBUG) Log.d(TAG, " MEETS CALENDAR");
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 {
cursor.close();
}
return result;
}
private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
String selection = ATTENDEE_SELECTION;
String[] selectionArgs = { Integer.toString(eventId), email };
if (DEBUG_ATTENDEES) {
selection = null;
selectionArgs = null;
}
final Cursor cursor = mContext.getContentResolver().query(Attendees.CONTENT_URI,
ATTENDEE_PROJECTION, selection, selectionArgs, null);
try {
if (cursor.getCount() == 0) {
if (DEBUG) Log.d(TAG, "No attendees found");
return true;
}
boolean rt = false;
while (cursor.moveToNext()) {
final long rowEventId = cursor.getLong(0);
final String rowEmail = cursor.getString(1);
final int status = cursor.getInt(2);
final int type = cursor.getInt(3);
final boolean meetsReply = meetsReply(filter.reply, status);
final boolean meetsAttendance = meetsAttendance(filter.attendance, type);
if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
"rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
String.format("status=%s, type=%s, meetsReply=%s, meetsAttendance=%s",
attendeeStatusToString(status), attendeeTypeToString(type), meetsReply,
meetsAttendance));
final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
&& meetsReply && meetsAttendance;
rt |= eventMeets;
}
return rt;
} finally {
cursor.close();
}
}
private void setRegistered(boolean registered) {
if (mRegistered == registered) return;
final ContentResolver cr = mContext.getContentResolver();
if (mRegistered) {
cr.unregisterContentObserver(mObserver);
}
mRegistered = registered;
if (mRegistered) {
cr.registerContentObserver(Instances.CONTENT_URI, false, mObserver);
}
}
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 attendeeTypeToString(int type) {
switch (type) {
case Attendees.TYPE_NONE: return "TYPE_NONE";
case Attendees.TYPE_REQUIRED: return "TYPE_REQUIRED";
case Attendees.TYPE_OPTIONAL: return "TYPE_OPTIONAL";
case Attendees.TYPE_RESOURCE: return "TYPE_RESOURCE";
default: return "TYPE_" + type;
}
}
private static boolean meetsAttendance(int attendance, int attendeeType) {
switch (attendance) {
case EventInfo.ATTENDANCE_OPTIONAL:
return attendeeType == Attendees.TYPE_OPTIONAL;
case EventInfo.ATTENDANCE_REQUIRED:
return attendeeType == Attendees.TYPE_REQUIRED;
default: // EventInfo.ATTENDANCE_REQUIRED_OR_OPTIONAL
return true;
}
}
private static boolean meetsReply(int reply, int attendeeStatus) {
switch (reply) {
case EventInfo.REPLY_YES:
return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
case EventInfo.REPLY_ANY_EXCEPT_NO:
return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
default: // EventInfo.REPLY_ANY
return true;
}
}
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);
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();
}
}

View File

@@ -121,6 +121,9 @@ public class ConditionProviders extends ManagedServices {
@Override
public void onBootPhaseAppsCanStart() {
super.onBootPhaseAppsCanStart();
for (int i = 0; i < mSystemConditionProviders.size(); i++) {
mSystemConditionProviders.valueAt(i).onBootComplete();
}
if (mCallback != null) {
mCallback.onBootComplete();
}

View File

@@ -34,12 +34,11 @@ import android.util.Slog;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.util.Date;
/** Built-in zen condition provider for simple time-based conditions */
public class CountdownConditionProvider extends SystemConditionProviderService {
private static final String TAG = "ConditionProviders";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String TAG = "ConditionProviders.CCP";
private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
public static final ComponentName COMPONENT =
new ComponentName("android", CountdownConditionProvider.class.getName());
@@ -73,6 +72,11 @@ public class CountdownConditionProvider extends SystemConditionProviderService {
attachBaseContext(base);
}
@Override
public void onBootComplete() {
// noop
}
@Override
public IConditionProvider asInterface() {
return (IConditionProvider) onBind(null);
@@ -170,8 +174,4 @@ public class CountdownConditionProvider extends SystemConditionProviderService {
ts(time), time - now, span, ts(now));
}
private static String ts(long time) {
return new Date(time) + " (" + time + ")";
}
}

View File

@@ -16,16 +16,23 @@
package com.android.server.notification;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.service.notification.Condition;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.EventInfo;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import com.android.server.notification.CalendarTracker.CheckEventResult;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
@@ -34,20 +41,27 @@ import java.io.PrintWriter;
* Built-in zen condition provider for calendar event-based conditions.
*/
public class EventConditionProvider extends SystemConditionProviderService {
private static final String TAG = "ConditionProviders";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String TAG = "ConditionProviders.ECP";
private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
public static final ComponentName COMPONENT =
new ComponentName("android", EventConditionProvider.class.getName());
private static final String NOT_SHOWN = "...";
private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
private static final int REQUEST_CODE_EVALUATE = 1;
private static final String EXTRA_TIME = "time";
private final Context mContext = this;
private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
private final CalendarTracker mTracker = new CalendarTracker(mContext);
private boolean mConnected;
private boolean mRegistered;
private boolean mBootComplete; // don't hammer the calendar provider until boot completes.
public EventConditionProvider() {
if (DEBUG) Slog.d(TAG, "new EventConditionProvider()");
if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
}
@Override
@@ -62,14 +76,25 @@ public class EventConditionProvider extends SystemConditionProviderService {
@Override
public void dump(PrintWriter pw, DumpFilter filter) {
pw.println(" EventConditionProvider:");
pw.print(" "); pw.print(SIMPLE_NAME); pw.println(":");
pw.print(" mConnected="); pw.println(mConnected);
pw.print(" mRegistered="); pw.println(mRegistered);
pw.print(" mBootComplete="); pw.println(mBootComplete);
pw.println(" mSubscriptions=");
for (Uri conditionId : mSubscriptions) {
pw.print(" ");
pw.println(conditionId);
}
pw.println(" mTracker=");
mTracker.dump(" ", pw);
}
@Override
public void onBootComplete() {
if (DEBUG) Slog.d(TAG, "onBootComplete");
if (mBootComplete) return;
mBootComplete = true;
evaluateSubscriptions();
}
@Override
@@ -98,8 +123,9 @@ public class EventConditionProvider extends SystemConditionProviderService {
notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
return;
}
mSubscriptions.add(conditionId);
evaluateSubscriptions();
if (mSubscriptions.add(conditionId)) {
evaluateSubscriptions();
}
}
@Override
@@ -121,9 +147,52 @@ public class EventConditionProvider extends SystemConditionProviderService {
}
private void evaluateSubscriptions() {
for (Uri conditionId : mSubscriptions) {
notifyCondition(conditionId, Condition.STATE_FALSE, "notImplemented");
if (DEBUG) Log.d(TAG, "evaluateSubscriptions");
if (!mBootComplete) {
if (DEBUG) Log.d(TAG, "Skipping evaluate before boot complete");
return;
}
final long now = System.currentTimeMillis();
mTracker.setCallback(mSubscriptions.isEmpty() ? null : mTrackerCallback);
setRegistered(!mSubscriptions.isEmpty());
long reevaluateAt = 0;
for (Uri conditionId : mSubscriptions) {
final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
if (event == null) {
notifyCondition(conditionId, Condition.STATE_FALSE, "badConditionId");
continue;
}
final CheckEventResult result = mTracker.checkEvent(event, now);
if (result.recheckAt != 0 && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
reevaluateAt = result.recheckAt;
}
if (!result.inEvent) {
notifyCondition(conditionId, Condition.STATE_FALSE, "!inEventNow");
continue;
}
notifyCondition(conditionId, Condition.STATE_TRUE, "inEventNow");
}
updateAlarm(now, reevaluateAt);
if (DEBUG) Log.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
}
private void updateAlarm(long now, long time) {
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
REQUEST_CODE_EVALUATE,
new Intent(ACTION_EVALUATE)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_TIME, time),
PendingIntent.FLAG_UPDATE_CURRENT);
alarms.cancel(pendingIntent);
if (time == 0 || time < now) {
if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
: "specified time in the past"));
return;
}
if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
ts(time), formatDuration(time - now), ts(now)));
alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
private void notifyCondition(Uri conditionId, int state, String reason) {
@@ -139,4 +208,34 @@ public class EventConditionProvider extends SystemConditionProviderService {
return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
}
private void setRegistered(boolean registered) {
if (mRegistered == registered) return;
if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
mRegistered = registered;
if (mRegistered) {
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
filter.addAction(ACTION_EVALUATE);
registerReceiver(mReceiver, filter);
} else {
unregisterReceiver(mReceiver);
}
}
private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
@Override
public void onChanged() {
if (DEBUG) Log.d(TAG, "mTrackerCallback.onChanged");
evaluateSubscriptions();
}
};
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
evaluateSubscriptions();
}
};
}

View File

@@ -31,25 +31,24 @@ import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.util.Date;
import java.util.TimeZone;
/**
* Built-in zen condition provider for daily scheduled time-based conditions.
*/
public class ScheduleConditionProvider extends SystemConditionProviderService {
private static final String TAG = "ConditionProviders";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String TAG = "ConditionProviders.SCP";
private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
public static final ComponentName COMPONENT =
new ComponentName("android", ScheduleConditionProvider.class.getName());
private static final String NOT_SHOWN = "...";
private static final String ACTION_EVALUATE = TAG + ".EVALUATE";
private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
private static final int REQUEST_CODE_EVALUATE = 1;
private static final String EXTRA_TIME = "time";
@@ -60,7 +59,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
private boolean mRegistered;
public ScheduleConditionProvider() {
if (DEBUG) Slog.d(TAG, "new ScheduleConditionProvider()");
if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
}
@Override
@@ -75,7 +74,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
@Override
public void dump(PrintWriter pw, DumpFilter filter) {
pw.println(" ScheduleConditionProvider:");
pw.print(" "); pw.print(SIMPLE_NAME); pw.println(":");
pw.print(" mConnected="); pw.println(mConnected);
pw.print(" mRegistered="); pw.println(mRegistered);
pw.println(" mSubscriptions=");
@@ -93,6 +92,11 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
mConnected = true;
}
@Override
public void onBootComplete() {
// noop
}
@Override
public void onDestroy() {
super.onDestroy();
@@ -175,16 +179,6 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
}
}
private static String ts(long time) {
return new Date(time) + " (" + time + ")";
}
private static String formatDuration(long millis) {
final StringBuilder sb = new StringBuilder();
TimeUtils.formatDuration(millis, sb);
return sb.toString();
}
private static boolean meetsSchedule(Uri conditionId, long time) {
final ScheduleCalendar cal = toScheduleCalendar(conditionId);
return cal != null && cal.isInSchedule(time);

View File

@@ -21,10 +21,12 @@ import android.content.Context;
import android.net.Uri;
import android.service.notification.ConditionProviderService;
import android.service.notification.IConditionProvider;
import android.util.TimeUtils;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.util.Date;
public abstract class SystemConditionProviderService extends ConditionProviderService {
@@ -33,4 +35,15 @@ public abstract class SystemConditionProviderService extends ConditionProviderSe
abstract public IConditionProvider asInterface();
abstract public ComponentName getComponent();
abstract public boolean isValidConditionId(Uri id);
abstract public void onBootComplete();
protected static String ts(long time) {
return new Date(time) + " (" + time + ")";
}
protected static String formatDuration(long millis) {
final StringBuilder sb = new StringBuilder();
TimeUtils.formatDuration(millis, sb);
return sb.toString();
}
}