/*
|
* 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.ActivityManager;
|
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.os.Binder;
|
import android.provider.Settings;
|
import android.service.notification.Condition;
|
import android.service.notification.IConditionProvider;
|
import android.service.notification.ScheduleCalendar;
|
import android.service.notification.ZenModeConfig;
|
import android.text.TextUtils;
|
import android.util.ArrayMap;
|
import android.util.ArraySet;
|
import android.util.Log;
|
import android.util.Slog;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.server.notification.NotificationManagerService.DumpFilter;
|
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Calendar;
|
import java.util.List;
|
|
/**
|
* Built-in zen condition provider for daily scheduled time-based conditions.
|
*/
|
public class ScheduleConditionProvider extends SystemConditionProviderService {
|
static final String TAG = "ConditionProviders.SCP";
|
static final boolean DEBUG = true || 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 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";
|
private static final String SEPARATOR = ";";
|
private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
|
|
private final Context mContext = this;
|
private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
|
private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
|
|
private AlarmManager mAlarmManager;
|
private boolean mConnected;
|
private boolean mRegistered;
|
private long mNextAlarmTime;
|
|
public ScheduleConditionProvider() {
|
if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
|
}
|
|
@Override
|
public ComponentName getComponent() {
|
return COMPONENT;
|
}
|
|
@Override
|
public boolean isValidConditionId(Uri id) {
|
return ZenModeConfig.isValidScheduleConditionId(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.println(" mSubscriptions=");
|
final long now = System.currentTimeMillis();
|
synchronized (mSubscriptions) {
|
for (Uri conditionId : mSubscriptions.keySet()) {
|
pw.print(" ");
|
pw.print(meetsSchedule(mSubscriptions.get(conditionId), now) ? "* " : " ");
|
pw.println(conditionId);
|
pw.print(" ");
|
pw.println(mSubscriptions.get(conditionId).toString());
|
}
|
}
|
pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
|
dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
|
}
|
|
@Override
|
public void onConnected() {
|
if (DEBUG) Slog.d(TAG, "onConnected");
|
mConnected = true;
|
readSnoozed();
|
}
|
|
@Override
|
public void onBootComplete() {
|
// noop
|
}
|
|
@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.isValidScheduleConditionId(conditionId)) {
|
notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
|
return;
|
}
|
synchronized (mSubscriptions) {
|
mSubscriptions.put(conditionId, ZenModeConfig.toScheduleCalendar(conditionId));
|
}
|
evaluateSubscriptions();
|
}
|
|
@Override
|
public void onUnsubscribe(Uri conditionId) {
|
if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
|
synchronized (mSubscriptions) {
|
mSubscriptions.remove(conditionId);
|
}
|
removeSnoozed(conditionId);
|
evaluateSubscriptions();
|
}
|
|
@Override
|
public void attachBase(Context base) {
|
attachBaseContext(base);
|
}
|
|
@Override
|
public IConditionProvider asInterface() {
|
return (IConditionProvider) onBind(null);
|
}
|
|
private void evaluateSubscriptions() {
|
if (mAlarmManager == null) {
|
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
|
}
|
final long now = System.currentTimeMillis();
|
mNextAlarmTime = 0;
|
long nextUserAlarmTime = getNextAlarm();
|
List<Condition> conditionsToNotify = new ArrayList<>();
|
synchronized (mSubscriptions) {
|
setRegistered(!mSubscriptions.isEmpty());
|
for (Uri conditionId : mSubscriptions.keySet()) {
|
Condition condition =
|
evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
|
now, nextUserAlarmTime);
|
if (condition != null) {
|
conditionsToNotify.add(condition);
|
}
|
}
|
}
|
notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
|
updateAlarm(now, mNextAlarmTime);
|
}
|
|
@VisibleForTesting
|
@GuardedBy("mSubscriptions")
|
Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
|
long now, long nextUserAlarmTime) {
|
if (DEBUG) Slog.d(TAG, String.format("evaluateSubscriptionLocked cal=%s, now=%s, "
|
+ "nextUserAlarmTime=%s", cal, ts(now), ts(nextUserAlarmTime)));
|
Condition condition;
|
if (cal == null) {
|
condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
|
removeSnoozed(conditionId);
|
return condition;
|
}
|
if (cal.isInSchedule(now)) {
|
if (conditionSnoozed(conditionId)) {
|
condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
|
} else if (cal.shouldExitForAlarm(now)) {
|
condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
|
addSnoozed(conditionId);
|
} else {
|
condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
|
}
|
} else {
|
condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
|
removeSnoozed(conditionId);
|
}
|
cal.maybeSetNextAlarm(now, nextUserAlarmTime);
|
final long nextChangeTime = cal.getNextChangeTime(now);
|
if (nextChangeTime > 0 && nextChangeTime > now) {
|
if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
|
mNextAlarmTime = nextChangeTime;
|
}
|
}
|
return condition;
|
}
|
|
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 > now) {
|
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);
|
} else {
|
if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
|
}
|
}
|
|
public long getNextAlarm() {
|
final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
|
ActivityManager.getCurrentUser());
|
return info != null ? info.getTriggerTime() : 0;
|
}
|
|
private boolean meetsSchedule(ScheduleCalendar cal, long time) {
|
return cal != null && cal.isInSchedule(time);
|
}
|
|
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);
|
filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
|
registerReceiver(mReceiver, filter);
|
} else {
|
unregisterReceiver(mReceiver);
|
}
|
}
|
|
private Condition createCondition(Uri id, int state, String reason) {
|
if (DEBUG) Slog.d(TAG, "notifyCondition " + id
|
+ " " + Condition.stateToString(state)
|
+ " reason=" + reason);
|
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 boolean conditionSnoozed(Uri conditionId) {
|
synchronized (mSnoozedForAlarm) {
|
return mSnoozedForAlarm.contains(conditionId);
|
}
|
}
|
|
@VisibleForTesting
|
void addSnoozed(Uri conditionId) {
|
synchronized (mSnoozedForAlarm) {
|
mSnoozedForAlarm.add(conditionId);
|
saveSnoozedLocked();
|
}
|
}
|
|
private void removeSnoozed(Uri conditionId) {
|
synchronized (mSnoozedForAlarm) {
|
mSnoozedForAlarm.remove(conditionId);
|
saveSnoozedLocked();
|
}
|
}
|
|
private void saveSnoozedLocked() {
|
final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
|
final int currentUser = ActivityManager.getCurrentUser();
|
Settings.Secure.putStringForUser(mContext.getContentResolver(),
|
SCP_SETTING,
|
setting,
|
currentUser);
|
}
|
|
private void readSnoozed() {
|
synchronized (mSnoozedForAlarm) {
|
long identity = Binder.clearCallingIdentity();
|
try {
|
final String setting = Settings.Secure.getStringForUser(
|
mContext.getContentResolver(),
|
SCP_SETTING,
|
ActivityManager.getCurrentUser());
|
if (setting != null) {
|
final String[] tokens = setting.split(SEPARATOR);
|
for (int i = 0; i < tokens.length; i++) {
|
String token = tokens[i];
|
if (token != null) {
|
token = token.trim();
|
}
|
if (TextUtils.isEmpty(token)) {
|
continue;
|
}
|
mSnoozedForAlarm.add(Uri.parse(token));
|
}
|
}
|
} finally {
|
Binder.restoreCallingIdentity(identity);
|
}
|
}
|
}
|
|
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
|
if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
|
synchronized (mSubscriptions) {
|
for (Uri conditionId : mSubscriptions.keySet()) {
|
final ScheduleCalendar cal = mSubscriptions.get(conditionId);
|
if (cal != null) {
|
cal.setTimeZone(Calendar.getInstance().getTimeZone());
|
}
|
}
|
}
|
}
|
evaluateSubscriptions();
|
}
|
};
|
|
}
|