NextAlarmConditionProvider.java revision 7ab8ecd053d08aec230a81fccfcc5a5c780f15c7
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.ActivityManager; 20import android.app.AlarmManager; 21import android.app.PendingIntent; 22import android.app.AlarmManager.AlarmClockInfo; 23import android.content.BroadcastReceiver; 24import android.content.ComponentName; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.net.Uri; 29import android.os.Handler; 30import android.os.Message; 31import android.os.PowerManager; 32import android.os.UserHandle; 33import android.service.notification.Condition; 34import android.service.notification.ConditionProviderService; 35import android.service.notification.IConditionProvider; 36import android.service.notification.ZenModeConfig; 37import android.util.TimeUtils; 38import android.text.format.DateFormat; 39import android.util.Log; 40import android.util.Slog; 41 42import com.android.internal.R; 43import com.android.server.notification.NotificationManagerService.DumpFilter; 44 45import java.io.PrintWriter; 46import java.util.Locale; 47 48/** 49 * Built-in zen condition provider for alarm-clock-based conditions. 50 * 51 * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise 52 * it as an exit condition for zen mode (unless the built-in downtime condition is also available). 53 * 54 * <p>When this next alarm is selected as the active exit condition, follow subsequent changes 55 * to the user's next alarm, assuming it remains within the 12-hr window. 56 * 57 * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not 58 * survive a reboot. Maintain the illusion of a consistent next alarm value by holding on to 59 * a persisted condition until we receive the first value after reboot, or timeout with no value. 60 */ 61public class NextAlarmConditionProvider extends ConditionProviderService { 62 private static final String TAG = "NextAlarmConditions"; 63 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 64 65 private static final String ACTION_TRIGGER = TAG + ".trigger"; 66 private static final String EXTRA_TRIGGER = "trigger"; 67 private static final int REQUEST_CODE = 100; 68 private static final long SECONDS = 1000; 69 private static final long MINUTES = 60 * SECONDS; 70 private static final long HOURS = 60 * MINUTES; 71 private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS; // treat clear+set as update 72 private static final long EARLY = 5 * SECONDS; // fire early, ensure alarm stream is unmuted 73 private static final long WAIT_AFTER_CONNECT = 5 * MINUTES;// for initial alarm re-registration 74 private static final long WAIT_AFTER_BOOT = 20 * SECONDS; // for initial alarm re-registration 75 private static final String NEXT_ALARM_PATH = "next_alarm"; 76 public static final ComponentName COMPONENT = 77 new ComponentName("android", NextAlarmConditionProvider.class.getName()); 78 79 private final Context mContext = this; 80 private final H mHandler = new H(); 81 82 private long mConnected; 83 private boolean mRegistered; 84 private AlarmManager mAlarmManager; 85 private int mCurrentUserId; 86 private long mLookaheadThreshold; 87 private long mScheduledAlarmTime; 88 private Callback mCallback; 89 private Uri mCurrentSubscription; 90 private PowerManager.WakeLock mWakeLock; 91 private long mBootCompleted; 92 93 public NextAlarmConditionProvider() { 94 if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()"); 95 } 96 97 public void dump(PrintWriter pw, DumpFilter filter) { 98 pw.println(" NextAlarmConditionProvider:"); 99 pw.print(" mConnected="); pw.println(mConnected); 100 pw.print(" mBootCompleted="); pw.println(mBootCompleted); 101 pw.print(" mRegistered="); pw.println(mRegistered); 102 pw.print(" mCurrentUserId="); pw.println(mCurrentUserId); 103 pw.print(" mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime)); 104 pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); 105 pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); 106 pw.print(" mCurrentSubscription="); pw.println(mCurrentSubscription); 107 pw.print(" mWakeLock="); pw.println(mWakeLock); 108 } 109 110 public void setCallback(Callback callback) { 111 mCallback = callback; 112 } 113 114 @Override 115 public void onConnected() { 116 if (DEBUG) Slog.d(TAG, "onConnected"); 117 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 118 final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 119 mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 120 mLookaheadThreshold = mContext.getResources() 121 .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS; 122 init(); 123 mConnected = System.currentTimeMillis(); 124 } 125 126 public void onUserSwitched() { 127 if (DEBUG) Slog.d(TAG, "onUserSwitched"); 128 if (mConnected != 0) { 129 init(); 130 } 131 } 132 133 @Override 134 public void onDestroy() { 135 super.onDestroy(); 136 if (DEBUG) Slog.d(TAG, "onDestroy"); 137 if (mRegistered) { 138 mContext.unregisterReceiver(mReceiver); 139 mRegistered = false; 140 } 141 mConnected = 0; 142 } 143 144 @Override 145 public void onRequestConditions(int relevance) { 146 if (mConnected == 0 || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return; 147 148 final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); 149 if (nextAlarm == null) return; // no next alarm 150 if (mCallback != null && mCallback.isInDowntime()) return; // prefer downtime condition 151 if (!isWithinLookaheadThreshold(nextAlarm)) return; // alarm not within window 152 153 // next alarm exists, and is within the configured lookahead threshold 154 notifyCondition(newConditionId(), nextAlarm, Condition.STATE_TRUE, "request"); 155 } 156 157 private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) { 158 if (alarm == null) return false; 159 final long delta = getEarlyTriggerTime(alarm) - System.currentTimeMillis(); 160 return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold); 161 } 162 163 @Override 164 public void onSubscribe(Uri conditionId) { 165 if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); 166 if (!isNextAlarmCondition(conditionId)) { 167 notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition"); 168 return; 169 } 170 mCurrentSubscription = conditionId; 171 mHandler.postEvaluate(0); 172 } 173 174 private static long getEarlyTriggerTime(AlarmClockInfo alarm) { 175 return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0; 176 } 177 178 private boolean isDoneWaitingAfterBoot(long time) { 179 if (mBootCompleted > 0) return (time - mBootCompleted) > WAIT_AFTER_BOOT; 180 if (mConnected > 0) return (time - mConnected) > WAIT_AFTER_CONNECT; 181 return true; 182 } 183 184 private void handleEvaluate() { 185 final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); 186 final long triggerTime = getEarlyTriggerTime(nextAlarm); 187 final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm); 188 final long now = System.currentTimeMillis(); 189 final boolean booted = isDoneWaitingAfterBoot(now); 190 if (DEBUG) Slog.d(TAG, "handleEvaluate mCurrentSubscription=" + mCurrentSubscription 191 + " nextAlarm=" + formatAlarmDebug(triggerTime) 192 + " withinThreshold=" + withinThreshold 193 + " booted=" + booted); 194 if (mCurrentSubscription == null) return; // no one cares 195 if (!booted) { 196 // we don't know yet 197 notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_UNKNOWN, "!booted"); 198 final long recheckTime = (mBootCompleted > 0 ? mBootCompleted : now) + WAIT_AFTER_BOOT; 199 rescheduleAlarm(recheckTime); 200 return; 201 } 202 if (!withinThreshold) { 203 // triggertime invalid or in the past, condition = false 204 notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_FALSE, "!within"); 205 mCurrentSubscription = null; 206 return; 207 } 208 // triggertime in the future, condition = true, schedule alarm 209 notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_TRUE, "within"); 210 rescheduleAlarm(triggerTime); 211 } 212 213 private static String formatDuration(long millis) { 214 final StringBuilder sb = new StringBuilder(); 215 TimeUtils.formatDuration(millis, sb); 216 return sb.toString(); 217 } 218 219 private void rescheduleAlarm(long time) { 220 if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time); 221 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 222 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, 223 new Intent(ACTION_TRIGGER) 224 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 225 .putExtra(EXTRA_TRIGGER, time), 226 PendingIntent.FLAG_UPDATE_CURRENT); 227 alarms.cancel(pendingIntent); 228 mScheduledAlarmTime = time; 229 if (time > 0) { 230 if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)", 231 formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis()))); 232 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 233 } 234 } 235 236 private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) { 237 final String formattedAlarm = alarm == null ? "" : formatAlarm(alarm.getTriggerTime()); 238 if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state) 239 + " alarm=" + formattedAlarm + " reason=" + reason); 240 notifyCondition(new Condition(id, 241 mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm), 242 mContext.getString(R.string.zen_mode_next_alarm_line_one), 243 formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW)); 244 } 245 246 @Override 247 public void onUnsubscribe(Uri conditionId) { 248 if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); 249 if (conditionId != null && conditionId.equals(mCurrentSubscription)) { 250 mCurrentSubscription = null; 251 rescheduleAlarm(0); 252 } 253 } 254 255 public void attachBase(Context base) { 256 attachBaseContext(base); 257 } 258 259 public IConditionProvider asInterface() { 260 return (IConditionProvider) onBind(null); 261 } 262 263 private Uri newConditionId() { 264 return new Uri.Builder().scheme(Condition.SCHEME) 265 .authority(ZenModeConfig.SYSTEM_AUTHORITY) 266 .appendPath(NEXT_ALARM_PATH) 267 .appendPath(Integer.toString(mCurrentUserId)) 268 .build(); 269 } 270 271 private boolean isNextAlarmCondition(Uri conditionId) { 272 return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) 273 && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) 274 && conditionId.getPathSegments().size() == 2 275 && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH) 276 && conditionId.getPathSegments().get(1).equals(Integer.toString(mCurrentUserId)); 277 } 278 279 private void init() { 280 if (mRegistered) { 281 mContext.unregisterReceiver(mReceiver); 282 } 283 mCurrentUserId = ActivityManager.getCurrentUser(); 284 final IntentFilter filter = new IntentFilter(); 285 filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); 286 filter.addAction(ACTION_TRIGGER); 287 filter.addAction(Intent.ACTION_TIME_CHANGED); 288 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 289 filter.addAction(Intent.ACTION_BOOT_COMPLETED); 290 mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null, 291 null); 292 mRegistered = true; 293 mHandler.postEvaluate(0); 294 } 295 296 private String formatAlarm(long time) { 297 return formatAlarm(time, "Hm", "hma"); 298 } 299 300 private String formatAlarm(long time, String skeleton24, String skeleton12) { 301 final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12; 302 final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 303 return DateFormat.format(pattern, time).toString(); 304 } 305 306 private String formatAlarmDebug(AlarmClockInfo alarm) { 307 return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0); 308 } 309 310 private String formatAlarmDebug(long time) { 311 if (time <= 0) return Long.toString(time); 312 return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa")); 313 } 314 315 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 316 @Override 317 public void onReceive(Context context, Intent intent) { 318 final String action = intent.getAction(); 319 if (DEBUG) Slog.d(TAG, "onReceive " + action); 320 long delay = 0; 321 if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { 322 delay = NEXT_ALARM_UPDATE_DELAY; 323 if (DEBUG) Slog.d(TAG, String.format(" next alarm for user %s: %s", 324 mCurrentUserId, 325 formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId)))); 326 } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { 327 mBootCompleted = System.currentTimeMillis(); 328 } 329 mHandler.postEvaluate(delay); 330 mWakeLock.acquire(delay + 5000); // stay awake during evaluate 331 } 332 }; 333 334 public interface Callback { 335 boolean isInDowntime(); 336 } 337 338 private class H extends Handler { 339 private static final int MSG_EVALUATE = 1; 340 341 public void postEvaluate(long delay) { 342 removeMessages(MSG_EVALUATE); 343 sendEmptyMessageDelayed(MSG_EVALUATE, delay); 344 } 345 346 @Override 347 public void handleMessage(Message msg) { 348 if (msg.what == MSG_EVALUATE) { 349 handleEvaluate(); 350 } 351 } 352 } 353} 354