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