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