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 
17 package com.android.server.notification;
18 
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.net.Uri;
28 import android.os.Handler;
29 import android.os.HandlerThread;
30 import android.os.Process;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.service.notification.Condition;
34 import android.service.notification.IConditionProvider;
35 import android.service.notification.ZenModeConfig;
36 import android.service.notification.ZenModeConfig.EventInfo;
37 import android.util.ArraySet;
38 import android.util.Log;
39 import android.util.Slog;
40 import android.util.SparseArray;
41 
42 import com.android.server.notification.CalendarTracker.CheckEventResult;
43 import com.android.server.notification.NotificationManagerService.DumpFilter;
44 import com.android.server.pm.PackageManagerService;
45 
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Built-in zen condition provider for calendar event-based conditions.
52  */
53 public 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 
EventConditionProvider()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
getComponent()85     public ComponentName getComponent() {
86         return COMPONENT;
87     }
88 
89     @Override
isValidConditionId(Uri id)90     public boolean isValidConditionId(Uri id) {
91         return ZenModeConfig.isValidEventConditionId(id);
92     }
93 
94     @Override
dump(PrintWriter pw, DumpFilter filter)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
onBootComplete()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
onConnected()133     public void onConnected() {
134         if (DEBUG) Slog.d(TAG, "onConnected");
135         mConnected = true;
136     }
137 
138     @Override
onDestroy()139     public void onDestroy() {
140         super.onDestroy();
141         if (DEBUG) Slog.d(TAG, "onDestroy");
142         mConnected = false;
143     }
144 
145     @Override
onSubscribe(Uri conditionId)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
onUnsubscribe(Uri conditionId)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
attachBase(Context base)170     public void attachBase(Context base) {
171         attachBaseContext(base);
172     }
173 
174     @Override
asInterface()175     public IConditionProvider asInterface() {
176         return (IConditionProvider) onBind(null);
177     }
178 
reloadTrackers()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 
evaluateSubscriptions()196     private void evaluateSubscriptions() {
197         if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) {
198             mWorker.post(mEvaluateSubscriptionsW);
199         }
200     }
201 
evaluateSubscriptionsW()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.calName == 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 
rescheduleAlarm(long now, long time)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 = getPendingIntent(time);
271         alarms.cancel(pendingIntent);
272         if (time == 0 || time < now) {
273             if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
274                     : "specified time in the past"));
275             return;
276         }
277         if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
278                 ts(time), formatDuration(time - now), ts(now)));
279         alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
280     }
281 
getPendingIntent(long time)282     PendingIntent getPendingIntent(long time) {
283         final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
284                 REQUEST_CODE_EVALUATE,
285                 new Intent(ACTION_EVALUATE)
286                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
287                         .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
288                         .putExtra(EXTRA_TIME, time),
289                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
290         return pendingIntent;
291     }
292 
createCondition(Uri id, int state)293     private Condition createCondition(Uri id, int state) {
294         final String summary = NOT_SHOWN;
295         final String line1 = NOT_SHOWN;
296         final String line2 = NOT_SHOWN;
297         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
298     }
299 
setRegistered(boolean registered)300     private void setRegistered(boolean registered) {
301         if (mRegistered == registered) return;
302         if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
303         mRegistered = registered;
304         if (mRegistered) {
305             final IntentFilter filter = new IntentFilter();
306             filter.addAction(Intent.ACTION_TIME_CHANGED);
307             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
308             filter.addAction(ACTION_EVALUATE);
309             registerReceiver(mReceiver, filter,
310                     Context.RECEIVER_EXPORTED_UNAUDITED);
311         } else {
312             unregisterReceiver(mReceiver);
313         }
314     }
315 
getContextForUser(Context context, UserHandle user)316     private static Context getContextForUser(Context context, UserHandle user) {
317         try {
318             return context.createPackageContextAsUser(context.getPackageName(), 0, user);
319         } catch (NameNotFoundException e) {
320             return null;
321         }
322     }
323 
324     private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
325         @Override
326         public void onChanged() {
327             if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged");
328             mWorker.removeCallbacks(mEvaluateSubscriptionsW);
329             mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY);
330         }
331     };
332 
333     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
334         @Override
335         public void onReceive(Context context, Intent intent) {
336             if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
337             evaluateSubscriptions();
338         }
339     };
340 
341     private final Runnable mEvaluateSubscriptionsW = new Runnable() {
342         @Override
343         public void run() {
344             evaluateSubscriptionsW();
345         }
346     };
347 }
348