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