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