DowntimeConditionProvider.java revision 3c984d67ebf07e1215c6b403f0e18e309f2604e9
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(), Condition.STATE_TRUE));
126            }
127        }
128    }
129
130    @Override
131    public void onSubscribe(Uri conditionId) {
132        if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId);
133        final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId);
134        if (downtime != null && mConfig != null) {
135            final int state = mConfig.toDowntimeInfo().equals(downtime) && isInDowntime()
136                    ? Condition.STATE_TRUE : Condition.STATE_FALSE;
137            if (DEBUG) Slog.d(TAG, "notify condition state: " + Condition.stateToString(state));
138            notifyCondition(createCondition(downtime, state));
139        }
140    }
141
142    @Override
143    public void onUnsubscribe(Uri conditionId) {
144        if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId);
145    }
146
147    public void setConfig(ZenModeConfig config) {
148        if (Objects.equals(mConfig, config)) return;
149        if (DEBUG) Slog.d(TAG, "setConfig");
150        mConfig = config;
151        if (mConnected) {
152            init();
153        }
154    }
155
156    public boolean isInDowntime() {
157        return mDowntimeMode != Global.ZEN_MODE_OFF;
158    }
159
160    public Condition createCondition(DowntimeInfo downtime, int state) {
161        if (downtime == null) return null;
162        final Uri id = ZenModeConfig.toDowntimeConditionId(downtime);
163        final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma";
164        final Locale locale = Locale.getDefault();
165        final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
166        final long time = getTime(System.currentTimeMillis(), downtime.endHour, downtime.endMinute);
167        final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(time));
168        final String summary = mContext.getString(R.string.downtime_condition_summary, formatted);
169        final String line1 = mContext.getString(R.string.downtime_condition_line_one);
170        return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW);
171    }
172
173    public boolean isDowntimeCondition(Condition condition) {
174        return condition != null && ZenModeConfig.isValidDowntimeConditionId(condition.id);
175    }
176
177    private void init() {
178        updateDays();
179        reevaluateDowntime();
180        updateAlarms();
181    }
182
183    private void updateDays() {
184        mDays.clear();
185        if (mConfig != null) {
186            final int[] days = ZenModeConfig.tryParseDays(mConfig.sleepMode);
187            for (int i = 0; days != null && i < days.length; i++) {
188                mDays.add(days[i]);
189            }
190        }
191    }
192
193    private boolean isInDowntime(long time) {
194        if (mConfig == null || mDays.size() == 0) return false;
195        final long start = getTime(time, mConfig.sleepStartHour, mConfig.sleepStartMinute);
196        long end = getTime(time, mConfig.sleepEndHour, mConfig.sleepEndMinute);
197        if (start == end) return false;
198        if (end < start) {
199            end = addDays(end, 1);
200        }
201        final boolean orAlarm = mConfig.sleepNone;
202        return isInDowntime(-1, time, start, end, orAlarm)
203                || isInDowntime(0, time, start, end, orAlarm);
204    }
205
206    private boolean isInDowntime(int daysOffset, long time, long start, long end, boolean orAlarm) {
207        final int n = Calendar.SATURDAY;
208        final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
209        start = addDays(start, daysOffset);
210        end = addDays(end, daysOffset);
211        if (orAlarm) {
212            end = findFiredAlarm(start, end);
213        }
214        return mDays.contains(day) && time >= start && time < end;
215    }
216
217    private long findFiredAlarm(long start, long end) {
218        final int N = mFiredAlarms.size();
219        for (int i = 0; i < N; i++) {
220            final long firedAlarm = mFiredAlarms.valueAt(i);
221            if (firedAlarm > start && firedAlarm < end) {
222                return firedAlarm;
223            }
224        }
225        return end;
226    }
227
228    private void reevaluateDowntime() {
229        final long now = System.currentTimeMillis();
230        final boolean inDowntimeNow = isInDowntime(now);
231        final int downtimeMode = inDowntimeNow ? (mConfig.sleepNone
232                ? Global.ZEN_MODE_NO_INTERRUPTIONS : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
233                : Global.ZEN_MODE_OFF;
234        if (DEBUG) Slog.d(TAG, "downtimeMode=" + downtimeMode);
235        if (downtimeMode == mDowntimeMode) return;
236        mDowntimeMode = downtimeMode;
237        Slog.i(TAG, (isInDowntime() ? "Entering" : "Exiting" ) + " downtime");
238        ZenLog.traceDowntime(mDowntimeMode, getDayOfWeek(now), mDays);
239        fireDowntimeChanged();
240    }
241
242    private void fireDowntimeChanged() {
243        if (mCallback != null) {
244            mCallback.onDowntimeChanged(mDowntimeMode);
245        }
246    }
247
248    private void updateAlarms() {
249        if (mConfig == null) return;
250        updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute);
251        updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute);
252    }
253
254    private int getDayOfWeek(long time) {
255        mCalendar.setTimeInMillis(time);
256        return mCalendar.get(Calendar.DAY_OF_WEEK);
257    }
258
259    private long getTime(long millis, int hour, int min) {
260        mCalendar.setTimeInMillis(millis);
261        mCalendar.set(Calendar.HOUR_OF_DAY, hour);
262        mCalendar.set(Calendar.MINUTE, min);
263        mCalendar.set(Calendar.SECOND, 0);
264        mCalendar.set(Calendar.MILLISECOND, 0);
265        return mCalendar.getTimeInMillis();
266    }
267
268    private long addDays(long time, int days) {
269        mCalendar.setTimeInMillis(time);
270        mCalendar.add(Calendar.DATE, days);
271        return mCalendar.getTimeInMillis();
272    }
273
274    private void updateAlarm(String action, int requestCode, int hr, int min) {
275        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
276        final long now = System.currentTimeMillis();
277        mCalendar.setTimeInMillis(now);
278        mCalendar.set(Calendar.HOUR_OF_DAY, hr);
279        mCalendar.set(Calendar.MINUTE, min);
280        mCalendar.set(Calendar.SECOND, 0);
281        mCalendar.set(Calendar.MILLISECOND, 0);
282        long time = mCalendar.getTimeInMillis();
283        if (time <= now) {
284            time = addDays(time, 1);
285        }
286        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
287                new Intent(action)
288                    .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
289                    .putExtra(EXTRA_TIME, time),
290                PendingIntent.FLAG_UPDATE_CURRENT);
291        alarms.cancel(pendingIntent);
292        if (mConfig.sleepMode != null) {
293            if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s",
294                    action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now)));
295            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
296        }
297    }
298
299    private static String ts(long time) {
300        return new Date(time) + " (" + time + ")";
301    }
302
303    private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
304        if (!booted) return;  // we don't know yet
305        if (nextAlarm == null) return;  // not fireable
306        if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm));
307        if (System.currentTimeMillis() > wakeupTime) {
308            if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime));
309            trimFiredAlarms();
310            mFiredAlarms.add(wakeupTime);
311        }
312        reevaluateDowntime();
313    }
314
315    private void trimFiredAlarms() {
316        // remove fired alarms over 2 days old
317        final long keepAfter = System.currentTimeMillis() - 2 * 24 * 60 * 60 * 1000;
318        final int N = mFiredAlarms.size();
319        for (int i = N - 1; i >= 0; i--) {
320            final long firedAlarm = mFiredAlarms.valueAt(i);
321            if (firedAlarm < keepAfter) {
322                mFiredAlarms.removeAt(i);
323            }
324        }
325    }
326
327    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
328        @Override
329        public void onReceive(Context context, Intent intent) {
330            final String action = intent.getAction();
331            final long now = System.currentTimeMillis();
332            if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) {
333                final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
334                if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
335                        action, ts(schTime), ts(now), now - schTime));
336            } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
337                if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault());
338                mCalendar.setTimeZone(TimeZone.getDefault());
339            } else {
340                if (DEBUG) Slog.d(TAG, action + " fired at " + now);
341            }
342            reevaluateDowntime();
343            updateAlarms();
344        }
345    };
346
347    private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() {
348        @Override
349        public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
350            DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted);
351        }
352    };
353
354    public interface Callback {
355        void onDowntimeChanged(int downtimeMode);
356        NextAlarmTracker getNextAlarmTracker();
357    }
358}
359