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;
38import android.util.TimeUtils;
39
40import com.android.internal.R;
41import com.android.server.notification.NotificationManagerService.DumpFilter;
42
43import java.io.PrintWriter;
44import java.text.SimpleDateFormat;
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 static final long SECONDS = 1000;
65    private static final long MINUTES = 60 * SECONDS;
66    private static final long HOURS = 60 * MINUTES;
67
68    private final Context mContext = this;
69    private final DowntimeCalendar mCalendar = new DowntimeCalendar();
70    private final FiredAlarms mFiredAlarms = new FiredAlarms();
71    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
72    private final ConditionProviders mConditionProviders;
73    private final NextAlarmTracker mTracker;
74    private final ZenModeHelper mZenModeHelper;
75
76    private boolean mConnected;
77    private long mLookaheadThreshold;
78    private ZenModeConfig mConfig;
79    private boolean mDowntimed;
80    private boolean mConditionClearing;
81    private boolean mRequesting;
82
83    public DowntimeConditionProvider(ConditionProviders conditionProviders,
84            NextAlarmTracker tracker, ZenModeHelper zenModeHelper) {
85        if (DEBUG) Slog.d(TAG, "new DowntimeConditionProvider()");
86        mConditionProviders = conditionProviders;
87        mTracker = tracker;
88        mZenModeHelper = zenModeHelper;
89    }
90
91    public void dump(PrintWriter pw, DumpFilter filter) {
92        pw.println("    DowntimeConditionProvider:");
93        pw.print("      mConnected="); pw.println(mConnected);
94        pw.print("      mSubscriptions="); pw.println(mSubscriptions);
95        pw.print("      mLookaheadThreshold="); pw.print(mLookaheadThreshold);
96        pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
97        pw.print("      mCalendar="); pw.println(mCalendar);
98        pw.print("      mFiredAlarms="); pw.println(mFiredAlarms);
99        pw.print("      mDowntimed="); pw.println(mDowntimed);
100        pw.print("      mConditionClearing="); pw.println(mConditionClearing);
101        pw.print("      mRequesting="); pw.println(mRequesting);
102    }
103
104    public void attachBase(Context base) {
105        attachBaseContext(base);
106    }
107
108    public IConditionProvider asInterface() {
109        return (IConditionProvider) onBind(null);
110    }
111
112    @Override
113    public void onConnected() {
114        if (DEBUG) Slog.d(TAG, "onConnected");
115        mConnected = true;
116        mLookaheadThreshold = PropConfig.getInt(mContext, "downtime.condition.lookahead",
117                R.integer.config_downtime_condition_lookahead_threshold_hrs) * HOURS;
118        final IntentFilter filter = new IntentFilter();
119        filter.addAction(ENTER_ACTION);
120        filter.addAction(EXIT_ACTION);
121        filter.addAction(Intent.ACTION_TIME_CHANGED);
122        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
123        mContext.registerReceiver(mReceiver, filter);
124        mTracker.addCallback(mTrackerCallback);
125        mZenModeHelper.addCallback(mZenCallback);
126        init();
127    }
128
129    @Override
130    public void onDestroy() {
131        if (DEBUG) Slog.d(TAG, "onDestroy");
132        mTracker.removeCallback(mTrackerCallback);
133        mZenModeHelper.removeCallback(mZenCallback);
134        mConnected = false;
135    }
136
137    @Override
138    public void onRequestConditions(int relevance) {
139        if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance);
140        if (!mConnected) return;
141        mRequesting = (relevance & Condition.FLAG_RELEVANT_NOW) != 0;
142        evaluateSubscriptions();
143    }
144
145    @Override
146    public void onSubscribe(Uri conditionId) {
147        if (DEBUG) Slog.d(TAG, "onSubscribe conditionId=" + conditionId);
148        final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId);
149        if (downtime == null) return;
150        mFiredAlarms.clear();
151        mSubscriptions.add(conditionId);
152        notifyCondition(downtime);
153    }
154
155    private boolean shouldShowCondition() {
156        final long now = System.currentTimeMillis();
157        if (DEBUG) Slog.d(TAG, "shouldShowCondition now=" + mCalendar.isInDowntime(now)
158                + " lookahead="
159                + (mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold)));
160        return mCalendar.isInDowntime(now)
161                || mCalendar.nextDowntimeStart(now) <= (now + mLookaheadThreshold);
162    }
163
164    private void notifyCondition(DowntimeInfo downtime) {
165        if (mConfig == null) {
166            // we don't know yet
167            notifyCondition(createCondition(downtime, Condition.STATE_UNKNOWN));
168            return;
169        }
170        if (!downtime.equals(mConfig.toDowntimeInfo())) {
171            // not the configured downtime, consider it false
172            notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
173            return;
174        }
175        if (!shouldShowCondition()) {
176            // configured downtime, but not within the time range
177            notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
178            return;
179        }
180        if (isZenNone() && mFiredAlarms.findBefore(System.currentTimeMillis())) {
181            // within the configured time range, but wake up if none and the next alarm is fired
182            notifyCondition(createCondition(downtime, Condition.STATE_FALSE));
183            return;
184        }
185        // within the configured time range, condition still valid
186        notifyCondition(createCondition(downtime, Condition.STATE_TRUE));
187    }
188
189    private boolean isZenNone() {
190        return mZenModeHelper.getZenMode() == Global.ZEN_MODE_NO_INTERRUPTIONS;
191    }
192
193    private boolean isZenOff() {
194        return mZenModeHelper.getZenMode() == Global.ZEN_MODE_OFF;
195    }
196
197    private void evaluateSubscriptions() {
198        ArraySet<Uri> conditions = mSubscriptions;
199        if (mConfig != null && mRequesting && shouldShowCondition()) {
200            final Uri id = ZenModeConfig.toDowntimeConditionId(mConfig.toDowntimeInfo());
201            if (!conditions.contains(id)) {
202                conditions = new ArraySet<Uri>(conditions);
203                conditions.add(id);
204            }
205        }
206        for (Uri conditionId : conditions) {
207            final DowntimeInfo downtime = ZenModeConfig.tryParseDowntimeConditionId(conditionId);
208            if (downtime != null) {
209                notifyCondition(downtime);
210            }
211        }
212    }
213
214    @Override
215    public void onUnsubscribe(Uri conditionId) {
216        final boolean current = mSubscriptions.contains(conditionId);
217        if (DEBUG) Slog.d(TAG, "onUnsubscribe conditionId=" + conditionId + " current=" + current);
218        mSubscriptions.remove(conditionId);
219        mFiredAlarms.clear();
220    }
221
222    public void setConfig(ZenModeConfig config) {
223        if (Objects.equals(mConfig, config)) return;
224        final boolean downtimeChanged = mConfig == null || config == null
225                || !mConfig.toDowntimeInfo().equals(config.toDowntimeInfo());
226        mConfig = config;
227        if (DEBUG) Slog.d(TAG, "setConfig downtimeChanged=" + downtimeChanged);
228        if (mConnected && downtimeChanged) {
229            mDowntimed = false;
230            init();
231        }
232        // when active, mark downtime as entered for today
233        if (mConfig != null && mConfig.exitCondition != null
234                && ZenModeConfig.isValidDowntimeConditionId(mConfig.exitCondition.id)) {
235            mDowntimed = true;
236        }
237    }
238
239    public void onManualConditionClearing() {
240        mConditionClearing = true;
241    }
242
243    private Condition createCondition(DowntimeInfo downtime, int state) {
244        if (downtime == null) return null;
245        final Uri id = ZenModeConfig.toDowntimeConditionId(downtime);
246        final String skeleton = DateFormat.is24HourFormat(mContext) ? "Hm" : "hma";
247        final Locale locale = Locale.getDefault();
248        final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
249        final long now = System.currentTimeMillis();
250        long endTime = mCalendar.getNextTime(now, downtime.endHour, downtime.endMinute);
251        if (isZenNone()) {
252            final AlarmClockInfo nextAlarm = mTracker.getNextAlarm();
253            final long nextAlarmTime = nextAlarm != null ? nextAlarm.getTriggerTime() : 0;
254            if (nextAlarmTime > now && nextAlarmTime < endTime) {
255                endTime = nextAlarmTime;
256            }
257        }
258        final String formatted = new SimpleDateFormat(pattern, locale).format(new Date(endTime));
259        final String summary = mContext.getString(R.string.downtime_condition_summary, formatted);
260        final String line1 = mContext.getString(R.string.downtime_condition_line_one);
261        return new Condition(id, summary, line1, formatted, 0, state, Condition.FLAG_RELEVANT_NOW);
262    }
263
264    private void init() {
265        mCalendar.setDowntimeInfo(mConfig != null ? mConfig.toDowntimeInfo() : null);
266        evaluateSubscriptions();
267        updateAlarms();
268        evaluateAutotrigger();
269    }
270
271    private void updateAlarms() {
272        if (mConfig == null) return;
273        updateAlarm(ENTER_ACTION, ENTER_CODE, mConfig.sleepStartHour, mConfig.sleepStartMinute);
274        updateAlarm(EXIT_ACTION, EXIT_CODE, mConfig.sleepEndHour, mConfig.sleepEndMinute);
275    }
276
277
278    private void updateAlarm(String action, int requestCode, int hr, int min) {
279        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
280        final long now = System.currentTimeMillis();
281        final long time = mCalendar.getNextTime(now, hr, min);
282        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
283                new Intent(action)
284                    .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
285                    .putExtra(EXTRA_TIME, time),
286                PendingIntent.FLAG_UPDATE_CURRENT);
287        alarms.cancel(pendingIntent);
288        if (mConfig.sleepMode != null) {
289            if (DEBUG) Slog.d(TAG, String.format("Scheduling %s for %s, in %s, now=%s",
290                    action, ts(time), NextAlarmTracker.formatDuration(time - now), ts(now)));
291            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
292        }
293    }
294
295    private static String ts(long time) {
296        return new Date(time) + " (" + time + ")";
297    }
298
299    private void onEvaluateNextAlarm(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
300        if (!booted) return;  // we don't know yet
301        if (DEBUG) Slog.d(TAG, "onEvaluateNextAlarm " + mTracker.formatAlarmDebug(nextAlarm));
302        if (nextAlarm != null && wakeupTime > 0 && System.currentTimeMillis() > wakeupTime) {
303            if (DEBUG) Slog.d(TAG, "Alarm fired: " + mTracker.formatAlarmDebug(wakeupTime));
304            mFiredAlarms.add(wakeupTime);
305        }
306        evaluateSubscriptions();
307    }
308
309    private void evaluateAutotrigger() {
310        String skipReason = null;
311        if (mConfig == null) {
312            skipReason = "no config";
313        } else if (mDowntimed) {
314            skipReason = "already downtimed";
315        } else if (mZenModeHelper.getZenMode() != Global.ZEN_MODE_OFF) {
316            skipReason = "already in zen";
317        } else if (!mCalendar.isInDowntime(System.currentTimeMillis())) {
318            skipReason = "not in downtime";
319        }
320        if (skipReason != null) {
321            ZenLog.traceDowntimeAutotrigger("Autotrigger skipped: " + skipReason);
322            return;
323        }
324        ZenLog.traceDowntimeAutotrigger("Autotrigger fired");
325        mZenModeHelper.setZenMode(mConfig.sleepNone ? Global.ZEN_MODE_NO_INTERRUPTIONS
326                : Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, "downtime");
327        final Condition condition = createCondition(mConfig.toDowntimeInfo(), Condition.STATE_TRUE);
328        mConditionProviders.setZenModeCondition(condition, "downtime");
329    }
330
331    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
332        @Override
333        public void onReceive(Context context, Intent intent) {
334            final String action = intent.getAction();
335            final long now = System.currentTimeMillis();
336            if (ENTER_ACTION.equals(action) || EXIT_ACTION.equals(action)) {
337                final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
338                if (DEBUG) Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
339                        action, ts(schTime), ts(now), now - schTime));
340                if (ENTER_ACTION.equals(action)) {
341                    evaluateAutotrigger();
342                } else /*EXIT_ACTION*/ {
343                    mDowntimed = false;
344                }
345                mFiredAlarms.clear();
346            } else if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
347                if (DEBUG) Slog.d(TAG, "timezone changed to " + TimeZone.getDefault());
348                mCalendar.setTimeZone(TimeZone.getDefault());
349                mFiredAlarms.clear();
350            } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
351                if (DEBUG) Slog.d(TAG, "time changed to " + now);
352                mFiredAlarms.clear();
353            } else {
354                if (DEBUG) Slog.d(TAG, action + " fired at " + now);
355            }
356            evaluateSubscriptions();
357            updateAlarms();
358        }
359    };
360
361    private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() {
362        @Override
363        public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
364            DowntimeConditionProvider.this.onEvaluateNextAlarm(nextAlarm, wakeupTime, booted);
365        }
366    };
367
368    private final ZenModeHelper.Callback mZenCallback = new ZenModeHelper.Callback() {
369        @Override
370        void onZenModeChanged() {
371            if (mConditionClearing && isZenOff()) {
372                evaluateAutotrigger();
373            }
374            mConditionClearing = false;
375            evaluateSubscriptions();
376        }
377    };
378
379    private class FiredAlarms {
380        private final ArraySet<Long> mFiredAlarms = new ArraySet<Long>();
381
382        @Override
383        public String toString() {
384            final StringBuilder sb = new StringBuilder();
385            for (int i = 0; i < mFiredAlarms.size(); i++) {
386                if (i > 0) sb.append(',');
387                sb.append(mTracker.formatAlarmDebug(mFiredAlarms.valueAt(i)));
388            }
389            return sb.toString();
390        }
391
392        public void add(long firedAlarm) {
393            mFiredAlarms.add(firedAlarm);
394        }
395
396        public void clear() {
397            mFiredAlarms.clear();
398        }
399
400        public boolean findBefore(long time) {
401            for (int i = 0; i < mFiredAlarms.size(); i++) {
402                if (mFiredAlarms.valueAt(i) < time) {
403                    return true;
404                }
405            }
406            return false;
407        }
408    }
409}
410