• 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.ActivityManager;
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.service.notification.Condition;
31 import android.service.notification.ScheduleCalendar;
32 import android.service.notification.ZenModeConfig;
33 import android.text.TextUtils;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.util.Slog;
38 
39 import com.android.internal.annotations.GuardedBy;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.server.notification.NotificationManagerService.DumpFilter;
42 import com.android.server.pm.PackageManagerService;
43 
44 import java.io.PrintWriter;
45 import java.time.Clock;
46 import java.util.ArrayList;
47 import java.util.Calendar;
48 import java.util.List;
49 
50 /**
51  * Built-in zen condition provider for daily scheduled time-based conditions.
52  */
53 public class ScheduleConditionProvider extends SystemConditionProviderService {
54     static final String TAG = "ConditionProviders.SCP";
55     static final boolean DEBUG = true || Log.isLoggable("ConditionProviders", Log.DEBUG);
56 
57     private static final String NOT_SHOWN = "...";
58     private static final String SIMPLE_NAME = ScheduleConditionProvider.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 String SEPARATOR = ";";
63     private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
64 
65     private final Context mContext = this;
66     private final Clock mClock;
67     private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
68     @GuardedBy("mSnoozedForAlarm")
69     private final ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
70 
71     private AlarmManager mAlarmManager;
72     private boolean mConnected;
73     private boolean mRegistered;
74     private long mNextAlarmTime;
75 
ScheduleConditionProvider()76     public ScheduleConditionProvider() {
77         this(Clock.systemUTC());
78     }
79 
80     @VisibleForTesting
ScheduleConditionProvider(Clock clock)81     ScheduleConditionProvider(Clock clock) {
82         if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
83         mClock = clock;
84     }
85 
86     @Override
isValidConditionId(Uri id)87     public boolean isValidConditionId(Uri id) {
88         return ZenModeConfig.isValidScheduleConditionId(id);
89     }
90 
91     @Override
dump(PrintWriter pw, DumpFilter filter)92     public void dump(PrintWriter pw, DumpFilter filter) {
93         pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
94         pw.print("      mConnected="); pw.println(mConnected);
95         pw.print("      mRegistered="); pw.println(mRegistered);
96         pw.println("      mSubscriptions=");
97         final long now = mClock.millis();
98         synchronized (mSubscriptions) {
99             for (Uri conditionId : mSubscriptions.keySet()) {
100                 pw.print("        ");
101                 pw.print(meetsSchedule(mSubscriptions.get(conditionId), now) ? "* " : "  ");
102                 pw.println(conditionId);
103                 pw.print("            ");
104                 pw.println(mSubscriptions.get(conditionId).toString());
105             }
106         }
107         synchronized (mSnoozedForAlarm) {
108             pw.println(
109                     "      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
110         }
111         dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
112     }
113 
114     @Override
onConnected()115     public void onConnected() {
116         if (DEBUG) Slog.d(TAG, "onConnected");
117         mConnected = true;
118         readSnoozed();
119     }
120 
121     @Override
onBootComplete()122     public void onBootComplete() {
123         // noop
124     }
125 
126     @Override
onUserSwitched(UserHandle user)127     public void onUserSwitched(UserHandle user) {
128         // Nothing to do here because evaluateSubscriptions() is called for the new configuration
129         // when users switch, and that will reevaluate the next alarm, which is the only piece that
130         // is user-dependent.
131     }
132 
133     @Override
onDestroy()134     public void onDestroy() {
135         super.onDestroy();
136         if (DEBUG) Slog.d(TAG, "onDestroy");
137         mConnected = false;
138     }
139 
140     @Override
onSubscribe(Uri conditionId)141     public void onSubscribe(Uri conditionId) {
142         if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
143         if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
144             notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
145             return;
146         }
147         synchronized (mSubscriptions) {
148             mSubscriptions.put(conditionId, ZenModeConfig.toScheduleCalendar(conditionId));
149         }
150         evaluateSubscriptions();
151     }
152 
153     @Override
onUnsubscribe(Uri conditionId)154     public void onUnsubscribe(Uri conditionId) {
155         if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
156         synchronized (mSubscriptions) {
157             mSubscriptions.remove(conditionId);
158         }
159         removeSnoozed(conditionId);
160         evaluateSubscriptions();
161     }
162 
evaluateSubscriptions()163     private void evaluateSubscriptions() {
164         final long now = mClock.millis();
165         mNextAlarmTime = 0;
166         long nextUserAlarmTime = getNextAlarmClockAlarm();
167         List<Condition> conditionsToNotify = new ArrayList<>();
168         synchronized (mSubscriptions) {
169             setRegistered(!mSubscriptions.isEmpty());
170             for (Uri conditionId : mSubscriptions.keySet()) {
171                 Condition condition =
172                         evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
173                                 now, nextUserAlarmTime);
174                 if (condition != null) {
175                     conditionsToNotify.add(condition);
176                 }
177             }
178         }
179         notifyConditions(conditionsToNotify.toArray(new Condition[conditionsToNotify.size()]));
180         updateAlarm(now, mNextAlarmTime);
181     }
182 
183     @VisibleForTesting
184     @GuardedBy("mSubscriptions")
evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal, long now, long nextUserAlarmTime)185     Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
186             long now, long nextUserAlarmTime) {
187         if (DEBUG) Slog.d(TAG, String.format("evaluateSubscriptionLocked cal=%s, now=%s, "
188                         + "nextUserAlarmTime=%s", cal, ts(now), ts(nextUserAlarmTime)));
189         Condition condition;
190         if (cal == null) {
191             condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
192             removeSnoozed(conditionId);
193             return condition;
194         }
195         if (cal.isInSchedule(now)) {
196             if (conditionSnoozed(conditionId)) {
197                 condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
198             } else if (cal.shouldExitForAlarm(now)) {
199                 condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
200                 addSnoozed(conditionId);
201             } else {
202                 condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
203             }
204         } else {
205             condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
206             removeSnoozed(conditionId);
207         }
208         cal.maybeSetNextAlarm(now, nextUserAlarmTime);
209         final long nextChangeTime = cal.getNextChangeTime(now);
210         if (nextChangeTime > 0 && nextChangeTime > now) {
211             if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
212                 mNextAlarmTime = nextChangeTime;
213             }
214         }
215         return condition;
216     }
217 
updateAlarm(long now, long time)218     private void updateAlarm(long now, long time) {
219         final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
220         final PendingIntent pendingIntent = getPendingIntent(time);
221         alarms.cancel(pendingIntent);
222         if (time > now) {
223             if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
224                     ts(time), formatDuration(time - now), ts(now)));
225             alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
226         } else {
227             if (DEBUG) Slog.d(TAG, "Not scheduling evaluate");
228         }
229     }
230 
231     @VisibleForTesting
getPendingIntent(long time)232     PendingIntent getPendingIntent(long time) {
233         return PendingIntent.getBroadcast(mContext,
234                 REQUEST_CODE_EVALUATE,
235                 new Intent(ACTION_EVALUATE)
236                         .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
237                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
238                         .putExtra(EXTRA_TIME, time),
239                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
240     }
241 
getNextAlarmClockAlarm()242     private long getNextAlarmClockAlarm() {
243         if (mAlarmManager == null) {
244             mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
245         }
246         final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
247                 ActivityManager.getCurrentUser());
248         return info != null ? info.getTriggerTime() : 0;
249     }
250 
meetsSchedule(ScheduleCalendar cal, long time)251     private boolean meetsSchedule(ScheduleCalendar cal, long time) {
252         return cal != null && cal.isInSchedule(time);
253     }
254 
setRegistered(boolean registered)255     private void setRegistered(boolean registered) {
256         if (mRegistered == registered) return;
257         if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
258         mRegistered = registered;
259         if (mRegistered) {
260             final IntentFilter filter = new IntentFilter();
261             filter.addAction(Intent.ACTION_TIME_CHANGED);
262             filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
263             filter.addAction(ACTION_EVALUATE);
264             filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
265             if (android.app.Flags.modesHsum()) {
266                 registerReceiverForAllUsers(mReceiver, filter, /* broadcastPermission= */ null,
267                         /* scheduler= */ null);
268             } else {
269                 registerReceiver(mReceiver, filter,
270                         Context.RECEIVER_EXPORTED_UNAUDITED);
271             }
272         } else {
273             unregisterReceiver(mReceiver);
274         }
275     }
276 
createCondition(Uri id, int state, String reason)277     private Condition createCondition(Uri id, int state, String reason) {
278         if (DEBUG) Slog.d(TAG, "notifyCondition " + id
279                 + " " + Condition.stateToString(state)
280                 + " reason=" + reason);
281         final String summary = NOT_SHOWN;
282         final String line1 = NOT_SHOWN;
283         final String line2 = NOT_SHOWN;
284         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
285     }
286 
conditionSnoozed(Uri conditionId)287     private boolean conditionSnoozed(Uri conditionId) {
288         synchronized (mSnoozedForAlarm) {
289             return mSnoozedForAlarm.contains(conditionId);
290         }
291     }
292 
293     @VisibleForTesting
addSnoozed(Uri conditionId)294     void addSnoozed(Uri conditionId) {
295         synchronized (mSnoozedForAlarm) {
296             mSnoozedForAlarm.add(conditionId);
297             saveSnoozedLocked();
298         }
299     }
300 
removeSnoozed(Uri conditionId)301     private void removeSnoozed(Uri conditionId) {
302         synchronized (mSnoozedForAlarm) {
303             mSnoozedForAlarm.remove(conditionId);
304             saveSnoozedLocked();
305         }
306     }
307 
308     @GuardedBy("mSnoozedForAlarm")
saveSnoozedLocked()309     private void saveSnoozedLocked() {
310         final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
311         final int currentUser = ActivityManager.getCurrentUser();
312         Settings.Secure.putStringForUser(mContext.getContentResolver(),
313                 SCP_SETTING,
314                 setting,
315                 currentUser);
316     }
317 
readSnoozed()318     private void readSnoozed() {
319         synchronized (mSnoozedForAlarm) {
320             final long identity = Binder.clearCallingIdentity();
321             try {
322                 final String setting = Settings.Secure.getStringForUser(
323                         mContext.getContentResolver(),
324                         SCP_SETTING,
325                         ActivityManager.getCurrentUser());
326                 if (setting != null) {
327                     final String[] tokens = setting.split(SEPARATOR);
328                     for (int i = 0; i < tokens.length; i++) {
329                         String token = tokens[i];
330                         if (token != null) {
331                             token = token.trim();
332                         }
333                         if (TextUtils.isEmpty(token)) {
334                             continue;
335                         }
336                         mSnoozedForAlarm.add(Uri.parse(token));
337                     }
338                 }
339             } finally {
340                 Binder.restoreCallingIdentity(identity);
341             }
342         }
343     }
344 
345     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
346         @Override
347         public void onReceive(Context context, Intent intent) {
348             if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
349             if (android.app.Flags.modesHsum()) {
350                 if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(intent.getAction())
351                         && getSendingUserId() != ActivityManager.getCurrentUser()) {
352                     // A different user changed their next alarm.
353                     return;
354                 }
355             }
356 
357             if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
358                 synchronized (mSubscriptions) {
359                     for (Uri conditionId : mSubscriptions.keySet()) {
360                         final ScheduleCalendar cal = mSubscriptions.get(conditionId);
361                         if (cal != null) {
362                             cal.setTimeZone(Calendar.getInstance().getTimeZone());
363                         }
364                     }
365                 }
366             }
367             evaluateSubscriptions();
368         }
369     };
370 
371     @VisibleForTesting // otherwise = NONE
getSubscriptions()372     public ArrayMap<Uri, ScheduleCalendar> getSubscriptions() {
373         return mSubscriptions;
374     }
375 }
376