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