/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.notification; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.os.UserManager; import android.service.notification.Condition; import android.service.notification.IConditionProvider; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.EventInfo; import android.util.ArraySet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import com.android.server.notification.CalendarTracker.CheckEventResult; import com.android.server.notification.NotificationManagerService.DumpFilter; import java.io.PrintWriter; /** * Built-in zen condition provider for calendar event-based conditions. */ public class EventConditionProvider extends SystemConditionProviderService { private static final String TAG = "ConditionProviders.ECP"; private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG); public static final ComponentName COMPONENT = new ComponentName("android", EventConditionProvider.class.getName()); private static final String NOT_SHOWN = "..."; private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName(); private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE"; private static final int REQUEST_CODE_EVALUATE = 1; private static final String EXTRA_TIME = "time"; private static final long CHANGE_DELAY = 2 * 1000; // coalesce chatty calendar changes private final Context mContext = this; private final ArraySet mSubscriptions = new ArraySet(); private final SparseArray mTrackers = new SparseArray<>(); private final Handler mWorker; private boolean mConnected; private boolean mRegistered; private boolean mBootComplete; // don't hammer the calendar provider until boot completes. private long mNextAlarmTime; public EventConditionProvider(Looper worker) { if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()"); mWorker = new Handler(worker); } @Override public ComponentName getComponent() { return COMPONENT; } @Override public boolean isValidConditionId(Uri id) { return ZenModeConfig.isValidEventConditionId(id); } @Override public void dump(PrintWriter pw, DumpFilter filter) { pw.print(" "); pw.print(SIMPLE_NAME); pw.println(":"); pw.print(" mConnected="); pw.println(mConnected); pw.print(" mRegistered="); pw.println(mRegistered); pw.print(" mBootComplete="); pw.println(mBootComplete); dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, System.currentTimeMillis()); pw.println(" mSubscriptions="); for (Uri conditionId : mSubscriptions) { pw.print(" "); pw.println(conditionId); } pw.println(" mTrackers="); for (int i = 0; i < mTrackers.size(); i++) { pw.print(" user="); pw.println(mTrackers.keyAt(i)); mTrackers.valueAt(i).dump(" ", pw); } } @Override public void onBootComplete() { if (DEBUG) Slog.d(TAG, "onBootComplete"); if (mBootComplete) return; mBootComplete = true; final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { reloadTrackers(); } }, filter); reloadTrackers(); } @Override public void onConnected() { if (DEBUG) Slog.d(TAG, "onConnected"); mConnected = true; } @Override public void onDestroy() { super.onDestroy(); if (DEBUG) Slog.d(TAG, "onDestroy"); mConnected = false; } @Override public void onRequestConditions(int relevance) { if (DEBUG) Slog.d(TAG, "onRequestConditions relevance=" + relevance); // does not advertise conditions } @Override public void onSubscribe(Uri conditionId) { if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId); if (!ZenModeConfig.isValidEventConditionId(conditionId)) { notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition"); return; } if (mSubscriptions.add(conditionId)) { evaluateSubscriptions(); } } @Override public void onUnsubscribe(Uri conditionId) { if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId); if (mSubscriptions.remove(conditionId)) { evaluateSubscriptions(); } } @Override public void attachBase(Context base) { attachBaseContext(base); } @Override public IConditionProvider asInterface() { return (IConditionProvider) onBind(null); } private void reloadTrackers() { if (DEBUG) Slog.d(TAG, "reloadTrackers"); for (int i = 0; i < mTrackers.size(); i++) { mTrackers.valueAt(i).setCallback(null); } mTrackers.clear(); for (UserHandle user : UserManager.get(mContext).getUserProfiles()) { final Context context = user.isOwner() ? mContext : getContextForUser(mContext, user); if (context == null) { Slog.w(TAG, "Unable to create context for user " + user.getIdentifier()); continue; } mTrackers.put(user.getIdentifier(), new CalendarTracker(mContext, context)); } evaluateSubscriptions(); } private void evaluateSubscriptions() { if (!mWorker.hasCallbacks(mEvaluateSubscriptionsW)) { mWorker.post(mEvaluateSubscriptionsW); } } private void evaluateSubscriptionsW() { if (DEBUG) Slog.d(TAG, "evaluateSubscriptions"); if (!mBootComplete) { if (DEBUG) Slog.d(TAG, "Skipping evaluate before boot complete"); return; } final long now = System.currentTimeMillis(); for (int i = 0; i < mTrackers.size(); i++) { mTrackers.valueAt(i).setCallback(mSubscriptions.isEmpty() ? null : mTrackerCallback); } setRegistered(!mSubscriptions.isEmpty()); long reevaluateAt = 0; for (Uri conditionId : mSubscriptions) { final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId); if (event == null) { notifyCondition(conditionId, Condition.STATE_FALSE, "badConditionId"); continue; } CheckEventResult result = null; if (event.calendar == null) { // any calendar // event could exist on any tracker for (int i = 0; i < mTrackers.size(); i++) { final CalendarTracker tracker = mTrackers.valueAt(i); final CheckEventResult r = tracker.checkEvent(event, now); if (result == null) { result = r; } else { result.inEvent |= r.inEvent; result.recheckAt = Math.min(result.recheckAt, r.recheckAt); } } } else { // event should exist on one tracker final int userId = EventInfo.resolveUserId(event.userId); final CalendarTracker tracker = mTrackers.get(userId); if (tracker == null) { Slog.w(TAG, "No calendar tracker found for user " + userId); notifyCondition(conditionId, Condition.STATE_FALSE, "badUserId"); continue; } result = tracker.checkEvent(event, now); } if (result.recheckAt != 0 && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) { reevaluateAt = result.recheckAt; } if (!result.inEvent) { notifyCondition(conditionId, Condition.STATE_FALSE, "!inEventNow"); continue; } notifyCondition(conditionId, Condition.STATE_TRUE, "inEventNow"); } rescheduleAlarm(now, reevaluateAt); if (DEBUG) Slog.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now)); } private void rescheduleAlarm(long now, long time) { mNextAlarmTime = time; final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE_EVALUATE, new Intent(ACTION_EVALUATE) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) .putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT); alarms.cancel(pendingIntent); if (time == 0 || time < now) { if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified" : "specified time in the past")); return; } if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s", ts(time), formatDuration(time - now), ts(now))); alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); } private void notifyCondition(Uri conditionId, int state, String reason) { if (DEBUG) Slog.d(TAG, "notifyCondition " + conditionId + " " + Condition.stateToString(state) + " reason=" + reason); notifyCondition(createCondition(conditionId, state)); } private Condition createCondition(Uri id, int state) { final String summary = NOT_SHOWN; final String line1 = NOT_SHOWN; final String line2 = NOT_SHOWN; return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS); } private void setRegistered(boolean registered) { if (mRegistered == registered) return; if (DEBUG) Slog.d(TAG, "setRegistered " + registered); mRegistered = registered; if (mRegistered) { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(ACTION_EVALUATE); registerReceiver(mReceiver, filter); } else { unregisterReceiver(mReceiver); } } private static Context getContextForUser(Context context, UserHandle user) { try { return context.createPackageContextAsUser(context.getPackageName(), 0, user); } catch (NameNotFoundException e) { return null; } } private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() { @Override public void onChanged() { if (DEBUG) Slog.d(TAG, "mTrackerCallback.onChanged"); mWorker.removeCallbacks(mEvaluateSubscriptionsW); mWorker.postDelayed(mEvaluateSubscriptionsW, CHANGE_DELAY); } }; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction()); evaluateSubscriptions(); } }; private final Runnable mEvaluateSubscriptionsW = new Runnable() { @Override public void run() { evaluateSubscriptionsW(); } }; }