• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.pm.UserInfo;
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.ZenModeConfig;
35 import android.service.notification.ZenModeConfig.EventInfo;
36 import android.util.ArraySet;
37 import android.util.Log;
38 import android.util.Slog;
39 import android.util.SparseArray;
40 
41 import com.android.internal.annotations.VisibleForTesting;
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     private static final String NOT_SHOWN = "...";
58     private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
59     private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
60     private static final int REQUEST_CODE_EVALUATE = 1;
61     private static final String EXTRA_TIME = "time";
62     private static final long CHANGE_DELAY = 2 * 1000;  // coalesce chatty calendar changes
63 
64     @VisibleForTesting Context mContext = this;
65     private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
66     private final SparseArray<CalendarTracker> mTrackers = new SparseArray<>();
67     private final Handler mWorker;
68     private final HandlerThread mThread;
69 
70     private UserHandle mCurrentUser = UserHandle.SYSTEM;
71     private boolean mConnected;
72     private boolean mRegistered;
73     private boolean mBootComplete;  // don't hammer the calendar provider until boot completes.
74     private long mNextAlarmTime;
75 
EventConditionProvider()76     public EventConditionProvider() {
77         if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
78         mThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
79         mThread.start();
80         mWorker = new Handler(mThread.getLooper());
81     }
82 
83     @Override
isValidConditionId(Uri id)84     public boolean isValidConditionId(Uri id) {
85         return ZenModeConfig.isValidEventConditionId(id);
86     }
87 
88     @Override
dump(PrintWriter pw, DumpFilter filter)89     public void dump(PrintWriter pw, DumpFilter filter) {
90         pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
91         pw.print("      mConnected="); pw.println(mConnected);
92         pw.print("      mRegistered="); pw.println(mRegistered);
93         pw.print("      mBootComplete="); pw.println(mBootComplete);
94         dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, System.currentTimeMillis());
95         synchronized (mSubscriptions) {
96             pw.println("      mSubscriptions=");
97             for (Uri conditionId : mSubscriptions) {
98                 pw.print("        ");
99                 pw.println(conditionId);
100             }
101         }
102         pw.println("      mTrackers=");
103         for (int i = 0; i < mTrackers.size(); i++) {
104             pw.print("        user="); pw.println(mTrackers.keyAt(i));
105             mTrackers.valueAt(i).dump("          ", pw);
106         }
107     }
108 
109     @Override
onBootComplete()110     public void onBootComplete() {
111         if (DEBUG) Slog.d(TAG, "onBootComplete");
112         if (mBootComplete) return;
113         mBootComplete = true;
114         final IntentFilter filter = new IntentFilter();
115         filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
116         filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
117         mContext.registerReceiver(new BroadcastReceiver() {
118             @Override
119             public void onReceive(Context context, Intent intent) {
120                 if (android.app.Flags.modesHsum()) {
121                     // Possibly the intent signals a profile added on a different user, but it
122                     // doesn't matter (except for a bit of wasted work here). We will reload
123                     // trackers for that user when we switch.
124                     reloadTrackers(mCurrentUser);
125                 } else {
126                     reloadTrackers();
127                 }
128             }
129         }, filter);
130 
131         if (android.app.Flags.modesHsum()) {
132             reloadTrackers(UserHandle.SYSTEM);
133         } else {
134             reloadTrackers();
135         }
136     }
137 
138     @Override
onUserSwitched(UserHandle user)139     public void onUserSwitched(UserHandle user) {
140         if (DEBUG) Slog.d(TAG, "onUserSwitched: " + user);
141         if (mCurrentUser.getIdentifier() != user.getIdentifier()) {
142             mCurrentUser = user;
143             reloadTrackers(user);
144         }
145     }
146 
147     @Override
onConnected()148     public void onConnected() {
149         if (DEBUG) Slog.d(TAG, "onConnected");
150         mConnected = true;
151     }
152 
153     @Override
onDestroy()154     public void onDestroy() {
155         super.onDestroy();
156         if (DEBUG) Slog.d(TAG, "onDestroy");
157         mConnected = false;
158     }
159 
160     @Override
onSubscribe(Uri conditionId)161     public void onSubscribe(Uri conditionId) {
162         if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
163         if (!ZenModeConfig.isValidEventConditionId(conditionId)) {
164             notifyCondition(createCondition(conditionId, Condition.STATE_FALSE));
165             return;
166         }
167         synchronized (mSubscriptions) {
168             if (mSubscriptions.add(conditionId)) {
169                 evaluateSubscriptions();
170             }
171         }
172     }
173 
174     @Override
onUnsubscribe(Uri conditionId)175     public void onUnsubscribe(Uri conditionId) {
176         if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
177         synchronized (mSubscriptions) {
178             if (mSubscriptions.remove(conditionId)) {
179                 evaluateSubscriptions();
180             }
181         }
182     }
183 
reloadTrackers(UserHandle user)184     private void reloadTrackers(UserHandle user) {
185         if (DEBUG) Slog.d(TAG, "reloadTrackers user=" + user);
186         for (int i = 0; i < mTrackers.size(); i++) {
187             mTrackers.valueAt(i).setCallback(null);
188         }
189         mTrackers.clear();
190 
191         // Ensure that user is the main user and not a profile.
192         UserManager userManager = UserManager.get(mContext);
193         UserHandle possibleParent = userManager.getProfileParent(user);
194         if (possibleParent != null) {
195             Slog.wtf(TAG, "reloadTrackers should not be called with profile " + user
196                     + "; continuing with parent " + possibleParent);
197             user = possibleParent;
198         }
199 
200         for (UserInfo profile : userManager.getProfiles(user.getIdentifier())) {
201             final Context profileContext = getContextForUser(mContext, profile.getUserHandle());
202             if (profileContext == null) {
203                 Slog.w(TAG, "Unable to create context for profile " + profile.id + " of user "
204                         + user.getIdentifier());
205                 continue;
206             }
207             mTrackers.put(profile.id, new CalendarTracker(mContext, profileContext));
208         }
209         evaluateSubscriptions();
210     }
211 
212     @Deprecated // Remove when inlining MODES_HSUM
reloadTrackers()213     private void reloadTrackers() {
214         if (DEBUG) Slog.d(TAG, "reloadTrackers");
215         if (android.app.Flags.modesHsum()) {
216             Slog.wtf(TAG, "Shouldn't call reloadTrackers() without user in MODES_HSUM",
217                     new Exception());
218         }
219         for (int i = 0; i < mTrackers.size(); i++) {
220             mTrackers.valueAt(i).setCallback(null);
221         }
222         mTrackers.clear();
223         for (UserHandle user : UserManager.get(mContext).getUserProfiles()) {
224             final Context context = user.isSystem() ? mContext : getContextForUser(mContext, user);
225             if (context == null) {
226                 Slog.w(TAG, "Unable to create context for user " + user.getIdentifier());
227                 continue;
228             }
229             mTrackers.put(user.getIdentifier(), new CalendarTracker(mContext, context));
230         }
231         evaluateSubscriptions();
232     }
233 
evaluateSubscriptions()234     private void evaluateSubscriptions() {
235         if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) {
236             mWorker.post(mEvaluateSubscriptionsW);
237         }
238     }
239 
evaluateSubscriptionsW()240     private void evaluateSubscriptionsW() {
241         if (DEBUG) Slog.d(TAG, "evaluateSubscriptions");
242         if (!mBootComplete) {
243             if (DEBUG) Slog.d(TAG, "Skipping evaluate before boot complete");
244             return;
245         }
246         final long now = System.currentTimeMillis();
247         List<Condition> conditionsToNotify = new ArrayList<>();
248         synchronized (mSubscriptions) {
249             for (int i = 0; i < mTrackers.size(); i++) {
250                 mTrackers.valueAt(i).setCallback(
251                         mSubscriptions.isEmpty() ? null : mTrackerCallback);
252             }
253             setRegistered(!mSubscriptions.isEmpty());
254             long reevaluateAt = 0;
255             for (Uri conditionId : mSubscriptions) {
256                 final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
257                 if (event == null) {
258                     conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
259                     continue;
260                 }
261                 CheckEventResult result = null;
262                 if (event.calName == null) { // any calendar
263                     // event could exist on any tracker
264                     for (int i = 0; i < mTrackers.size(); i++) {
265                         final CalendarTracker tracker = mTrackers.valueAt(i);
266                         final CheckEventResult r = tracker.checkEvent(event, now);
267                         if (result == null) {
268                             result = r;
269                         } else {
270                             result.inEvent |= r.inEvent;
271                             result.recheckAt = Math.min(result.recheckAt, r.recheckAt);
272                         }
273                     }
274                 } else {
275                     // event should exist on one tracker
276                     final int userId = EventInfo.resolveUserId(event.userId);
277                     final CalendarTracker tracker = mTrackers.get(userId);
278                     if (tracker == null) {
279                         Slog.w(TAG,
280                                 "No calendar tracker found for user " + userId + " and calendar = "
281                                         + event.calName);
282                         conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
283                         continue;
284                     }
285                     result = tracker.checkEvent(event, now);
286                 }
287 
288                 if (result == null) {
289                     Slog.e(TAG, "No CheckEventResult for userId=" + event.userId + ", calId="
290                             + event.calendarId + ", calName=" + event.calName
291                             + "; trackers count is " + mTrackers.size());
292                     conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
293                     continue;
294                 }
295 
296                 if (result.recheckAt != 0
297                         && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
298                     reevaluateAt = result.recheckAt;
299                 }
300                 if (!result.inEvent) {
301                     conditionsToNotify.add(createCondition(conditionId, Condition.STATE_FALSE));
302                     continue;
303                 }
304                 conditionsToNotify.add(createCondition(conditionId, Condition.STATE_TRUE));
305             }
306             rescheduleAlarm(now, reevaluateAt);
307         }
308         for (Condition condition : conditionsToNotify) {
309             if (condition != null) {
310                 notifyCondition(condition);
311             }
312         }
313         if (DEBUG) Slog.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
314     }
315 
rescheduleAlarm(long now, long time)316     private void rescheduleAlarm(long now, long time) {
317         mNextAlarmTime = time;
318         final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
319         final PendingIntent pendingIntent = getPendingIntent(time);
320         alarms.cancel(pendingIntent);
321         if (time == 0 || time < now) {
322             if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
323                     : "specified time in the past"));
324             return;
325         }
326         if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
327                 ts(time), formatDuration(time - now), ts(now)));
328         alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
329     }
330 
getPendingIntent(long time)331     PendingIntent getPendingIntent(long time) {
332         final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
333                 REQUEST_CODE_EVALUATE,
334                 new Intent(ACTION_EVALUATE)
335                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
336                         .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
337                         .putExtra(EXTRA_TIME, time),
338                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
339         return pendingIntent;
340     }
341 
createCondition(Uri id, int state)342     private Condition createCondition(Uri id, int state) {
343         final String summary = NOT_SHOWN;
344         final String line1 = NOT_SHOWN;
345         final String line2 = NOT_SHOWN;
346         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
347     }
348 
setRegistered(boolean registered)349     private void setRegistered(boolean registered) {
350         if (mRegistered == registered) return;
351         if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
352         mRegistered = registered;
353         if (mRegistered) {
354             final IntentFilter filter = new IntentFilter();
355             filter.addAction(Intent.ACTION_TIME_CHANGED);
356             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
357             filter.addAction(ACTION_EVALUATE);
358             registerReceiver(mReceiver, filter,
359                     Context.RECEIVER_EXPORTED_UNAUDITED);
360         } else {
361             unregisterReceiver(mReceiver);
362         }
363     }
364 
getContextForUser(Context context, UserHandle user)365     private static Context getContextForUser(Context context, UserHandle user) {
366         try {
367             return context.createPackageContextAsUser(context.getPackageName(), 0, user);
368         } catch (NameNotFoundException e) {
369             return null;
370         }
371     }
372 
373     private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
374         @Override
375         public void onChanged() {
376             if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged");
377             mWorker.removeCallbacks(mEvaluateSubscriptionsW);
378             mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY);
379         }
380     };
381 
382     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
383         @Override
384         public void onReceive(Context context, Intent intent) {
385             if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
386             evaluateSubscriptions();
387         }
388     };
389 
390     private final Runnable mEvaluateSubscriptionsW = new Runnable() {
391         @Override
392         public void run() {
393             evaluateSubscriptionsW();
394         }
395     };
396 
397     @VisibleForTesting // (otherwise = NONE)
getTrackers()398     public SparseArray<CalendarTracker> getTrackers() {
399         return mTrackers;
400     }
401 }
402