1/*
2 * Copyright (C) 2015 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.ActivityManager;
20import android.app.AlarmManager;
21import android.app.PendingIntent;
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.os.Binder;
29import android.provider.Settings;
30import android.service.notification.Condition;
31import android.service.notification.IConditionProvider;
32import android.service.notification.ZenModeConfig;
33import android.service.notification.ZenModeConfig.ScheduleInfo;
34import android.text.TextUtils;
35import android.util.ArrayMap;
36import android.util.ArraySet;
37import android.util.Log;
38import android.util.Slog;
39
40import com.android.server.notification.NotificationManagerService.DumpFilter;
41
42import java.io.PrintWriter;
43import java.util.ArrayList;
44import java.util.Calendar;
45import java.util.List;
46import java.util.TimeZone;
47
48/**
49 * Built-in zen condition provider for daily scheduled time-based conditions.
50 */
51public class ScheduleConditionProvider extends SystemConditionProviderService {
52    static final String TAG = "ConditionProviders.SCP";
53    static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
54
55    public static final ComponentName COMPONENT =
56            new ComponentName("android", ScheduleConditionProvider.class.getName());
57    private static final String NOT_SHOWN = "...";
58    private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
59    private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
60    private static final int REQUEST_CODE_EVALUATE = 1;
61    private static final String EXTRA_TIME = "time";
62    private static final String SEPARATOR = ";";
63    private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
64
65
66    private final Context mContext = this;
67    private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
68    private ArraySet<Uri> mSnoozed = new ArraySet<>();
69
70    private AlarmManager mAlarmManager;
71    private boolean mConnected;
72    private boolean mRegistered;
73    private long mNextAlarmTime;
74
75    public ScheduleConditionProvider() {
76        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
77    }
78
79    @Override
80    public ComponentName getComponent() {
81        return COMPONENT;
82    }
83
84    @Override
85    public boolean isValidConditionId(Uri id) {
86        return ZenModeConfig.isValidScheduleConditionId(id);
87    }
88
89    @Override
90    public void dump(PrintWriter pw, DumpFilter filter) {
91        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
92        pw.print("      mConnected="); pw.println(mConnected);
93        pw.print("      mRegistered="); pw.println(mRegistered);
94        pw.println("      mSubscriptions=");
95        final long now = System.currentTimeMillis();
96        synchronized (mSubscriptions) {
97            for (Uri conditionId : mSubscriptions.keySet()) {
98                pw.print("        ");
99                pw.print(meetsSchedule(mSubscriptions.get(conditionId), now) ? "* " : "  ");
100                pw.println(conditionId);
101                pw.print("            ");
102                pw.println(mSubscriptions.get(conditionId).toString());
103            }
104        }
105        pw.println("      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
106        dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
107    }
108
109    @Override
110    public void onConnected() {
111        if (DEBUG) Slog.d(TAG, "onConnected");
112        mConnected = true;
113        readSnoozed();
114    }
115
116    @Override
117    public void onBootComplete() {
118        // noop
119    }
120
121    @Override
122    public void onDestroy() {
123        super.onDestroy();
124        if (DEBUG) Slog.d(TAG, "onDestroy");
125        mConnected = false;
126    }
127
128    @Override
129    public void onSubscribe(Uri conditionId) {
130        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
131        if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
132            notifyCondition(createCondition(conditionId, Condition.STATE_FALSE, "badCondition"));
133            return;
134        }
135        synchronized (mSubscriptions) {
136            mSubscriptions.put(conditionId, toScheduleCalendar(conditionId));
137        }
138        evaluateSubscriptions();
139    }
140
141    @Override
142    public void onUnsubscribe(Uri conditionId) {
143        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
144        synchronized (mSubscriptions) {
145            mSubscriptions.remove(conditionId);
146        }
147        removeSnoozed(conditionId);
148        evaluateSubscriptions();
149    }
150
151    @Override
152    public void attachBase(Context base) {
153        attachBaseContext(base);
154    }
155
156    @Override
157    public IConditionProvider asInterface() {
158        return (IConditionProvider) onBind(null);
159    }
160
161    private void evaluateSubscriptions() {
162        if (mAlarmManager == null) {
163            mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
164        }
165        final long now = System.currentTimeMillis();
166        mNextAlarmTime = 0;
167        long nextUserAlarmTime = getNextAlarm();
168        List<Condition> conditionsToNotify = new ArrayList<>();
169        synchronized (mSubscriptions) {
170            setRegistered(!mSubscriptions.isEmpty());
171            for (Uri conditionId : mSubscriptions.keySet()) {
172                final ScheduleCalendar cal = mSubscriptions.get(conditionId);
173                if (cal != null && cal.isInSchedule(now)) {
174                    if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
175                        conditionsToNotify.add(createCondition(
176                                conditionId, Condition.STATE_FALSE, "alarmCanceled"));
177                        addSnoozed(conditionId);
178                    } else {
179                        conditionsToNotify.add(createCondition(
180                                conditionId, Condition.STATE_TRUE, "meetsSchedule"));
181                    }
182                    cal.maybeSetNextAlarm(now, nextUserAlarmTime);
183                } else {
184                    conditionsToNotify.add(createCondition(
185                            conditionId, Condition.STATE_FALSE, "!meetsSchedule"));
186                    removeSnoozed(conditionId);
187                    if (cal != null && nextUserAlarmTime == 0) {
188                        cal.maybeSetNextAlarm(now, nextUserAlarmTime);
189                    }
190                }
191                if (cal != null) {
192                    final long nextChangeTime = cal.getNextChangeTime(now);
193                    if (nextChangeTime > 0 && nextChangeTime > now) {
194                        if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
195                            mNextAlarmTime = nextChangeTime;
196                        }
197                    }
198                }
199            }
200        }
201        notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
202        updateAlarm(now, mNextAlarmTime);
203    }
204
205    private void updateAlarm(long now, long time) {
206        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
207        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
208                REQUEST_CODE_EVALUATE,
209                new Intent(ACTION_EVALUATE)
210                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
211                        .putExtra(EXTRA_TIME, time),
212                PendingIntent.FLAG_UPDATE_CURRENT);
213        alarms.cancel(pendingIntent);
214        if (time > now) {
215            if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
216                    ts(time), formatDuration(time - now), ts(now)));
217            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
218        } else {
219            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
220        }
221    }
222
223    public long getNextAlarm() {
224        final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
225                ActivityManager.getCurrentUser());
226        return info != null ? info.getTriggerTime() : 0;
227    }
228
229    private boolean meetsSchedule(ScheduleCalendar cal, long time) {
230        return cal != null && cal.isInSchedule(time);
231    }
232
233    private static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
234        final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
235        if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
236        final ScheduleCalendar sc = new ScheduleCalendar();
237        sc.setSchedule(schedule);
238        sc.setTimeZone(TimeZone.getDefault());
239        return sc;
240    }
241
242    private void setRegistered(boolean registered) {
243        if (mRegistered == registered) return;
244        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
245        mRegistered = registered;
246        if (mRegistered) {
247            final IntentFilter filter = new IntentFilter();
248            filter.addAction(Intent.ACTION_TIME_CHANGED);
249            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
250            filter.addAction(ACTION_EVALUATE);
251            filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
252            registerReceiver(mReceiver, filter);
253        } else {
254            unregisterReceiver(mReceiver);
255        }
256    }
257
258    private Condition createCondition(Uri id, int state, String reason) {
259        if (DEBUG) Slog.d(TAG, "notifyCondition " + id
260                + " " + Condition.stateToString(state)
261                + " reason=" + reason);
262        final String summary = NOT_SHOWN;
263        final String line1 = NOT_SHOWN;
264        final String line2 = NOT_SHOWN;
265        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
266    }
267
268    private boolean conditionSnoozed(Uri conditionId) {
269        synchronized (mSnoozed) {
270            return mSnoozed.contains(conditionId);
271        }
272    }
273
274    private void addSnoozed(Uri conditionId) {
275        synchronized (mSnoozed) {
276            mSnoozed.add(conditionId);
277            saveSnoozedLocked();
278        }
279    }
280
281    private void removeSnoozed(Uri conditionId) {
282        synchronized (mSnoozed) {
283            mSnoozed.remove(conditionId);
284            saveSnoozedLocked();
285        }
286    }
287
288    public void saveSnoozedLocked() {
289        final String setting = TextUtils.join(SEPARATOR, mSnoozed);
290        final int currentUser = ActivityManager.getCurrentUser();
291        Settings.Secure.putStringForUser(mContext.getContentResolver(),
292                SCP_SETTING,
293                setting,
294                currentUser);
295    }
296
297    public void readSnoozed() {
298        synchronized (mSnoozed) {
299            long identity = Binder.clearCallingIdentity();
300            try {
301                final String setting = Settings.Secure.getStringForUser(
302                        mContext.getContentResolver(),
303                        SCP_SETTING,
304                        ActivityManager.getCurrentUser());
305                if (setting != null) {
306                    final String[] tokens = setting.split(SEPARATOR);
307                    for (int i = 0; i < tokens.length; i++) {
308                        String token = tokens[i];
309                        if (token != null) {
310                            token = token.trim();
311                        }
312                        if (TextUtils.isEmpty(token)) {
313                            continue;
314                        }
315                        mSnoozed.add(Uri.parse(token));
316                    }
317                }
318            } finally {
319                Binder.restoreCallingIdentity(identity);
320            }
321        }
322    }
323
324    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
325        @Override
326        public void onReceive(Context context, Intent intent) {
327            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
328            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
329                synchronized (mSubscriptions) {
330                    for (Uri conditionId : mSubscriptions.keySet()) {
331                        final ScheduleCalendar cal = mSubscriptions.get(conditionId);
332                        if (cal != null) {
333                            cal.setTimeZone(Calendar.getInstance().getTimeZone());
334                        }
335                    }
336                }
337            }
338            evaluateSubscriptions();
339        }
340    };
341
342}
343