/*
|
* 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.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.content.pm.PackageManager.NameNotFoundException;
|
import android.net.Uri;
|
import android.os.Handler;
|
import android.os.HandlerThread;
|
import android.os.Process;
|
import android.os.UserHandle;
|
import android.os.UserManager;
|
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 android.util.SparseArray;
|
|
import com.android.server.notification.CalendarTracker.CheckEventResult;
|
import com.android.server.notification.NotificationManagerService.DumpFilter;
|
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
/**
|
* Built-in zen condition provider for calendar event-based conditions.
|
*/
|
public class EventConditionProvider extends SystemConditionProviderService {
|
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 static final long CHANGE_DELAY = 2 * 1000; // coalesce chatty calendar changes
|
|
private final Context mContext = this;
|
private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
|
private final SparseArray<CalendarTracker> mTrackers = new SparseArray<>();
|
private final Handler mWorker;
|
private final HandlerThread mThread;
|
|
private boolean mConnected;
|
private boolean mRegistered;
|
private boolean mBootComplete; // don't hammer the calendar provider until boot completes.
|
private long mNextAlarmTime;
|
|
public EventConditionProvider() {
|
if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
|
mThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
|
mThread.start();
|
mWorker = new Handler(mThread.getLooper());
|
}
|
|
@Override
|
public ComponentName getComponent() {
|
return COMPONENT;
|
}
|
|
@Override
|
public boolean isValidConditionId(Uri id) {
|
return ZenModeConfig.isValidEventConditionId(id);
|
}
|
|
@Override
|
public void dump(PrintWriter pw, DumpFilter filter) {
|
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);
|
dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, System.currentTimeMillis());
|
synchronized (mSubscriptions) {
|
pw.println(" mSubscriptions=");
|
for (Uri conditionId : mSubscriptions) {
|
pw.print(" ");
|
pw.println(conditionId);
|
}
|
}
|
pw.println(" mTrackers=");
|
for (int i = 0; i < mTrackers.size(); i++) {
|
pw.print(" user="); pw.println(mTrackers.keyAt(i));
|
mTrackers.valueAt(i).dump(" ", pw);
|
}
|
}
|
|
@Override
|
public void onBootComplete() {
|
if (DEBUG) Slog.d(TAG, "onBootComplete");
|
if (mBootComplete) return;
|
mBootComplete = true;
|
final IntentFilter filter = new IntentFilter();
|
filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
|
filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
|
mContext.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
reloadTrackers();
|
}
|
}, filter);
|
reloadTrackers();
|
}
|
|
@Override
|
public void onConnected() {
|
if (DEBUG) Slog.d(TAG, "onConnected");
|
mConnected = true;
|
}
|
|
@Override
|
public void onDestroy() {
|
super.onDestroy();
|
if (DEBUG) Slog.d(TAG, "onDestroy");
|
mConnected = false;
|
}
|
|
@Override
|
public void onSubscribe(Uri conditionId) {
|
if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
|
if (!ZenModeConfig.isValidEventConditionId(conditionId)) {
|
notifyCondition(createCondition(conditionId, Condition.STATE_FALSE));
|
return;
|
}
|
synchronized (mSubscriptions) {
|
if (mSubscriptions.add(conditionId)) {
|
evaluateSubscriptions();
|
}
|
}
|
}
|
|
@Override
|
public void onUnsubscribe(Uri conditionId) {
|
if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
|
synchronized (mSubscriptions) {
|
if (mSubscriptions.remove(conditionId)) {
|
evaluateSubscriptions();
|
}
|
}
|
}
|
|
@Override
|
public void attachBase(Context base) {
|
attachBaseContext(base);
|
}
|
|
@Override
|
public IConditionProvider asInterface() {
|
return (IConditionProvider) onBind(null);
|
}
|
|
private void reloadTrackers() {
|
if (DEBUG) Slog.d(TAG, "reloadTrackers");
|
for (int i = 0; i < mTrackers.size(); i++) {
|
mTrackers.valueAt(i).setCallback(null);
|
}
|
mTrackers.clear();
|
for (UserHandle user : UserManager.get(mContext).getUserProfiles()) {
|
final Context context = user.isSystem() ? mContext : getContextForUser(mContext, user);
|
if (context == null) {
|
Slog.w(TAG, "Unable to create context for user " + user.getIdentifier());
|
continue;
|
}
|
mTrackers.put(user.getIdentifier(), new CalendarTracker(mContext, context));
|
}
|
evaluateSubscriptions();
|
}
|
|
private void evaluateSubscriptions() {
|
if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) {
|
mWorker.post(mEvaluateSubscriptionsW);
|
}
|
}
|
|
private void evaluateSubscriptionsW() {
|
if (DEBUG) Slog.d(TAG, "evaluateSubscriptions");
|
if (!mBootComplete) {
|
if (DEBUG) Slog.d(TAG, "Skipping evaluate before boot complete");
|
return;
|
}
|
final long now = System.currentTimeMillis();
|
List<Condition> conditionsToNotify = new ArrayList<>();
|
synchronized (mSubscriptions) {
|
for (int i = 0; i < mTrackers.size(); i++) {
|
mTrackers.valueAt(i).setCallback(
|
mSubscriptions.isEmpty() ? null : mTrackerCallback);
|
}
|
setRegistered(!mSubscriptions.isEmpty());
|
long reevaluateAt = 0;
|
for (Uri conditionId : mSubscriptions) {
|
final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
|
if (event == null) {
|
conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
|
continue;
|
}
|
CheckEventResult result = null;
|
if (event.calName == null) { // any calendar
|
// event could exist on any tracker
|
for (int i = 0; i < mTrackers.size(); i++) {
|
final CalendarTracker tracker = mTrackers.valueAt(i);
|
final CheckEventResult r = tracker.checkEvent(event, now);
|
if (result == null) {
|
result = r;
|
} else {
|
result.inEvent |= r.inEvent;
|
result.recheckAt = Math.min(result.recheckAt, r.recheckAt);
|
}
|
}
|
} else {
|
// event should exist on one tracker
|
final int userId = EventInfo.resolveUserId(event.userId);
|
final CalendarTracker tracker = mTrackers.get(userId);
|
if (tracker == null) {
|
Slog.w(TAG, "No calendar tracker found for user " + userId);
|
conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
|
continue;
|
}
|
result = tracker.checkEvent(event, now);
|
}
|
if (result.recheckAt != 0
|
&& (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
|
reevaluateAt = result.recheckAt;
|
}
|
if (!result.inEvent) {
|
conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
|
continue;
|
}
|
conditionsToNotify.add(createCondition(conditionId, Condition.STATE_TRUE));
|
}
|
rescheduleAlarm(now, reevaluateAt);
|
}
|
for (Condition condition : conditionsToNotify) {
|
if (condition != null) {
|
notifyCondition(condition);
|
}
|
}
|
if (DEBUG) Slog.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
|
}
|
|
private void rescheduleAlarm(long now, long time) {
|
mNextAlarmTime = 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 Condition createCondition(Uri id, int state) {
|
final String summary = NOT_SHOWN;
|
final String line1 = NOT_SHOWN;
|
final String line2 = NOT_SHOWN;
|
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 static Context getContextForUser(Context context, UserHandle user) {
|
try {
|
return context.createPackageContextAsUser(context.getPackageName(), 0, user);
|
} catch (NameNotFoundException e) {
|
return null;
|
}
|
}
|
|
private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
|
@Override
|
public void onChanged() {
|
if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged");
|
mWorker.removeCallbacks(mEvaluateSubscriptionsW);
|
mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY);
|
}
|
};
|
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
|
evaluateSubscriptions();
|
}
|
};
|
|
private final Runnable mEvaluateSubscriptionsW = new Runnable() {
|
@Override
|
public void run() {
|
evaluateSubscriptionsW();
|
}
|
};
|
}
|