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