NextAlarmConditionProvider.java revision 7ab8ecd053d08aec230a81fccfcc5a5c780f15c7
1/*
2 * Copyright (C) 2014 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
17package com.android.server.notification;
18
19import android.app.ActivityManager;
20import android.app.AlarmManager;
21import android.app.PendingIntent;
22import android.app.AlarmManager.AlarmClockInfo;
23import android.content.BroadcastReceiver;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.net.Uri;
29import android.os.Handler;
30import android.os.Message;
31import android.os.PowerManager;
32import android.os.UserHandle;
33import android.service.notification.Condition;
34import android.service.notification.ConditionProviderService;
35import android.service.notification.IConditionProvider;
36import android.service.notification.ZenModeConfig;
37import android.util.TimeUtils;
38import android.text.format.DateFormat;
39import android.util.Log;
40import android.util.Slog;
41
42import com.android.internal.R;
43import com.android.server.notification.NotificationManagerService.DumpFilter;
44
45import java.io.PrintWriter;
46import java.util.Locale;
47
48/**
49 * Built-in zen condition provider for alarm-clock-based conditions.
50 *
51 * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise
52 * it as an exit condition for zen mode (unless the built-in downtime condition is also available).
53 *
54 * <p>When this next alarm is selected as the active exit condition, follow subsequent changes
55 * to the user's next alarm, assuming it remains within the 12-hr window.
56 *
57 * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not
58 * survive a reboot.  Maintain the illusion of a consistent next alarm value by holding on to
59 * a persisted condition until we receive the first value after reboot, or timeout with no value.
60 */
61public class NextAlarmConditionProvider extends ConditionProviderService {
62    private static final String TAG = "NextAlarmConditions";
63    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
64
65    private static final String ACTION_TRIGGER = TAG + ".trigger";
66    private static final String EXTRA_TRIGGER = "trigger";
67    private static final int REQUEST_CODE = 100;
68    private static final long SECONDS = 1000;
69    private static final long MINUTES = 60 * SECONDS;
70    private static final long HOURS = 60 * MINUTES;
71    private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS;  // treat clear+set as update
72    private static final long EARLY = 5 * SECONDS;  // fire early, ensure alarm stream is unmuted
73    private static final long WAIT_AFTER_CONNECT = 5 * MINUTES;// for initial alarm re-registration
74    private static final long WAIT_AFTER_BOOT = 20 * SECONDS;  // for initial alarm re-registration
75    private static final String NEXT_ALARM_PATH = "next_alarm";
76    public static final ComponentName COMPONENT =
77            new ComponentName("android", NextAlarmConditionProvider.class.getName());
78
79    private final Context mContext = this;
80    private final H mHandler = new H();
81
82    private long mConnected;
83    private boolean mRegistered;
84    private AlarmManager mAlarmManager;
85    private int mCurrentUserId;
86    private long mLookaheadThreshold;
87    private long mScheduledAlarmTime;
88    private Callback mCallback;
89    private Uri mCurrentSubscription;
90    private PowerManager.WakeLock mWakeLock;
91    private long mBootCompleted;
92
93    public NextAlarmConditionProvider() {
94        if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()");
95    }
96
97    public void dump(PrintWriter pw, DumpFilter filter) {
98        pw.println("    NextAlarmConditionProvider:");
99        pw.print("      mConnected="); pw.println(mConnected);
100        pw.print("      mBootCompleted="); pw.println(mBootCompleted);
101        pw.print("      mRegistered="); pw.println(mRegistered);
102        pw.print("      mCurrentUserId="); pw.println(mCurrentUserId);
103        pw.print("      mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime));
104        pw.print("      mLookaheadThreshold="); pw.print(mLookaheadThreshold);
105        pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
106        pw.print("      mCurrentSubscription="); pw.println(mCurrentSubscription);
107        pw.print("      mWakeLock="); pw.println(mWakeLock);
108    }
109
110    public void setCallback(Callback callback) {
111        mCallback = callback;
112    }
113
114    @Override
115    public void onConnected() {
116        if (DEBUG) Slog.d(TAG, "onConnected");
117        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
118        final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
119        mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
120        mLookaheadThreshold = mContext.getResources()
121                .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS;
122        init();
123        mConnected = System.currentTimeMillis();
124    }
125
126    public void onUserSwitched() {
127        if (DEBUG) Slog.d(TAG, "onUserSwitched");
128        if (mConnected != 0) {
129            init();
130        }
131    }
132
133    @Override
134    public void onDestroy() {
135        super.onDestroy();
136        if (DEBUG) Slog.d(TAG, "onDestroy");
137        if (mRegistered) {
138            mContext.unregisterReceiver(mReceiver);
139            mRegistered = false;
140        }
141        mConnected = 0;
142    }
143
144    @Override
145    public void onRequestConditions(int relevance) {
146        if (mConnected == 0 || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return;
147
148        final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId);
149        if (nextAlarm == null) return;  // no next alarm
150        if (mCallback != null && mCallback.isInDowntime()) return;  // prefer downtime condition
151        if (!isWithinLookaheadThreshold(nextAlarm)) return;  // alarm not within window
152
153        // next alarm exists, and is within the configured lookahead threshold
154        notifyCondition(newConditionId(), nextAlarm, Condition.STATE_TRUE, "request");
155    }
156
157    private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) {
158        if (alarm == null) return false;
159        final long delta = getEarlyTriggerTime(alarm) - System.currentTimeMillis();
160        return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold);
161    }
162
163    @Override
164    public void onSubscribe(Uri conditionId) {
165        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
166        if (!isNextAlarmCondition(conditionId)) {
167            notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition");
168            return;
169        }
170        mCurrentSubscription = conditionId;
171        mHandler.postEvaluate(0);
172    }
173
174    private static long getEarlyTriggerTime(AlarmClockInfo alarm) {
175        return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0;
176    }
177
178    private boolean isDoneWaitingAfterBoot(long time) {
179        if (mBootCompleted > 0) return (time - mBootCompleted) > WAIT_AFTER_BOOT;
180        if (mConnected > 0) return (time - mConnected) > WAIT_AFTER_CONNECT;
181        return true;
182    }
183
184    private void handleEvaluate() {
185        final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId);
186        final long triggerTime = getEarlyTriggerTime(nextAlarm);
187        final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm);
188        final long now = System.currentTimeMillis();
189        final boolean booted = isDoneWaitingAfterBoot(now);
190        if (DEBUG) Slog.d(TAG, "handleEvaluate mCurrentSubscription=" + mCurrentSubscription
191                + " nextAlarm=" + formatAlarmDebug(triggerTime)
192                + " withinThreshold=" + withinThreshold
193                + " booted=" + booted);
194        if (mCurrentSubscription == null) return;  // no one cares
195        if (!booted) {
196            // we don't know yet
197            notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_UNKNOWN, "!booted");
198            final long recheckTime = (mBootCompleted > 0 ? mBootCompleted : now) + WAIT_AFTER_BOOT;
199            rescheduleAlarm(recheckTime);
200            return;
201        }
202        if (!withinThreshold) {
203            // triggertime invalid or in the past, condition = false
204            notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_FALSE, "!within");
205            mCurrentSubscription = null;
206            return;
207        }
208        // triggertime in the future, condition = true, schedule alarm
209        notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_TRUE, "within");
210        rescheduleAlarm(triggerTime);
211    }
212
213    private static String formatDuration(long millis) {
214        final StringBuilder sb = new StringBuilder();
215        TimeUtils.formatDuration(millis, sb);
216        return sb.toString();
217    }
218
219    private void rescheduleAlarm(long time) {
220        if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time);
221        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
222        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
223                new Intent(ACTION_TRIGGER)
224                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
225                        .putExtra(EXTRA_TRIGGER, time),
226                PendingIntent.FLAG_UPDATE_CURRENT);
227        alarms.cancel(pendingIntent);
228        mScheduledAlarmTime = time;
229        if (time > 0) {
230            if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)",
231                    formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis())));
232            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
233        }
234    }
235
236    private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) {
237        final String formattedAlarm = alarm == null ? "" : formatAlarm(alarm.getTriggerTime());
238        if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state)
239                + " alarm=" + formattedAlarm + " reason=" + reason);
240        notifyCondition(new Condition(id,
241                mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm),
242                mContext.getString(R.string.zen_mode_next_alarm_line_one),
243                formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW));
244    }
245
246    @Override
247    public void onUnsubscribe(Uri conditionId) {
248        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
249        if (conditionId != null && conditionId.equals(mCurrentSubscription)) {
250            mCurrentSubscription = null;
251            rescheduleAlarm(0);
252        }
253    }
254
255    public void attachBase(Context base) {
256        attachBaseContext(base);
257    }
258
259    public IConditionProvider asInterface() {
260        return (IConditionProvider) onBind(null);
261    }
262
263    private Uri newConditionId() {
264        return new Uri.Builder().scheme(Condition.SCHEME)
265                .authority(ZenModeConfig.SYSTEM_AUTHORITY)
266                .appendPath(NEXT_ALARM_PATH)
267                .appendPath(Integer.toString(mCurrentUserId))
268                .build();
269    }
270
271    private boolean isNextAlarmCondition(Uri conditionId) {
272        return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME)
273                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
274                && conditionId.getPathSegments().size() == 2
275                && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH)
276                && conditionId.getPathSegments().get(1).equals(Integer.toString(mCurrentUserId));
277    }
278
279    private void init() {
280        if (mRegistered) {
281            mContext.unregisterReceiver(mReceiver);
282        }
283        mCurrentUserId = ActivityManager.getCurrentUser();
284        final IntentFilter filter = new IntentFilter();
285        filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
286        filter.addAction(ACTION_TRIGGER);
287        filter.addAction(Intent.ACTION_TIME_CHANGED);
288        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
289        filter.addAction(Intent.ACTION_BOOT_COMPLETED);
290        mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null,
291                null);
292        mRegistered = true;
293        mHandler.postEvaluate(0);
294    }
295
296    private String formatAlarm(long time) {
297        return formatAlarm(time, "Hm", "hma");
298    }
299
300    private String formatAlarm(long time, String skeleton24, String skeleton12) {
301        final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12;
302        final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
303        return DateFormat.format(pattern, time).toString();
304    }
305
306    private String formatAlarmDebug(AlarmClockInfo alarm) {
307        return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0);
308    }
309
310    private String formatAlarmDebug(long time) {
311        if (time <= 0) return Long.toString(time);
312        return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa"));
313    }
314
315    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
316        @Override
317        public void onReceive(Context context, Intent intent) {
318            final String action = intent.getAction();
319            if (DEBUG) Slog.d(TAG, "onReceive " + action);
320            long delay = 0;
321            if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
322                delay = NEXT_ALARM_UPDATE_DELAY;
323                if (DEBUG) Slog.d(TAG, String.format("  next alarm for user %s: %s",
324                        mCurrentUserId,
325                        formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId))));
326            } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
327                mBootCompleted = System.currentTimeMillis();
328            }
329            mHandler.postEvaluate(delay);
330            mWakeLock.acquire(delay + 5000);  // stay awake during evaluate
331        }
332    };
333
334    public interface Callback {
335        boolean isInDowntime();
336    }
337
338    private class H extends Handler {
339        private static final int MSG_EVALUATE = 1;
340
341        public void postEvaluate(long delay) {
342            removeMessages(MSG_EVALUATE);
343            sendEmptyMessageDelayed(MSG_EVALUATE, delay);
344        }
345
346        @Override
347        public void handleMessage(Message msg) {
348            if (msg.what == MSG_EVALUATE) {
349                handleEvaluate();
350            }
351        }
352    }
353}
354