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.ScheduleCalendar;
33import android.service.notification.ZenModeConfig;
34import android.text.TextUtils;
35import android.util.ArrayMap;
36import android.util.ArraySet;
37import android.util.Log;
38import android.util.Slog;
39
40import com.android.internal.annotations.GuardedBy;
41import com.android.internal.annotations.VisibleForTesting;
42import com.android.server.notification.NotificationManagerService.DumpFilter;
43
44import java.io.PrintWriter;
45import java.util.ArrayList;
46import java.util.Calendar;
47import java.util.List;
48
49/**
50 * Built-in zen condition provider for daily scheduled time-based conditions.
51 */
52public class ScheduleConditionProvider extends SystemConditionProviderService {
53    static final String TAG = "ConditionProviders.SCP";
54    static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
55
56    public static final ComponentName COMPONENT =
57            new ComponentName("android", ScheduleConditionProvider.class.getName());
58    private static final String NOT_SHOWN = "...";
59    private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
60    private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
61    private static final int REQUEST_CODE_EVALUATE = 1;
62    private static final String EXTRA_TIME = "time";
63    private static final String SEPARATOR = ";";
64    private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
65
66    private final Context mContext = this;
67    private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
68    private ArraySet<Uri> mSnoozedForAlarm = 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, mSnoozedForAlarm));
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_ERROR, "invalidId"));
133            return;
134        }
135        synchronized (mSubscriptions) {
136            mSubscriptions.put(conditionId, ZenModeConfig.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                Condition condition =
173                        evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
174                                now, nextUserAlarmTime);
175                if (condition != null) {
176                    conditionsToNotify.add(condition);
177                }
178            }
179        }
180        notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
181        updateAlarm(now, mNextAlarmTime);
182    }
183
184    @VisibleForTesting
185    @GuardedBy("mSubscriptions")
186    Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
187            long now, long nextUserAlarmTime) {
188        if (DEBUG) Slog.d(TAG, String.format("evaluateSubscriptionLocked cal=%s, now=%s, "
189                        + "nextUserAlarmTime=%s", cal, ts(now), ts(nextUserAlarmTime)));
190        Condition condition;
191        if (cal == null) {
192            condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
193            removeSnoozed(conditionId);
194            return condition;
195        }
196        if (cal.isInSchedule(now)) {
197            if (conditionSnoozed(conditionId)) {
198                condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
199            } else if (cal.shouldExitForAlarm(now)) {
200                condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
201                addSnoozed(conditionId);
202            } else {
203                condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
204            }
205        } else {
206            condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
207            removeSnoozed(conditionId);
208        }
209        cal.maybeSetNextAlarm(now, nextUserAlarmTime);
210        final long nextChangeTime = cal.getNextChangeTime(now);
211        if (nextChangeTime > 0 && nextChangeTime > now) {
212            if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
213                mNextAlarmTime = nextChangeTime;
214            }
215        }
216        return condition;
217    }
218
219    private void updateAlarm(long now, long time) {
220        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
221        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
222                REQUEST_CODE_EVALUATE,
223                new Intent(ACTION_EVALUATE)
224                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
225                        .putExtra(EXTRA_TIME, time),
226                PendingIntent.FLAG_UPDATE_CURRENT);
227        alarms.cancel(pendingIntent);
228        if (time > now) {
229            if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
230                    ts(time), formatDuration(time - now), ts(now)));
231            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
232        } else {
233            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
234        }
235    }
236
237    public long getNextAlarm() {
238        final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
239                ActivityManager.getCurrentUser());
240        return info != null ? info.getTriggerTime() : 0;
241    }
242
243    private boolean meetsSchedule(ScheduleCalendar cal, long time) {
244        return cal != null && cal.isInSchedule(time);
245    }
246
247    private void setRegistered(boolean registered) {
248        if (mRegistered == registered) return;
249        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
250        mRegistered = registered;
251        if (mRegistered) {
252            final IntentFilter filter = new IntentFilter();
253            filter.addAction(Intent.ACTION_TIME_CHANGED);
254            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
255            filter.addAction(ACTION_EVALUATE);
256            filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
257            registerReceiver(mReceiver, filter);
258        } else {
259            unregisterReceiver(mReceiver);
260        }
261    }
262
263    private Condition createCondition(Uri id, int state, String reason) {
264        if (DEBUG) Slog.d(TAG, "notifyCondition " + id
265                + " " + Condition.stateToString(state)
266                + " reason=" + reason);
267        final String summary = NOT_SHOWN;
268        final String line1 = NOT_SHOWN;
269        final String line2 = NOT_SHOWN;
270        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
271    }
272
273    private boolean conditionSnoozed(Uri conditionId) {
274        synchronized (mSnoozedForAlarm) {
275            return mSnoozedForAlarm.contains(conditionId);
276        }
277    }
278
279    @VisibleForTesting
280    void addSnoozed(Uri conditionId) {
281        synchronized (mSnoozedForAlarm) {
282            mSnoozedForAlarm.add(conditionId);
283            saveSnoozedLocked();
284        }
285    }
286
287    private void removeSnoozed(Uri conditionId) {
288        synchronized (mSnoozedForAlarm) {
289            mSnoozedForAlarm.remove(conditionId);
290            saveSnoozedLocked();
291        }
292    }
293
294    private void saveSnoozedLocked() {
295        final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
296        final int currentUser = ActivityManager.getCurrentUser();
297        Settings.Secure.putStringForUser(mContext.getContentResolver(),
298                SCP_SETTING,
299                setting,
300                currentUser);
301    }
302
303    private void readSnoozed() {
304        synchronized (mSnoozedForAlarm) {
305            long identity = Binder.clearCallingIdentity();
306            try {
307                final String setting = Settings.Secure.getStringForUser(
308                        mContext.getContentResolver(),
309                        SCP_SETTING,
310                        ActivityManager.getCurrentUser());
311                if (setting != null) {
312                    final String[] tokens = setting.split(SEPARATOR);
313                    for (int i = 0; i < tokens.length; i++) {
314                        String token = tokens[i];
315                        if (token != null) {
316                            token = token.trim();
317                        }
318                        if (TextUtils.isEmpty(token)) {
319                            continue;
320                        }
321                        mSnoozedForAlarm.add(Uri.parse(token));
322                    }
323                }
324            } finally {
325                Binder.restoreCallingIdentity(identity);
326            }
327        }
328    }
329
330    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
331        @Override
332        public void onReceive(Context context, Intent intent) {
333            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
334            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
335                synchronized (mSubscriptions) {
336                    for (Uri conditionId : mSubscriptions.keySet()) {
337                        final ScheduleCalendar cal = mSubscriptions.get(conditionId);
338                        if (cal != null) {
339                            cal.setTimeZone(Calendar.getInstance().getTimeZone());
340                        }
341                    }
342                }
343            }
344            evaluateSubscriptions();
345        }
346    };
347
348}
349