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.content.pm.PackageManager.NameNotFoundException;
27import android.net.Uri;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.Looper;
31import android.os.Process;
32import android.os.UserHandle;
33import android.os.UserManager;
34import android.service.notification.Condition;
35import android.service.notification.IConditionProvider;
36import android.service.notification.ZenModeConfig;
37import android.service.notification.ZenModeConfig.EventInfo;
38import android.util.ArraySet;
39import android.util.Log;
40import android.util.Slog;
41import android.util.SparseArray;
42
43import com.android.server.notification.CalendarTracker.CheckEventResult;
44import com.android.server.notification.NotificationManagerService.DumpFilter;
45
46import java.io.PrintWriter;
47import java.util.ArrayList;
48import java.util.List;
49
50/**
51 * Built-in zen condition provider for calendar event-based conditions.
52 */
53public class EventConditionProvider extends SystemConditionProviderService {
54    private static final String TAG = "ConditionProviders.ECP";
55    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
56
57    public static final ComponentName COMPONENT =
58            new ComponentName("android", EventConditionProvider.class.getName());
59    private static final String NOT_SHOWN = "...";
60    private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
61    private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
62    private static final int REQUEST_CODE_EVALUATE = 1;
63    private static final String EXTRA_TIME = "time";
64    private static final long CHANGE_DELAY = 2 * 1000;  // coalesce chatty calendar changes
65
66    private final Context mContext = this;
67    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
68    private final SparseArray<CalendarTracker> mTrackers = new SparseArray<>();
69    private final Handler mWorker;
70    private final HandlerThread mThread;
71
72    private boolean mConnected;
73    private boolean mRegistered;
74    private boolean mBootComplete;  // don't hammer the calendar provider until boot completes.
75    private long mNextAlarmTime;
76
77    public EventConditionProvider() {
78        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
79        mThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
80        mThread.start();
81        mWorker = new Handler(mThread.getLooper());
82    }
83
84    @Override
85    public ComponentName getComponent() {
86        return COMPONENT;
87    }
88
89    @Override
90    public boolean isValidConditionId(Uri id) {
91        return ZenModeConfig.isValidEventConditionId(id);
92    }
93
94    @Override
95    public void dump(PrintWriter pw, DumpFilter filter) {
96        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
97        pw.print("      mConnected="); pw.println(mConnected);
98        pw.print("      mRegistered="); pw.println(mRegistered);
99        pw.print("      mBootComplete="); pw.println(mBootComplete);
100        dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, System.currentTimeMillis());
101        synchronized (mSubscriptions) {
102            pw.println("      mSubscriptions=");
103            for (Uri conditionId : mSubscriptions) {
104                pw.print("        ");
105                pw.println(conditionId);
106            }
107        }
108        pw.println("      mTrackers=");
109        for (int i = 0; i < mTrackers.size(); i++) {
110            pw.print("        user="); pw.println(mTrackers.keyAt(i));
111            mTrackers.valueAt(i).dump("          ", pw);
112        }
113    }
114
115    @Override
116    public void onBootComplete() {
117        if (DEBUG) Slog.d(TAG, "onBootComplete");
118        if (mBootComplete) return;
119        mBootComplete = true;
120        final IntentFilter filter = new IntentFilter();
121        filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
122        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
123        mContext.registerReceiver(new BroadcastReceiver() {
124            @Override
125            public void onReceive(Context context, Intent intent) {
126                reloadTrackers();
127            }
128        }, filter);
129        reloadTrackers();
130    }
131
132    @Override
133    public void onConnected() {
134        if (DEBUG) Slog.d(TAG, "onConnected");
135        mConnected = true;
136    }
137
138    @Override
139    public void onDestroy() {
140        super.onDestroy();
141        if (DEBUG) Slog.d(TAG, "onDestroy");
142        mConnected = false;
143    }
144
145    @Override
146    public void onSubscribe(Uri conditionId) {
147        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
148        if (!ZenModeConfig.isValidEventConditionId(conditionId)) {
149            notifyCondition(createCondition(conditionId, Condition.STATE_FALSE));
150            return;
151        }
152        synchronized (mSubscriptions) {
153            if (mSubscriptions.add(conditionId)) {
154                evaluateSubscriptions();
155            }
156        }
157    }
158
159    @Override
160    public void onUnsubscribe(Uri conditionId) {
161        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
162        synchronized (mSubscriptions) {
163            if (mSubscriptions.remove(conditionId)) {
164                evaluateSubscriptions();
165            }
166        }
167    }
168
169    @Override
170    public void attachBase(Context base) {
171        attachBaseContext(base);
172    }
173
174    @Override
175    public IConditionProvider asInterface() {
176        return (IConditionProvider) onBind(null);
177    }
178
179    private void reloadTrackers() {
180        if (DEBUG) Slog.d(TAG, "reloadTrackers");
181        for (int i = 0; i < mTrackers.size(); i++) {
182            mTrackers.valueAt(i).setCallback(null);
183        }
184        mTrackers.clear();
185        for (UserHandle user : UserManager.get(mContext).getUserProfiles()) {
186            final Context context = user.isSystem() ? mContext : getContextForUser(mContext, user);
187            if (context == null) {
188                Slog.w(TAG, "Unable to create context for user " + user.getIdentifier());
189                continue;
190            }
191            mTrackers.put(user.getIdentifier(), new CalendarTracker(mContext, context));
192        }
193        evaluateSubscriptions();
194    }
195
196    private void evaluateSubscriptions() {
197        if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) {
198            mWorker.post(mEvaluateSubscriptionsW);
199        }
200    }
201
202    private void evaluateSubscriptionsW() {
203        if (DEBUG) Slog.d(TAG, "evaluateSubscriptions");
204        if (!mBootComplete) {
205            if (DEBUG) Slog.d(TAG, "Skipping evaluate before boot complete");
206            return;
207        }
208        final long now = System.currentTimeMillis();
209        List<Condition> conditionsToNotify = new ArrayList<>();
210        synchronized (mSubscriptions) {
211            for (int i = 0; i < mTrackers.size(); i++) {
212                mTrackers.valueAt(i).setCallback(
213                        mSubscriptions.isEmpty() ? null : mTrackerCallback);
214            }
215            setRegistered(!mSubscriptions.isEmpty());
216            long reevaluateAt = 0;
217            for (Uri conditionId : mSubscriptions) {
218                final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
219                if (event == null) {
220                    conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
221                    continue;
222                }
223                CheckEventResult result = null;
224                if (event.calendar == null) { // any calendar
225                    // event could exist on any tracker
226                    for (int i = 0; i < mTrackers.size(); i++) {
227                        final CalendarTracker tracker = mTrackers.valueAt(i);
228                        final CheckEventResult r = tracker.checkEvent(event, now);
229                        if (result == null) {
230                            result = r;
231                        } else {
232                            result.inEvent |= r.inEvent;
233                            result.recheckAt = Math.min(result.recheckAt, r.recheckAt);
234                        }
235                    }
236                } else {
237                    // event should exist on one tracker
238                    final int userId = EventInfo.resolveUserId(event.userId);
239                    final CalendarTracker tracker = mTrackers.get(userId);
240                    if (tracker == null) {
241                        Slog.w(TAG, "No calendar tracker found for user " + userId);
242                        conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
243                        continue;
244                    }
245                    result = tracker.checkEvent(event, now);
246                }
247                if (result.recheckAt != 0
248                        && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
249                    reevaluateAt = result.recheckAt;
250                }
251                if (!result.inEvent) {
252                    conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
253                    continue;
254                }
255                conditionsToNotify.add(createCondition(conditionId, Condition.STATE_TRUE));
256            }
257            rescheduleAlarm(now, reevaluateAt);
258        }
259        for (Condition condition : conditionsToNotify) {
260            if (condition != null) {
261                notifyCondition(condition);
262            }
263        }
264        if (DEBUG) Slog.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
265    }
266
267    private void rescheduleAlarm(long now, long time) {
268        mNextAlarmTime = time;
269        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
270        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
271                REQUEST_CODE_EVALUATE,
272                new Intent(ACTION_EVALUATE)
273                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
274                        .putExtra(EXTRA_TIME, time),
275                PendingIntent.FLAG_UPDATE_CURRENT);
276        alarms.cancel(pendingIntent);
277        if (time == 0 || time < now) {
278            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
279                    : "specified time in the past"));
280            return;
281        }
282        if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
283                ts(time), formatDuration(time - now), ts(now)));
284        alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
285    }
286
287    private Condition createCondition(Uri id, int state) {
288        final String summary = NOT_SHOWN;
289        final String line1 = NOT_SHOWN;
290        final String line2 = NOT_SHOWN;
291        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
292    }
293
294    private void setRegistered(boolean registered) {
295        if (mRegistered == registered) return;
296        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
297        mRegistered = registered;
298        if (mRegistered) {
299            final IntentFilter filter = new IntentFilter();
300            filter.addAction(Intent.ACTION_TIME_CHANGED);
301            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
302            filter.addAction(ACTION_EVALUATE);
303            registerReceiver(mReceiver, filter);
304        } else {
305            unregisterReceiver(mReceiver);
306        }
307    }
308
309    private static Context getContextForUser(Context context, UserHandle user) {
310        try {
311            return context.createPackageContextAsUser(context.getPackageName(), 0, user);
312        } catch (NameNotFoundException e) {
313            return null;
314        }
315    }
316
317    private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
318        @Override
319        public void onChanged() {
320            if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged");
321            mWorker.removeCallbacks(mEvaluateSubscriptionsW);
322            mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY);
323        }
324    };
325
326    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
327        @Override
328        public void onReceive(Context context, Intent intent) {
329            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
330            evaluateSubscriptions();
331        }
332    };
333
334    private final Runnable mEvaluateSubscriptionsW = new Runnable() {
335        @Override
336        public void run() {
337            evaluateSubscriptionsW();
338        }
339    };
340}
341