NextAlarmConditionProvider.java revision 37bc92cc2332eb6f864977381135c19d6a081a92
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/** Built-in zen condition provider for alarm clock conditions */ 49public class NextAlarmConditionProvider extends ConditionProviderService { 50 private static final String TAG = "NextAlarmConditions"; 51 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 52 53 private static final String ACTION_TRIGGER = TAG + ".trigger"; 54 private static final String EXTRA_TRIGGER = "trigger"; 55 private static final int REQUEST_CODE = 100; 56 private static final long SECONDS = 1000; 57 private static final long HOURS = 60 * 60 * SECONDS; 58 private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS; // treat clear+set as update 59 private static final long EARLY = 5 * SECONDS; // fire early, ensure alarm stream is unmuted 60 private static final String NEXT_ALARM_PATH = "next_alarm"; 61 public static final ComponentName COMPONENT = 62 new ComponentName("android", NextAlarmConditionProvider.class.getName()); 63 64 private final Context mContext = this; 65 private final H mHandler = new H(); 66 67 private boolean mConnected; 68 private boolean mRegistered; 69 private AlarmManager mAlarmManager; 70 private int mCurrentUserId; 71 private long mLookaheadThreshold; 72 private long mScheduledAlarmTime; 73 private Callback mCallback; 74 private Uri mCurrentSubscription; 75 private PowerManager.WakeLock mWakeLock; 76 77 public NextAlarmConditionProvider() { 78 if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()"); 79 } 80 81 public void dump(PrintWriter pw, DumpFilter filter) { 82 pw.println(" NextAlarmConditionProvider:"); 83 pw.print(" mConnected="); pw.println(mConnected); 84 pw.print(" mRegistered="); pw.println(mRegistered); 85 pw.print(" mCurrentUserId="); pw.println(mCurrentUserId); 86 pw.print(" mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime)); 87 pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); 88 pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); 89 pw.print(" mCurrentSubscription="); pw.println(mCurrentSubscription); 90 pw.print(" mWakeLock="); pw.println(mWakeLock); 91 } 92 93 public void setCallback(Callback callback) { 94 mCallback = callback; 95 } 96 97 @Override 98 public void onConnected() { 99 if (DEBUG) Slog.d(TAG, "onConnected"); 100 mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 101 final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 102 mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 103 mLookaheadThreshold = mContext.getResources() 104 .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS; 105 init(); 106 mConnected = true; 107 } 108 109 public void onUserSwitched() { 110 if (DEBUG) Slog.d(TAG, "onUserSwitched"); 111 if (mConnected) { 112 init(); 113 } 114 } 115 116 @Override 117 public void onDestroy() { 118 super.onDestroy(); 119 if (DEBUG) Slog.d(TAG, "onDestroy"); 120 if (mConnected) { 121 mContext.unregisterReceiver(mReceiver); 122 } 123 mConnected = false; 124 } 125 126 @Override 127 public void onRequestConditions(int relevance) { 128 if (!mConnected || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return; 129 130 final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); 131 if (nextAlarm == null) return; // no next alarm 132 if (mCallback != null && mCallback.isInDowntime()) return; // prefer downtime condition 133 if (!isWithinLookaheadThreshold(nextAlarm)) return; // alarm not within window 134 135 // next alarm exists, and is within the configured lookahead threshold 136 notifyCondition(newConditionId(), nextAlarm, true, "request"); 137 } 138 139 private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) { 140 if (alarm == null) return false; 141 final long delta = getEarlyTriggerTime(alarm) - System.currentTimeMillis(); 142 return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold); 143 } 144 145 @Override 146 public void onSubscribe(Uri conditionId) { 147 if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); 148 if (!isNextAlarmCondition(conditionId)) { 149 notifyCondition(conditionId, null, false, "badCondition"); 150 return; 151 } 152 mCurrentSubscription = conditionId; 153 mHandler.postEvaluate(0); 154 } 155 156 private static long getEarlyTriggerTime(AlarmClockInfo alarm) { 157 return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0; 158 } 159 160 private void handleEvaluate() { 161 final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId); 162 final long triggerTime = getEarlyTriggerTime(nextAlarm); 163 final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm); 164 if (DEBUG) Slog.d(TAG, "handleEvaluate mCurrentSubscription=" + mCurrentSubscription 165 + " nextAlarm=" + formatAlarmDebug(triggerTime) 166 + " withinThreshold=" + withinThreshold); 167 if (mCurrentSubscription == null) return; // no one cares 168 if (!withinThreshold) { 169 // triggertime invalid or in the past, condition = false 170 notifyCondition(mCurrentSubscription, nextAlarm, false, "!withinThreshold"); 171 mCurrentSubscription = null; 172 return; 173 } 174 // triggertime in the future, condition = true, schedule alarm 175 notifyCondition(mCurrentSubscription, nextAlarm, true, "withinThreshold"); 176 rescheduleAlarm(triggerTime); 177 } 178 179 private static String formatDuration(long millis) { 180 final StringBuilder sb = new StringBuilder(); 181 TimeUtils.formatDuration(millis, sb); 182 return sb.toString(); 183 } 184 185 private void rescheduleAlarm(long time) { 186 if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time); 187 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 188 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, 189 new Intent(ACTION_TRIGGER) 190 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 191 .putExtra(EXTRA_TRIGGER, time), 192 PendingIntent.FLAG_UPDATE_CURRENT); 193 alarms.cancel(pendingIntent); 194 mScheduledAlarmTime = time; 195 if (time > 0) { 196 if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)", 197 formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis()))); 198 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 199 } 200 } 201 202 private void notifyCondition(Uri id, AlarmClockInfo alarm, boolean state, String reason) { 203 final String formattedAlarm = alarm == null ? "" : formatAlarm(alarm.getTriggerTime()); 204 if (DEBUG) Slog.d(TAG, "notifyCondition " + state + " alarm=" + formattedAlarm + " reason=" 205 + reason); 206 notifyCondition(new Condition(id, 207 mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm), 208 mContext.getString(R.string.zen_mode_next_alarm_line_one), 209 formattedAlarm, 0, 210 state ? Condition.STATE_TRUE : Condition.STATE_FALSE, 211 Condition.FLAG_RELEVANT_NOW)); 212 } 213 214 @Override 215 public void onUnsubscribe(Uri conditionId) { 216 if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); 217 if (conditionId != null && conditionId.equals(mCurrentSubscription)) { 218 mCurrentSubscription = null; 219 rescheduleAlarm(0); 220 } 221 } 222 223 public void attachBase(Context base) { 224 attachBaseContext(base); 225 } 226 227 public IConditionProvider asInterface() { 228 return (IConditionProvider) onBind(null); 229 } 230 231 private Uri newConditionId() { 232 return new Uri.Builder().scheme(Condition.SCHEME) 233 .authority(ZenModeConfig.SYSTEM_AUTHORITY) 234 .appendPath(NEXT_ALARM_PATH) 235 .appendPath(Integer.toString(mCurrentUserId)) 236 .build(); 237 } 238 239 private boolean isNextAlarmCondition(Uri conditionId) { 240 return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) 241 && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) 242 && conditionId.getPathSegments().size() == 2 243 && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH) 244 && conditionId.getPathSegments().get(1).equals(Integer.toString(mCurrentUserId)); 245 } 246 247 private void init() { 248 if (mRegistered) { 249 mContext.unregisterReceiver(mReceiver); 250 } 251 mCurrentUserId = ActivityManager.getCurrentUser(); 252 final IntentFilter filter = new IntentFilter(); 253 filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); 254 filter.addAction(ACTION_TRIGGER); 255 filter.addAction(Intent.ACTION_TIME_CHANGED); 256 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 257 mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null, 258 null); 259 mRegistered = true; 260 mHandler.postEvaluate(0); 261 } 262 263 private String formatAlarm(long time) { 264 return formatAlarm(time, "Hm", "hma"); 265 } 266 267 private String formatAlarm(long time, String skeleton24, String skeleton12) { 268 final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12; 269 final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); 270 return DateFormat.format(pattern, time).toString(); 271 } 272 273 private String formatAlarmDebug(AlarmClockInfo alarm) { 274 return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0); 275 } 276 277 private String formatAlarmDebug(long time) { 278 if (time <= 0) return Long.toString(time); 279 return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa")); 280 } 281 282 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 283 @Override 284 public void onReceive(Context context, Intent intent) { 285 final String action = intent.getAction(); 286 if (DEBUG) Slog.d(TAG, "onReceive " + action); 287 long delay = 0; 288 if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) { 289 delay = NEXT_ALARM_UPDATE_DELAY; 290 if (DEBUG) Slog.d(TAG, String.format(" next alarm for user %s: %s", 291 mCurrentUserId, 292 formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId)))); 293 } 294 mHandler.postEvaluate(delay); 295 mWakeLock.acquire(delay + 5000); // stay awake during evaluate 296 } 297 }; 298 299 public interface Callback { 300 boolean isInDowntime(); 301 } 302 303 private class H extends Handler { 304 private static final int MSG_EVALUATE = 1; 305 306 public void postEvaluate(long delay) { 307 removeMessages(MSG_EVALUATE); 308 sendEmptyMessageDelayed(MSG_EVALUATE, delay); 309 } 310 311 @Override 312 public void handleMessage(Message msg) { 313 if (msg.what == MSG_EVALUATE) { 314 handleEvaluate(); 315 } 316 } 317 } 318} 319