DowntimeConditionProvider.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.AlarmManager; 20import android.app.PendingIntent; 21import android.content.BroadcastReceiver; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.net.Uri; 27import android.service.notification.Condition; 28import android.service.notification.ConditionProviderService; 29import android.service.notification.IConditionProvider; 30import android.service.notification.ZenModeConfig; 31import android.service.notification.ZenModeConfig.DowntimeInfo; 32import android.text.format.DateFormat; 33import android.util.ArraySet; 34import android.util.Log; 35import android.util.Slog; 36 37import com.android.internal.R; 38import com.android.server.notification.NotificationManagerService.DumpFilter; 39 40import java.io.PrintWriter; 41import java.text.SimpleDateFormat; 42import java.util.Calendar; 43import java.util.Date; 44import java.util.Locale; 45import java.util.Objects; 46import java.util.TimeZone; 47 48/** Built-in zen condition provider for managing downtime */ 49public class DowntimeConditionProvider extends ConditionProviderService { 50 private static final String TAG = "DowntimeConditions"; 51 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 52 53 public static final ComponentName COMPONENT = 54 new ComponentName("android", DowntimeConditionProvider.class.getName()); 55 56 private static final String ENTER_ACTION = TAG + ".enter"; 57 private static final int ENTER_CODE = 100; 58 private static final String EXIT_ACTION = TAG + ".exit"; 59 private static final int EXIT_CODE = 101; 60 private static final String EXTRA_TIME = "time"; 61 62 private final Calendar mCalendar = Calendar.getInstance(); 63 private final Context mContext = this; 64 private final ArraySet<Integer> mDays = new ArraySet<Integer>(); 65 66 private boolean mConnected; 67 private boolean mInDowntime; 68 private ZenModeConfig mConfig; 69 private Callback mCallback; 70 71 public DowntimeConditionProvider() { 72 if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()"); 73 } 74 75 public void dump(PrintWriter pw, DumpFilter filter) { 76 pw.println(" DowntimeConditionProvider:"); 77 pw.print(" mConnected="); pw.println(mConnected); 78 pw.print(" mInDowntime="); pw.println(mInDowntime); 79 } 80 81 public void attachBase(Context base) { 82 attachBaseContext(base); 83 } 84 85 public IConditionProvider asInterface() { 86 return (IConditionProvider) onBind(null); 87 } 88 89 public void setCallback(Callback callback) { 90 mCallback = callback; 91 } 92 93 @Override 94 public void onConnected() { 95 if (DEBUG) Slog.d(TAG, "onConnected"); 96 mConnected = true; 97 final IntentFilter filter = new IntentFilter(); 98 filter.addAction(ENTER_ACTION); 99 filter.addAction(EXIT_ACTION); 100 filter.addAction(Intent.ACTION_TIME_CHANGED); 101 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 102 mContext.registerReceiver(mReceiver, filter); 103 init(); 104 } 105 106 @Override 107 public void onDestroy() { 108 if (DEBUG) Slog.d(TAG, "onDestroy"); 109 mConnected = false; 110 } 111 112 @Override 113 public void onRequestConditions(int relevance) { 114 if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); 115 if ((relevance & Condition.FLAG_RELEVANT_NOW) != 0) { 116 if (mInDowntime && mConfig != null) { 117 notifyCondition(createCondition(mConfig.toDowntimeInfo(), Condition.STATE_TRUE)); 118 } 119 } 120 } 121 122 @Override 123 public void onSubscribe(Uri conditionId) { 124 if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId); 125 final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId); 126 if (downtime != null && mConfig != null) { 127 final int state = mConfig.toDowntimeInfo().equals(downtime) && mInDowntime 128 ? Condition.STATE_TRUE : Condition.STATE_FALSE; 129 if (DEBUG) Slog.d(TAG, "notify condition state: " + Condition.stateToString(state)); 130 notifyCondition(createCondition(downtime, state)); 131 } 132 } 133 134 @Override 135 public void onUnsubscribe(Uri conditionId) { 136 if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId); 137 } 138 139 public void setConfig(ZenModeConfig config) { 140 if (Objects.equals(mConfig, config)) return; 141 if (DEBUG) Slog.d(TAG, "setConfig"); 142 mConfig = config; 143 if (mConnected) { 144 init(); 145 } 146 } 147 148 public boolean isInDowntime() { 149 return mInDowntime; 150 } 151 152 public Condition createCondition(DowntimeInfo downtime, int state) { 153 if (downtime == null) return null; 154 final Uri id = ZenModeConfig.toDowntimeConditionId(downtime); 155 final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma"; 156 final Locale locale = Locale.getDefault(); 157 final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); 158 final long time = getTime(System.currentTimeMillis(), downtime.endHour, downtime.endMinute); 159 final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(time)); 160 final String summary = mContext.getString(R.string.downtime_condition_summary, formatted); 161 final String line1 = mContext.getString(R.string.downtime_condition_line_one); 162 return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW); 163 } 164 165 public boolean isDowntimeCondition(Condition condition) { 166 return condition != null && ZenModeConfig.isValidDowntimeConditionId(condition.id); 167 } 168 169 private void init() { 170 updateDays(); 171 reevaluateDowntime(); 172 updateAlarms(); 173 } 174 175 private void updateDays() { 176 mDays.clear(); 177 if (mConfig != null) { 178 final int[] days = ZenModeConfig.tryParseDays(mConfig.sleepMode); 179 for (int i = 0; days != null && i < days.length; i++) { 180 mDays.add(days[i]); 181 } 182 } 183 } 184 185 private boolean isInDowntime(long time) { 186 if (mConfig == null || mDays.size() == 0) return false; 187 final long start = getTime(time, mConfig.sleepStartHour, mConfig.sleepStartMinute); 188 long end = getTime(time, mConfig.sleepEndHour, mConfig.sleepEndMinute); 189 if (start == end) return false; 190 if (end < start) { 191 end = addDays(end, 1); 192 } 193 return isInDowntime(-1, time, start, end) || isInDowntime(0, time, start, end); 194 } 195 196 private boolean isInDowntime(int daysOffset, long time, long start, long end) { 197 final int n = Calendar.SATURDAY; 198 final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1; 199 start = addDays(start, daysOffset); 200 end = addDays(end, daysOffset); 201 return mDays.contains(day) && time >= start && time < end; 202 } 203 204 private void reevaluateDowntime() { 205 final boolean inDowntime = isInDowntime(System.currentTimeMillis()); 206 if (DEBUG) Slog.d(TAG, "inDowntime=" + inDowntime); 207 if (inDowntime == mInDowntime) return; 208 Slog.i(TAG, (inDowntime ? "Entering" : "Exiting" ) + " downtime"); 209 mInDowntime = inDowntime; 210 ZenLog.traceDowntime(mInDowntime, getDayOfWeek(System.currentTimeMillis()), mDays); 211 fireDowntimeChanged(); 212 } 213 214 private void fireDowntimeChanged() { 215 if (mCallback != null) { 216 mCallback.onDowntimeChanged(mInDowntime); 217 } 218 } 219 220 private void updateAlarms() { 221 if (mConfig == null) return; 222 updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute); 223 updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute); 224 } 225 226 private int getDayOfWeek(long time) { 227 mCalendar.setTimeInMillis(time); 228 return mCalendar.get(Calendar.DAY_OF_WEEK); 229 } 230 231 private long getTime(long millis, int hour, int min) { 232 mCalendar.setTimeInMillis(millis); 233 mCalendar.set(Calendar.HOUR_OF_DAY, hour); 234 mCalendar.set(Calendar.MINUTE, min); 235 mCalendar.set(Calendar.SECOND, 0); 236 mCalendar.set(Calendar.MILLISECOND, 0); 237 return mCalendar.getTimeInMillis(); 238 } 239 240 private long addDays(long time, int days) { 241 mCalendar.setTimeInMillis(time); 242 mCalendar.add(Calendar.DATE, days); 243 return mCalendar.getTimeInMillis(); 244 } 245 246 private void updateAlarm(String action, int requestCode, int hr, int min) { 247 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 248 final long now = System.currentTimeMillis(); 249 mCalendar.setTimeInMillis(now); 250 mCalendar.set(Calendar.HOUR_OF_DAY, hr); 251 mCalendar.set(Calendar.MINUTE, min); 252 mCalendar.set(Calendar.SECOND, 0); 253 mCalendar.set(Calendar.MILLISECOND, 0); 254 long time = mCalendar.getTimeInMillis(); 255 if (time <= now) { 256 time = addDays(time, 1); 257 } 258 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, 259 new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT); 260 alarms.cancel(pendingIntent); 261 if (mConfig.sleepMode != null) { 262 if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s", 263 action, ts(time), time - now, ts(now))); 264 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 265 } 266 } 267 268 private static String ts(long time) { 269 return new Date(time) + " (" + time + ")"; 270 } 271 272 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 273 @Override 274 public void onReceive(Context context, Intent intent) { 275 final String action = intent.getAction(); 276 final long now = System.currentTimeMillis(); 277 if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) { 278 final long schTime = intent.getLongExtra(EXTRA_TIME, 0); 279 if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", 280 action, ts(schTime), ts(now), now - schTime)); 281 } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { 282 if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault()); 283 mCalendar.setTimeZone(TimeZone.getDefault()); 284 } else { 285 if (DEBUG) Slog.d(TAG, action + " fired at " + now); 286 } 287 reevaluateDowntime(); 288 updateAlarms(); 289 } 290 }; 291 292 public interface Callback { 293 void onDowntimeChanged(boolean inDowntime); 294 } 295} 296