1/** 2 * Copyright (c) 2014, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.notification; 18 19import android.app.AlarmManager; 20import android.app.PendingIntent; 21import android.app.AlarmManager.AlarmClockInfo; 22import android.content.BroadcastReceiver; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.net.Uri; 28import android.provider.Settings.Global; 29import android.service.notification.Condition; 30import android.service.notification.ConditionProviderService; 31import android.service.notification.IConditionProvider; 32import android.service.notification.ZenModeConfig; 33import android.service.notification.ZenModeConfig.DowntimeInfo; 34import android.text.format.DateFormat; 35import android.util.ArraySet; 36import android.util.Log; 37import android.util.Slog; 38import android.util.TimeUtils; 39 40import com.android.internal.R; 41import com.android.server.notification.NotificationManagerService.DumpFilter; 42 43import java.io.PrintWriter; 44import java.text.SimpleDateFormat; 45import java.util.Date; 46import java.util.Locale; 47import java.util.Objects; 48import java.util.TimeZone; 49 50/** Built-in zen condition provider for managing downtime */ 51public class DowntimeConditionProvider extends ConditionProviderService { 52 private static final String TAG = "DowntimeConditions"; 53 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 54 55 public static final ComponentName COMPONENT = 56 new ComponentName("android", DowntimeConditionProvider.class.getName()); 57 58 private static final String ENTER_ACTION = TAG + ".enter"; 59 private static final int ENTER_CODE = 100; 60 private static final String EXIT_ACTION = TAG + ".exit"; 61 private static final int EXIT_CODE = 101; 62 private static final String EXTRA_TIME = "time"; 63 64 private static final long SECONDS = 1000; 65 private static final long MINUTES = 60 * SECONDS; 66 private static final long HOURS = 60 * MINUTES; 67 68 private final Context mContext = this; 69 private final DowntimeCalendar mCalendar = new DowntimeCalendar(); 70 private final FiredAlarms mFiredAlarms = new FiredAlarms(); 71 private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>(); 72 private final ConditionProviders mConditionProviders; 73 private final NextAlarmTracker mTracker; 74 private final ZenModeHelper mZenModeHelper; 75 76 private boolean mConnected; 77 private long mLookaheadThreshold; 78 private ZenModeConfig mConfig; 79 private boolean mDowntimed; 80 private boolean mConditionClearing; 81 private boolean mRequesting; 82 83 public DowntimeConditionProvider(ConditionProviders conditionProviders, 84 NextAlarmTracker tracker, ZenModeHelper zenModeHelper) { 85 if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()"); 86 mConditionProviders = conditionProviders; 87 mTracker = tracker; 88 mZenModeHelper = zenModeHelper; 89 } 90 91 public void dump(PrintWriter pw, DumpFilter filter) { 92 pw.println(" DowntimeConditionProvider:"); 93 pw.print(" mConnected="); pw.println(mConnected); 94 pw.print(" mSubscriptions="); pw.println(mSubscriptions); 95 pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); 96 pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); 97 pw.print(" mCalendar="); pw.println(mCalendar); 98 pw.print(" mFiredAlarms="); pw.println(mFiredAlarms); 99 pw.print(" mDowntimed="); pw.println(mDowntimed); 100 pw.print(" mConditionClearing="); pw.println(mConditionClearing); 101 pw.print(" mRequesting="); pw.println(mRequesting); 102 } 103 104 public void attachBase(Context base) { 105 attachBaseContext(base); 106 } 107 108 public IConditionProvider asInterface() { 109 return (IConditionProvider) onBind(null); 110 } 111 112 @Override 113 public void onConnected() { 114 if (DEBUG) Slog.d(TAG, "onConnected"); 115 mConnected = true; 116 mLookaheadThreshold = PropConfig.getInt(mContext, "downtime.condition.lookahead", 117 R.integer.config_downtime_condition_lookahead_threshold_hrs) * HOURS; 118 final IntentFilter filter = new IntentFilter(); 119 filter.addAction(ENTER_ACTION); 120 filter.addAction(EXIT_ACTION); 121 filter.addAction(Intent.ACTION_TIME_CHANGED); 122 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 123 mContext.registerReceiver(mReceiver, filter); 124 mTracker.addCallback(mTrackerCallback); 125 mZenModeHelper.addCallback(mZenCallback); 126 init(); 127 } 128 129 @Override 130 public void onDestroy() { 131 if (DEBUG) Slog.d(TAG, "onDestroy"); 132 mTracker.removeCallback(mTrackerCallback); 133 mZenModeHelper.removeCallback(mZenCallback); 134 mConnected = false; 135 } 136 137 @Override 138 public void onRequestConditions(int relevance) { 139 if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); 140 if (!mConnected) return; 141 mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0; 142 evaluateSubscriptions(); 143 } 144 145 @Override 146 public void onSubscribe(Uri conditionId) { 147 if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId); 148 final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); 149 if (downtime == null) return; 150 mFiredAlarms.clear(); 151 mSubscriptions.add(conditionId); 152 notifyCondition(downtime); 153 } 154 155 private boolean shouldShowCondition() { 156 final long now = System.currentTimeMillis(); 157 if (DEBUG) Slog.d(TAG, "shouldShowCondition now=" + mCalendar.isInDowntime(now) 158 + " lookahead=" 159 + (mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold))); 160 return mCalendar.isInDowntime(now) 161 || mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold); 162 } 163 164 private void notifyCondition(DowntimeInfo downtime) { 165 if (mConfig == null) { 166 // we don't know yet 167 notifyCondition(createCondition(downtime, Condition.STATE_UNKNOWN)); 168 return; 169 } 170 if (!downtime.equals(mConfig.toDowntimeInfo())) { 171 // not the configured downtime, consider it false 172 notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); 173 return; 174 } 175 if (!shouldShowCondition()) { 176 // configured downtime, but not within the time range 177 notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); 178 return; 179 } 180 if (isZenNone() && mFiredAlarms.findBefore(System.currentTimeMillis())) { 181 // within the configured time range, but wake up if none and the next alarm is fired 182 notifyCondition(createCondition(downtime, Condition.STATE_FALSE)); 183 return; 184 } 185 // within the configured time range, condition still valid 186 notifyCondition(createCondition(downtime, Condition.STATE_TRUE)); 187 } 188 189 private boolean isZenNone() { 190 return mZenModeHelper.getZenMode() == Global.ZEN_MODE_NO_INTERRUPTIONS; 191 } 192 193 private boolean isZenOff() { 194 return mZenModeHelper.getZenMode() == Global.ZEN_MODE_OFF; 195 } 196 197 private void evaluateSubscriptions() { 198 ArraySet<Uri> conditions = mSubscriptions; 199 if (mConfig != null && mRequesting && shouldShowCondition()) { 200 final Uri id = ZenModeConfig.toDowntimeConditionId(mConfig.toDowntimeInfo()); 201 if (!conditions.contains(id)) { 202 conditions = new ArraySet<Uri>(conditions); 203 conditions.add(id); 204 } 205 } 206 for (Uri conditionId : conditions) { 207 final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); 208 if (downtime != null) { 209 notifyCondition(downtime); 210 } 211 } 212 } 213 214 @Override 215 public void onUnsubscribe(Uri conditionId) { 216 final boolean current = mSubscriptions.contains(conditionId); 217 if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId + " current=" + current); 218 mSubscriptions.remove(conditionId); 219 mFiredAlarms.clear(); 220 } 221 222 public void setConfig(ZenModeConfig config) { 223 if (Objects.equals(mConfig, config)) return; 224 final boolean downtimeChanged = mConfig == null || config == null 225 || !mConfig.toDowntimeInfo().equals(config.toDowntimeInfo()); 226 mConfig = config; 227 if (DEBUG) Slog.d(TAG, "setConfig downtimeChanged=" + downtimeChanged); 228 if (mConnected && downtimeChanged) { 229 mDowntimed = false; 230 init(); 231 } 232 // when active, mark downtime as entered for today 233 if (mConfig != null && mConfig.exitCondition != null 234 && ZenModeConfig.isValidDowntimeConditionId(mConfig.exitCondition.id)) { 235 mDowntimed = true; 236 } 237 } 238 239 public void onManualConditionClearing() { 240 mConditionClearing = true; 241 } 242 243 private Condition createCondition(DowntimeInfo downtime, int state) { 244 if (downtime == null) return null; 245 final Uri id = ZenModeConfig.toDowntimeConditionId(downtime); 246 final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma"; 247 final Locale locale = Locale.getDefault(); 248 final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); 249 final long now = System.currentTimeMillis(); 250 long endTime = mCalendar.getNextTime(now, downtime.endHour, downtime.endMinute); 251 if (isZenNone()) { 252 final AlarmClockInfo nextAlarm = mTracker.getNextAlarm(); 253 final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0; 254 if (nextAlarmTime > now && nextAlarmTime < endTime) { 255 endTime = nextAlarmTime; 256 } 257 } 258 final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(endTime)); 259 final String summary = mContext.getString(R.string.downtime_condition_summary, formatted); 260 final String line1 = mContext.getString(R.string.downtime_condition_line_one); 261 return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW); 262 } 263 264 private void init() { 265 mCalendar.setDowntimeInfo(mConfig != null ? mConfig.toDowntimeInfo() : null); 266 evaluateSubscriptions(); 267 updateAlarms(); 268 evaluateAutotrigger(); 269 } 270 271 private void updateAlarms() { 272 if (mConfig == null) return; 273 updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute); 274 updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute); 275 } 276 277 278 private void updateAlarm(String action, int requestCode, int hr, int min) { 279 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 280 final long now = System.currentTimeMillis(); 281 final long time = mCalendar.getNextTime(now, hr, min); 282 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, 283 new Intent(action) 284 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 285 .putExtra(EXTRA_TIME, time), 286 PendingIntent.FLAG_UPDATE_CURRENT); 287 alarms.cancel(pendingIntent); 288 if (mConfig.sleepMode != null) { 289 if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s", 290 action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now))); 291 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 292 } 293 } 294 295 private static String ts(long time) { 296 return new Date(time) + " (" + time + ")"; 297 } 298 299 private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 300 if (!booted) return; // we don't know yet 301 if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm)); 302 if (nextAlarm != null && wakeupTime > 0 && System.currentTimeMillis() > wakeupTime) { 303 if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime)); 304 mFiredAlarms.add(wakeupTime); 305 } 306 evaluateSubscriptions(); 307 } 308 309 private void evaluateAutotrigger() { 310 String skipReason = null; 311 if (mConfig == null) { 312 skipReason = "no config"; 313 } else if (mDowntimed) { 314 skipReason = "already downtimed"; 315 } else if (mZenModeHelper.getZenMode() != Global.ZEN_MODE_OFF) { 316 skipReason = "already in zen"; 317 } else if (!mCalendar.isInDowntime(System.currentTimeMillis())) { 318 skipReason = "not in downtime"; 319 } 320 if (skipReason != null) { 321 ZenLog.traceDowntimeAutotrigger("Autotrigger skipped: " + skipReason); 322 return; 323 } 324 ZenLog.traceDowntimeAutotrigger("Autotrigger fired"); 325 mZenModeHelper.setZenMode(mConfig.sleepNone ? Global.ZEN_MODE_NO_INTERRUPTIONS 326 : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, "downtime"); 327 final Condition condition = createCondition(mConfig.toDowntimeInfo(), Condition.STATE_TRUE); 328 mConditionProviders.setZenModeCondition(condition, "downtime"); 329 } 330 331 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 332 @Override 333 public void onReceive(Context context, Intent intent) { 334 final String action = intent.getAction(); 335 final long now = System.currentTimeMillis(); 336 if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) { 337 final long schTime = intent.getLongExtra(EXTRA_TIME, 0); 338 if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", 339 action, ts(schTime), ts(now), now - schTime)); 340 if (ENTER_ACTION.equals(action)) { 341 evaluateAutotrigger(); 342 } else /*EXIT_ACTION*/ { 343 mDowntimed = false; 344 } 345 mFiredAlarms.clear(); 346 } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 347 if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault()); 348 mCalendar.setTimeZone(TimeZone.getDefault()); 349 mFiredAlarms.clear(); 350 } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { 351 if (DEBUG) Slog.d(TAG, "time changed to " + now); 352 mFiredAlarms.clear(); 353 } else { 354 if (DEBUG) Slog.d(TAG, action + " fired at " + now); 355 } 356 evaluateSubscriptions(); 357 updateAlarms(); 358 } 359 }; 360 361 private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() { 362 @Override 363 public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 364 DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted); 365 } 366 }; 367 368 private final ZenModeHelper.Callback mZenCallback = new ZenModeHelper.Callback() { 369 @Override 370 void onZenModeChanged() { 371 if (mConditionClearing && isZenOff()) { 372 evaluateAutotrigger(); 373 } 374 mConditionClearing = false; 375 evaluateSubscriptions(); 376 } 377 }; 378 379 private class FiredAlarms { 380 private final ArraySet<Long> mFiredAlarms = new ArraySet<Long>(); 381 382 @Override 383 public String toString() { 384 final StringBuilder sb = new StringBuilder(); 385 for (int i = 0; i < mFiredAlarms.size(); i++) { 386 if (i > 0) sb.append(','); 387 sb.append(mTracker.formatAlarmDebug(mFiredAlarms.valueAt(i))); 388 } 389 return sb.toString(); 390 } 391 392 public void add(long firedAlarm) { 393 mFiredAlarms.add(firedAlarm); 394 } 395 396 public void clear() { 397 mFiredAlarms.clear(); 398 } 399 400 public boolean findBefore(long time) { 401 for (int i = 0; i < mFiredAlarms.size(); i++) { 402 if (mFiredAlarms.valueAt(i) < time) { 403 return true; 404 } 405 } 406 return false; 407 } 408 } 409} 410