DowntimeConditionProvider.java revision 25c3421c5e65ddc7f2b2bf1b1208f3634e6f5256
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; 38 39import com.android.internal.R; 40import com.android.server.notification.NotificationManagerService.DumpFilter; 41 42import java.io.PrintWriter; 43import java.text.SimpleDateFormat; 44import java.util.Calendar; 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 final Calendar mCalendar = Calendar.getInstance(); 65 private final Context mContext = this; 66 private final ArraySet<Integer> mDays = new ArraySet<Integer>(); 67 private final ArraySet<Long> mFiredAlarms = new ArraySet<Long>(); 68 69 private boolean mConnected; 70 private NextAlarmTracker mTracker; 71 private int mDowntimeMode; 72 private ZenModeConfig mConfig; 73 private Callback mCallback; 74 75 public DowntimeConditionProvider() { 76 if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()"); 77 } 78 79 public void dump(PrintWriter pw, DumpFilter filter) { 80 pw.println(" DowntimeConditionProvider:"); 81 pw.print(" mConnected="); pw.println(mConnected); 82 pw.print(" mDowntimeMode="); pw.println(Global.zenModeToString(mDowntimeMode)); 83 pw.print(" mFiredAlarms="); pw.println(mFiredAlarms); 84 } 85 86 public void attachBase(Context base) { 87 attachBaseContext(base); 88 } 89 90 public IConditionProvider asInterface() { 91 return (IConditionProvider) onBind(null); 92 } 93 94 public void setCallback(Callback callback) { 95 mCallback = callback; 96 } 97 98 @Override 99 public void onConnected() { 100 if (DEBUG) Slog.d(TAG, "onConnected"); 101 mConnected = true; 102 final IntentFilter filter = new IntentFilter(); 103 filter.addAction(ENTER_ACTION); 104 filter.addAction(EXIT_ACTION); 105 filter.addAction(Intent.ACTION_TIME_CHANGED); 106 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 107 mContext.registerReceiver(mReceiver, filter); 108 mTracker = mCallback.getNextAlarmTracker(); 109 mTracker.addCallback(mTrackerCallback); 110 init(); 111 } 112 113 @Override 114 public void onDestroy() { 115 if (DEBUG) Slog.d(TAG, "onDestroy"); 116 mTracker.removeCallback(mTrackerCallback); 117 mConnected = false; 118 } 119 120 @Override 121 public void onRequestConditions(int relevance) { 122 if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); 123 if ((relevance & Condition.FLAG_RELEVANT_NOW) != 0) { 124 if (isInDowntime() && mConfig != null) { 125 notifyCondition(createCondition(mConfig.toDowntimeInfo(), mConfig.sleepNone, 126 Condition.STATE_TRUE)); 127 } 128 } 129 } 130 131 @Override 132 public void onSubscribe(Uri conditionId) { 133 if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId); 134 final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); 135 if (downtime != null && mConfig != null) { 136 final int state = mConfig.toDowntimeInfo().equals(downtime) && isInDowntime() 137 ? Condition.STATE_TRUE : Condition.STATE_FALSE; 138 if (DEBUG) Slog.d(TAG, "notify condition state: " + Condition.stateToString(state)); 139 notifyCondition(createCondition(downtime, mConfig.sleepNone, state)); 140 } 141 } 142 143 @Override 144 public void onUnsubscribe(Uri conditionId) { 145 if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId); 146 } 147 148 public void setConfig(ZenModeConfig config) { 149 if (Objects.equals(mConfig, config)) return; 150 if (DEBUG) Slog.d(TAG, "setConfig"); 151 mConfig = config; 152 if (mConnected) { 153 init(); 154 } 155 } 156 157 public boolean isInDowntime() { 158 return mDowntimeMode != Global.ZEN_MODE_OFF; 159 } 160 161 public Condition createCondition(DowntimeInfo downtime, boolean orAlarm, int state) { 162 if (downtime == null) return null; 163 final Uri id = ZenModeConfig.toDowntimeConditionId(downtime); 164 final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma"; 165 final Locale locale = Locale.getDefault(); 166 final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); 167 final long now = System.currentTimeMillis(); 168 long endTime = getTime(now, downtime.endHour, downtime.endMinute); 169 if (orAlarm) { 170 final AlarmClockInfo nextAlarm = mTracker.getNextAlarm(); 171 final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0; 172 if (nextAlarmTime > now && nextAlarmTime < endTime) { 173 endTime = nextAlarmTime; 174 } 175 } 176 final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(endTime)); 177 final String summary = mContext.getString(R.string.downtime_condition_summary, formatted); 178 final String line1 = mContext.getString(R.string.downtime_condition_line_one); 179 return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW); 180 } 181 182 public boolean isDowntimeCondition(Condition condition) { 183 return condition != null && ZenModeConfig.isValidDowntimeConditionId(condition.id); 184 } 185 186 private void init() { 187 updateDays(); 188 reevaluateDowntime(); 189 updateAlarms(); 190 } 191 192 private void updateDays() { 193 mDays.clear(); 194 if (mConfig != null) { 195 final int[] days = ZenModeConfig.tryParseDays(mConfig.sleepMode); 196 for (int i = 0; days != null && i < days.length; i++) { 197 mDays.add(days[i]); 198 } 199 } 200 } 201 202 private boolean isInDowntime(long time) { 203 if (mConfig == null || mDays.size() == 0) return false; 204 final long start = getTime(time, mConfig.sleepStartHour, mConfig.sleepStartMinute); 205 long end = getTime(time, mConfig.sleepEndHour, mConfig.sleepEndMinute); 206 if (start == end) return false; 207 if (end < start) { 208 end = addDays(end, 1); 209 } 210 final boolean orAlarm = mConfig.sleepNone; 211 return isInDowntime(-1, time, start, end, orAlarm) 212 || isInDowntime(0, time, start, end, orAlarm); 213 } 214 215 private boolean isInDowntime(int daysOffset, long time, long start, long end, boolean orAlarm) { 216 final int n = Calendar.SATURDAY; 217 final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1; 218 start = addDays(start, daysOffset); 219 end = addDays(end, daysOffset); 220 if (orAlarm) { 221 end = findFiredAlarm(start, end); 222 } 223 return mDays.contains(day) && time >= start && time < end; 224 } 225 226 private long findFiredAlarm(long start, long end) { 227 final int N = mFiredAlarms.size(); 228 for (int i = 0; i < N; i++) { 229 final long firedAlarm = mFiredAlarms.valueAt(i); 230 if (firedAlarm > start && firedAlarm < end) { 231 return firedAlarm; 232 } 233 } 234 return end; 235 } 236 237 private void reevaluateDowntime() { 238 final long now = System.currentTimeMillis(); 239 final boolean inDowntimeNow = isInDowntime(now); 240 final int downtimeMode = inDowntimeNow ? (mConfig.sleepNone 241 ? Global.ZEN_MODE_NO_INTERRUPTIONS : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) 242 : Global.ZEN_MODE_OFF; 243 if (DEBUG) Slog.d(TAG, "downtimeMode=" + downtimeMode); 244 if (downtimeMode == mDowntimeMode) return; 245 mDowntimeMode = downtimeMode; 246 Slog.i(TAG, (isInDowntime() ? "Entering" : "Exiting" ) + " downtime"); 247 ZenLog.traceDowntime(mDowntimeMode, getDayOfWeek(now), mDays); 248 fireDowntimeChanged(); 249 } 250 251 private void fireDowntimeChanged() { 252 if (mCallback != null) { 253 mCallback.onDowntimeChanged(mDowntimeMode); 254 } 255 } 256 257 private void updateAlarms() { 258 if (mConfig == null) return; 259 updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute); 260 updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute); 261 } 262 263 private int getDayOfWeek(long time) { 264 mCalendar.setTimeInMillis(time); 265 return mCalendar.get(Calendar.DAY_OF_WEEK); 266 } 267 268 private long getTime(long millis, int hour, int min) { 269 mCalendar.setTimeInMillis(millis); 270 mCalendar.set(Calendar.HOUR_OF_DAY, hour); 271 mCalendar.set(Calendar.MINUTE, min); 272 mCalendar.set(Calendar.SECOND, 0); 273 mCalendar.set(Calendar.MILLISECOND, 0); 274 return mCalendar.getTimeInMillis(); 275 } 276 277 private long addDays(long time, int days) { 278 mCalendar.setTimeInMillis(time); 279 mCalendar.add(Calendar.DATE, days); 280 return mCalendar.getTimeInMillis(); 281 } 282 283 private void updateAlarm(String action, int requestCode, int hr, int min) { 284 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 285 final long now = System.currentTimeMillis(); 286 mCalendar.setTimeInMillis(now); 287 mCalendar.set(Calendar.HOUR_OF_DAY, hr); 288 mCalendar.set(Calendar.MINUTE, min); 289 mCalendar.set(Calendar.SECOND, 0); 290 mCalendar.set(Calendar.MILLISECOND, 0); 291 long time = mCalendar.getTimeInMillis(); 292 if (time <= now) { 293 time = addDays(time, 1); 294 } 295 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, 296 new Intent(action) 297 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 298 .putExtra(EXTRA_TIME, time), 299 PendingIntent.FLAG_UPDATE_CURRENT); 300 alarms.cancel(pendingIntent); 301 if (mConfig.sleepMode != null) { 302 if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s", 303 action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now))); 304 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 305 } 306 } 307 308 private static String ts(long time) { 309 return new Date(time) + " (" + time + ")"; 310 } 311 312 private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 313 if (!booted) return; // we don't know yet 314 // update condition description if we're in downtime (mode = none) 315 if (isInDowntime() && mConfig != null && mConfig.sleepNone) { 316 notifyCondition(createCondition(mConfig.toDowntimeInfo(), true /*orAlarm*/, 317 Condition.STATE_TRUE)); 318 } 319 if (nextAlarm == null) return; // not fireable 320 if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm)); 321 if (System.currentTimeMillis() > wakeupTime) { 322 if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime)); 323 trimFiredAlarms(); 324 mFiredAlarms.add(wakeupTime); 325 } 326 reevaluateDowntime(); 327 } 328 329 private void trimFiredAlarms() { 330 // remove fired alarms over 2 days old 331 final long keepAfter = System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000; 332 final int N = mFiredAlarms.size(); 333 for (int i = N - 1; i >= 0; i--) { 334 final long firedAlarm = mFiredAlarms.valueAt(i); 335 if (firedAlarm < keepAfter) { 336 mFiredAlarms.removeAt(i); 337 } 338 } 339 } 340 341 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 342 @Override 343 public void onReceive(Context context, Intent intent) { 344 final String action = intent.getAction(); 345 final long now = System.currentTimeMillis(); 346 if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) { 347 final long schTime = intent.getLongExtra(EXTRA_TIME, 0); 348 if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", 349 action, ts(schTime), ts(now), now - schTime)); 350 } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 351 if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault()); 352 mCalendar.setTimeZone(TimeZone.getDefault()); 353 mFiredAlarms.clear(); 354 } else if (Intent.ACTION_TIME_CHANGED.equals(action)) { 355 if (DEBUG) Slog.d(TAG, "time changed to " + now); 356 mFiredAlarms.clear(); 357 } else { 358 if (DEBUG) Slog.d(TAG, action + " fired at " + now); 359 } 360 reevaluateDowntime(); 361 updateAlarms(); 362 } 363 }; 364 365 private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() { 366 @Override 367 public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 368 DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted); 369 } 370 }; 371 372 public interface Callback { 373 void onDowntimeChanged(int downtimeMode); 374 NextAlarmTracker getNextAlarmTracker(); 375 } 376} 377