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 17 package com.android.server.notification; 18 19 import android.app.AlarmManager; 20 import android.app.AlarmManager.AlarmClockInfo; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.net.Uri; 24 import android.service.notification.Condition; 25 import android.service.notification.ConditionProviderService; 26 import android.service.notification.IConditionProvider; 27 import android.service.notification.ZenModeConfig; 28 import android.text.TextUtils; 29 import android.util.ArraySet; 30 import android.util.Log; 31 import android.util.Slog; 32 import android.util.TimeUtils; 33 34 import com.android.internal.R; 35 import com.android.server.notification.NotificationManagerService.DumpFilter; 36 37 import java.io.PrintWriter; 38 39 /** 40 * Built-in zen condition provider for alarm-clock-based conditions. 41 * 42 * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise 43 * it as an exit condition for zen mode. 44 * 45 * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not 46 * survive a reboot. Maintain the illusion of a consistent next alarm value by holding on to 47 * a persisted condition until we receive the first value after reboot, or timeout with no value. 48 */ 49 public 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 long SECONDS = 1000; 54 private static final long MINUTES = 60 * SECONDS; 55 private static final long HOURS = 60 * MINUTES; 56 57 private static final long BAD_CONDITION = -1; 58 59 public static final ComponentName COMPONENT = 60 new ComponentName("android", NextAlarmConditionProvider.class.getName()); 61 62 private final Context mContext = this; 63 private final NextAlarmTracker mTracker; 64 private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>(); 65 66 private boolean mConnected; 67 private long mLookaheadThreshold; 68 private boolean mRequesting; 69 70 public NextAlarmConditionProvider(NextAlarmTracker tracker) { 71 if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()"); 72 mTracker = tracker; 73 } 74 75 public void dump(PrintWriter pw, DumpFilter filter) { 76 pw.println(" NextAlarmConditionProvider:"); 77 pw.print(" mConnected="); pw.println(mConnected); 78 pw.print(" mLookaheadThreshold="); pw.print(mLookaheadThreshold); 79 pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")"); 80 pw.print(" mSubscriptions="); pw.println(mSubscriptions); 81 pw.print(" mRequesting="); pw.println(mRequesting); 82 } 83 84 @Override 85 public void onConnected() { 86 if (DEBUG) Slog.d(TAG, "onConnected"); 87 mLookaheadThreshold = PropConfig.getInt(mContext, "nextalarm.condition.lookahead", 88 R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS; 89 mConnected = true; 90 mTracker.addCallback(mTrackerCallback); 91 } 92 93 @Override 94 public void onDestroy() { 95 super.onDestroy(); 96 if (DEBUG) Slog.d(TAG, "onDestroy"); 97 mTracker.removeCallback(mTrackerCallback); 98 mConnected = false; 99 } 100 101 @Override 102 public void onRequestConditions(int relevance) { 103 if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); 104 if (!mConnected) return; 105 mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0; 106 mTracker.evaluate(); 107 } 108 109 @Override 110 public void onSubscribe(Uri conditionId) { 111 if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); 112 if (tryParseNextAlarmCondition(conditionId) == BAD_CONDITION) { 113 notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition"); 114 return; 115 } 116 mSubscriptions.add(conditionId); 117 mTracker.evaluate(); 118 } 119 120 @Override 121 public void onUnsubscribe(Uri conditionId) { 122 if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); 123 mSubscriptions.remove(conditionId); 124 } 125 126 public void attachBase(Context base) { 127 attachBaseContext(base); 128 } 129 130 public IConditionProvider asInterface() { 131 return (IConditionProvider) onBind(null); 132 } 133 134 private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) { 135 if (alarm == null) return false; 136 final long delta = NextAlarmTracker.getEarlyTriggerTime(alarm) - System.currentTimeMillis(); 137 return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold); 138 } 139 140 private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) { 141 final String formattedAlarm = alarm == null ? "" : mTracker.formatAlarm(alarm); 142 if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state) 143 + " alarm=" + formattedAlarm + " reason=" + reason); 144 notifyCondition(new Condition(id, 145 mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm), 146 mContext.getString(R.string.zen_mode_next_alarm_line_one), 147 formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW)); 148 } 149 150 private Uri newConditionId(AlarmClockInfo nextAlarm) { 151 return new Uri.Builder().scheme(Condition.SCHEME) 152 .authority(ZenModeConfig.SYSTEM_AUTHORITY) 153 .appendPath(ZenModeConfig.NEXT_ALARM_PATH) 154 .appendPath(Integer.toString(mTracker.getCurrentUserId())) 155 .appendPath(Long.toString(nextAlarm.getTriggerTime())) 156 .build(); 157 } 158 159 private long tryParseNextAlarmCondition(Uri conditionId) { 160 return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME) 161 && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY) 162 && conditionId.getPathSegments().size() == 3 163 && conditionId.getPathSegments().get(0).equals(ZenModeConfig.NEXT_ALARM_PATH) 164 && conditionId.getPathSegments().get(1) 165 .equals(Integer.toString(mTracker.getCurrentUserId())) 166 ? tryParseLong(conditionId.getPathSegments().get(2), BAD_CONDITION) 167 : BAD_CONDITION; 168 } 169 170 private static long tryParseLong(String value, long defValue) { 171 if (TextUtils.isEmpty(value)) return defValue; 172 try { 173 return Long.valueOf(value); 174 } catch (NumberFormatException e) { 175 return defValue; 176 } 177 } 178 179 private void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 180 final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm); 181 final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0; 182 if (DEBUG) Slog.d(TAG, "onEvaluate mSubscriptions=" + mSubscriptions 183 + " nextAlarmTime=" + mTracker.formatAlarmDebug(nextAlarmTime) 184 + " nextAlarmWakeup=" + mTracker.formatAlarmDebug(wakeupTime) 185 + " withinThreshold=" + withinThreshold 186 + " booted=" + booted); 187 188 ArraySet<Uri> conditions = mSubscriptions; 189 if (mRequesting && nextAlarm != null && withinThreshold) { 190 final Uri id = newConditionId(nextAlarm); 191 if (!conditions.contains(id)) { 192 conditions = new ArraySet<Uri>(conditions); 193 conditions.add(id); 194 } 195 } 196 for (Uri conditionId : conditions) { 197 final long time = tryParseNextAlarmCondition(conditionId); 198 if (time == BAD_CONDITION) { 199 notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "badCondition"); 200 } else if (!booted) { 201 // we don't know yet 202 if (mSubscriptions.contains(conditionId)) { 203 notifyCondition(conditionId, nextAlarm, Condition.STATE_UNKNOWN, "!booted"); 204 } 205 } else if (time != nextAlarmTime) { 206 // next alarm changed since subscription, consider obsolete 207 notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "changed"); 208 } else if (!withinThreshold) { 209 // next alarm outside threshold or in the past, condition = false 210 notifyCondition(conditionId, nextAlarm, Condition.STATE_FALSE, "!within"); 211 } else { 212 // next alarm within threshold and in the future, condition = true 213 notifyCondition(conditionId, nextAlarm, Condition.STATE_TRUE, "within"); 214 } 215 } 216 } 217 218 private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() { 219 @Override 220 public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) { 221 NextAlarmConditionProvider.this.onEvaluate(nextAlarm, wakeupTime, booted); 222 } 223 }; 224 } 225