blob: df4ecfdb489b3331a8f251f1a7e60da5f884c387 [file] [log] [blame]
/**
* Copyright (c) 2014, 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.app.AlarmManager.AlarmClockInfo;
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.provider.Settings.Global;
import android.service.notification.Condition;
import android.service.notification.ConditionProviderService;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.DowntimeInfo;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.internal.R;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
/** Built-in zen condition provider for managing downtime */
public class DowntimeConditionProvider extends ConditionProviderService {
private static final String TAG = "DowntimeConditions";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
public static final ComponentName COMPONENT =
new ComponentName("android", DowntimeConditionProvider.class.getName());
private static final String ENTER_ACTION = TAG + ".enter";
private static final int ENTER_CODE = 100;
private static final String EXIT_ACTION = TAG + ".exit";
private static final int EXIT_CODE = 101;
private static final String EXTRA_TIME = "time";
private static final long SECONDS = 1000;
private static final long MINUTES = 60 * SECONDS;
private static final long HOURS = 60 * MINUTES;
private final Context mContext = this;
private final DowntimeCalendar mCalendar = new DowntimeCalendar();
private final FiredAlarms mFiredAlarms = new FiredAlarms();
private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
private final ConditionProviders mConditionProviders;
private final NextAlarmTracker mTracker;
private final ZenModeHelper mZenModeHelper;
private boolean mConnected;
private long mLookaheadThreshold;
private ZenModeConfig mConfig;
private boolean mDowntimed;
private boolean mConditionClearing;
private boolean mRequesting;
public DowntimeConditionProvider(ConditionProviders conditionProviders,
NextAlarmTracker tracker, ZenModeHelper zenModeHelper) {
if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()");
mConditionProviders = conditionProviders;
mTracker = tracker;
mZenModeHelper = zenModeHelper;
}
public void dump(PrintWriter pw, DumpFilter filter) {
pw.println(" DowntimeConditionProvider:");
pw.print(" mConnected="); pw.println(mConnected);
pw.print(" mSubscriptions="); pw.println(mSubscriptions);
pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold);
pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
pw.print(" mCalendar="); pw.println(mCalendar);
pw.print(" mFiredAlarms="); pw.println(mFiredAlarms);
pw.print(" mDowntimed="); pw.println(mDowntimed);
pw.print(" mConditionClearing="); pw.println(mConditionClearing);
pw.print(" mRequesting="); pw.println(mRequesting);
}
public void attachBase(Context base) {
attachBaseContext(base);
}
public IConditionProvider asInterface() {
return (IConditionProvider) onBind(null);
}
@Override
public void onConnected() {
if (DEBUG) Slog.d(TAG, "onConnected");
mConnected = true;
mLookaheadThreshold = PropConfig.getInt(mContext, "downtime.condition.lookahead",
R.integer.config_downtime_condition_lookahead_threshold_hrs) * HOURS;
final IntentFilter filter = new IntentFilter();
filter.addAction(ENTER_ACTION);
filter.addAction(EXIT_ACTION);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
mContext.registerReceiver(mReceiver, filter);
mTracker.addCallback(mTrackerCallback);
mZenModeHelper.addCallback(mZenCallback);
init();
}
@Override
public void onDestroy() {
if (DEBUG) Slog.d(TAG, "onDestroy");
mTracker.removeCallback(mTrackerCallback);
mZenModeHelper.removeCallback(mZenCallback);
mConnected = false;
}
@Override
public void onRequestConditions(int relevance) {
if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance);
if (!mConnected) return;
mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0;
evaluateSubscriptions();
}
@Override
public void onSubscribe(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId);
final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId);
if (downtime == null) return;
mFiredAlarms.clear();
mSubscriptions.add(conditionId);
notifyCondition(downtime);
}
private boolean shouldShowCondition() {
final long now = System.currentTimeMillis();
if (DEBUG) Slog.d(TAG, "shouldShowCondition now=" + mCalendar.isInDowntime(now)
+ " lookahead="
+ (mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold)));
return mCalendar.isInDowntime(now)
|| mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold);
}
private void notifyCondition(DowntimeInfo downtime) {
if (mConfig == null) {
// we don't know yet
notifyCondition(createCondition(downtime, Condition.STATE_UNKNOWN));
return;
}
if (!downtime.equals(mConfig.toDowntimeInfo())) {
// not the configured downtime, consider it false
notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
return;
}
if (!shouldShowCondition()) {
// configured downtime, but not within the time range
notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
return;
}
if (isZenNone() && mFiredAlarms.findBefore(System.currentTimeMillis())) {
// within the configured time range, but wake up if none and the next alarm is fired
notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
return;
}
// within the configured time range, condition still valid
notifyCondition(createCondition(downtime, Condition.STATE_TRUE));
}
private boolean isZenNone() {
return mZenModeHelper.getZenMode() == Global.ZEN_MODE_NO_INTERRUPTIONS;
}
private boolean isZenOff() {
return mZenModeHelper.getZenMode() == Global.ZEN_MODE_OFF;
}
private void evaluateSubscriptions() {
ArraySet<Uri> conditions = mSubscriptions;
if (mConfig != null && mRequesting && shouldShowCondition()) {
final Uri id = ZenModeConfig.toDowntimeConditionId(mConfig.toDowntimeInfo());
if (!conditions.contains(id)) {
conditions = new ArraySet<Uri>(conditions);
conditions.add(id);
}
}
for (Uri conditionId : conditions) {
final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId);
if (downtime != null) {
notifyCondition(downtime);
}
}
}
@Override
public void onUnsubscribe(Uri conditionId) {
final boolean current = mSubscriptions.contains(conditionId);
if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId + " current=" + current);
mSubscriptions.remove(conditionId);
mFiredAlarms.clear();
}
public void setConfig(ZenModeConfig config) {
if (Objects.equals(mConfig, config)) return;
final boolean downtimeChanged = mConfig == null || config == null
|| !mConfig.toDowntimeInfo().equals(config.toDowntimeInfo());
mConfig = config;
if (DEBUG) Slog.d(TAG, "setConfig downtimeChanged=" + downtimeChanged);
if (mConnected && downtimeChanged) {
mDowntimed = false;
init();
}
// when active, mark downtime as entered for today
if (mConfig != null && mConfig.exitCondition != null
&& ZenModeConfig.isValidDowntimeConditionId(mConfig.exitCondition.id)) {
mDowntimed = true;
}
}
public void onManualConditionClearing() {
mConditionClearing = true;
}
private Condition createCondition(DowntimeInfo downtime, int state) {
if (downtime == null) return null;
final Uri id = ZenModeConfig.toDowntimeConditionId(downtime);
final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma";
final Locale locale = Locale.getDefault();
final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
final long now = System.currentTimeMillis();
long endTime = mCalendar.getNextTime(now, downtime.endHour, downtime.endMinute);
if (isZenNone()) {
final AlarmClockInfo nextAlarm = mTracker.getNextAlarm();
final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0;
if (nextAlarmTime > now && nextAlarmTime < endTime) {
endTime = nextAlarmTime;
}
}
final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(endTime));
final String summary = mContext.getString(R.string.downtime_condition_summary, formatted);
final String line1 = mContext.getString(R.string.downtime_condition_line_one);
return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW);
}
private void init() {
mCalendar.setDowntimeInfo(mConfig != null ? mConfig.toDowntimeInfo() : null);
evaluateSubscriptions();
updateAlarms();
evaluateAutotrigger();
}
private void updateAlarms() {
if (mConfig == null) return;
updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute);
updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute);
}
private void updateAlarm(String action, int requestCode, int hr, int min) {
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final long now = System.currentTimeMillis();
final long time = mCalendar.getNextTime(now, hr, min);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
new Intent(action)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_TIME, time),
PendingIntent.FLAG_UPDATE_CURRENT);
alarms.cancel(pendingIntent);
if (mConfig.sleepMode != null) {
if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s",
action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now)));
alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
}
private static String ts(long time) {
return new Date(time) + " (" + time + ")";
}
private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
if (!booted) return; // we don't know yet
if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm));
if (nextAlarm != null && wakeupTime > 0 && System.currentTimeMillis() > wakeupTime) {
if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime));
mFiredAlarms.add(wakeupTime);
}
evaluateSubscriptions();
}
private void evaluateAutotrigger() {
String skipReason = null;
if (mConfig == null) {
skipReason = "no config";
} else if (mDowntimed) {
skipReason = "already downtimed";
} else if (mZenModeHelper.getZenMode() != Global.ZEN_MODE_OFF) {
skipReason = "already in zen";
} else if (!mCalendar.isInDowntime(System.currentTimeMillis())) {
skipReason = "not in downtime";
}
if (skipReason != null) {
ZenLog.traceDowntimeAutotrigger("Autotrigger skipped: " + skipReason);
return;
}
ZenLog.traceDowntimeAutotrigger("Autotrigger fired");
mZenModeHelper.setZenMode(mConfig.sleepNone ? Global.ZEN_MODE_NO_INTERRUPTIONS
: Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, "downtime");
final Condition condition = createCondition(mConfig.toDowntimeInfo(), Condition.STATE_TRUE);
mConditionProviders.setZenModeCondition(condition, "downtime");
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final long now = System.currentTimeMillis();
if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) {
final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
action, ts(schTime), ts(now), now - schTime));
if (ENTER_ACTION.equals(action)) {
evaluateAutotrigger();
} else /*EXIT_ACTION*/ {
mDowntimed = false;
}
mFiredAlarms.clear();
} else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault());
mCalendar.setTimeZone(TimeZone.getDefault());
mFiredAlarms.clear();
} else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
if (DEBUG) Slog.d(TAG, "time changed to " + now);
mFiredAlarms.clear();
} else {
if (DEBUG) Slog.d(TAG, action + " fired at " + now);
}
evaluateSubscriptions();
updateAlarms();
}
};
private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() {
@Override
public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted);
}
};
private final ZenModeHelper.Callback mZenCallback = new ZenModeHelper.Callback() {
@Override
void onZenModeChanged() {
if (mConditionClearing && isZenOff()) {
evaluateAutotrigger();
}
mConditionClearing = false;
evaluateSubscriptions();
}
};
private class FiredAlarms {
private final ArraySet<Long> mFiredAlarms = new ArraySet<Long>();
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < mFiredAlarms.size(); i++) {
if (i > 0) sb.append(',');
sb.append(mTracker.formatAlarmDebug(mFiredAlarms.valueAt(i)));
}
return sb.toString();
}
public void add(long firedAlarm) {
mFiredAlarms.add(firedAlarm);
}
public void clear() {
mFiredAlarms.clear();
}
public boolean findBefore(long time) {
for (int i = 0; i < mFiredAlarms.size(); i++) {
if (mFiredAlarms.valueAt(i) < time) {
return true;
}
}
return false;
}
}
}