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.AlarmManager;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.net.Uri;
27import android.service.notification.Condition;
28import android.service.notification.IConditionProvider;
29import android.service.notification.ZenModeConfig;
30import android.service.notification.ZenModeConfig.ScheduleInfo;
31import android.util.ArraySet;
32import android.util.Log;
33import android.util.Slog;
34
35import com.android.server.notification.NotificationManagerService.DumpFilter;
36
37import java.io.PrintWriter;
38import java.util.TimeZone;
39
40/**
41 * Built-in zen condition provider for daily scheduled time-based conditions.
42 */
43public class ScheduleConditionProvider extends SystemConditionProviderService {
44    private static final String TAG = "ConditionProviders.SCP";
45    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
46
47    public static final ComponentName COMPONENT =
48            new ComponentName("android", ScheduleConditionProvider.class.getName());
49    private static final String NOT_SHOWN = "...";
50    private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
51    private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
52    private static final int REQUEST_CODE_EVALUATE = 1;
53    private static final String EXTRA_TIME = "time";
54
55    private final Context mContext = this;
56    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
57
58    private boolean mConnected;
59    private boolean mRegistered;
60    private long mNextAlarmTime;
61
62    public ScheduleConditionProvider() {
63        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
64    }
65
66    @Override
67    public ComponentName getComponent() {
68        return COMPONENT;
69    }
70
71    @Override
72    public boolean isValidConditionId(Uri id) {
73        return ZenModeConfig.isValidScheduleConditionId(id);
74    }
75
76    @Override
77    public void dump(PrintWriter pw, DumpFilter filter) {
78        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
79        pw.print("      mConnected="); pw.println(mConnected);
80        pw.print("      mRegistered="); pw.println(mRegistered);
81        pw.println("      mSubscriptions=");
82        final long now = System.currentTimeMillis();
83        for (Uri conditionId : mSubscriptions) {
84            pw.print("        ");
85            pw.print(meetsSchedule(conditionId, now) ? "* " : "  ");
86            pw.println(conditionId);
87        }
88        dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
89    }
90
91    @Override
92    public void onConnected() {
93        if (DEBUG) Slog.d(TAG, "onConnected");
94        mConnected = true;
95    }
96
97    @Override
98    public void onBootComplete() {
99        // noop
100    }
101
102    @Override
103    public void onDestroy() {
104        super.onDestroy();
105        if (DEBUG) Slog.d(TAG, "onDestroy");
106        mConnected = false;
107    }
108
109    @Override
110    public void onRequestConditions(int relevance) {
111        if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance);
112        // does not advertise conditions
113    }
114
115    @Override
116    public void onSubscribe(Uri conditionId) {
117        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
118        if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
119            notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
120            return;
121        }
122        mSubscriptions.add(conditionId);
123        evaluateSubscriptions();
124    }
125
126    @Override
127    public void onUnsubscribe(Uri conditionId) {
128        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
129        mSubscriptions.remove(conditionId);
130        evaluateSubscriptions();
131    }
132
133    @Override
134    public void attachBase(Context base) {
135        attachBaseContext(base);
136    }
137
138    @Override
139    public IConditionProvider asInterface() {
140        return (IConditionProvider) onBind(null);
141    }
142
143    private void evaluateSubscriptions() {
144        setRegistered(!mSubscriptions.isEmpty());
145        final long now = System.currentTimeMillis();
146        mNextAlarmTime = 0;
147        for (Uri conditionId : mSubscriptions) {
148            final ScheduleCalendar cal = toScheduleCalendar(conditionId);
149            if (cal != null && cal.isInSchedule(now)) {
150                notifyCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
151            } else {
152                notifyCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
153            }
154            if (cal != null) {
155                final long nextChangeTime = cal.getNextChangeTime(now);
156                if (nextChangeTime > 0 && nextChangeTime > now) {
157                    if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
158                        mNextAlarmTime = nextChangeTime;
159                    }
160                }
161            }
162        }
163        updateAlarm(now, mNextAlarmTime);
164    }
165
166    private void updateAlarm(long now, long time) {
167        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
168        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
169                REQUEST_CODE_EVALUATE,
170                new Intent(ACTION_EVALUATE)
171                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
172                        .putExtra(EXTRA_TIME, time),
173                PendingIntent.FLAG_UPDATE_CURRENT);
174        alarms.cancel(pendingIntent);
175        if (time > now) {
176            if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
177                    ts(time), formatDuration(time - now), ts(now)));
178            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
179        } else {
180            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
181        }
182    }
183
184    private static boolean meetsSchedule(Uri conditionId, long time) {
185        final ScheduleCalendar cal = toScheduleCalendar(conditionId);
186        return cal != null && cal.isInSchedule(time);
187    }
188
189    private static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
190        final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
191        if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
192        final ScheduleCalendar sc = new ScheduleCalendar();
193        sc.setSchedule(schedule);
194        sc.setTimeZone(TimeZone.getDefault());
195        return sc;
196    }
197
198    private void setRegistered(boolean registered) {
199        if (mRegistered == registered) return;
200        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
201        mRegistered = registered;
202        if (mRegistered) {
203            final IntentFilter filter = new IntentFilter();
204            filter.addAction(Intent.ACTION_TIME_CHANGED);
205            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
206            filter.addAction(ACTION_EVALUATE);
207            registerReceiver(mReceiver, filter);
208        } else {
209            unregisterReceiver(mReceiver);
210        }
211    }
212
213    private void notifyCondition(Uri conditionId, int state, String reason) {
214        if (DEBUG) Slog.d(TAG, "notifyCondition " + conditionId
215                + " " + Condition.stateToString(state)
216                + " reason=" + reason);
217        notifyCondition(createCondition(conditionId, state));
218    }
219
220    private Condition createCondition(Uri id, int state) {
221        final String summary = NOT_SHOWN;
222        final String line1 = NOT_SHOWN;
223        final String line2 = NOT_SHOWN;
224        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
225    }
226
227    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
228        @Override
229        public void onReceive(Context context, Intent intent) {
230            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
231            evaluateSubscriptions();
232        }
233    };
234
235}
236