NextAlarmConditionProvider.java revision 6b0623a1129a7e1bc4949b7dca4fa133ff322c4a
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.AlarmManager;
20import android.app.AlarmManager.AlarmClockInfo;
21import android.content.ComponentName;
22import android.content.Context;
23import android.net.Uri;
24import android.service.notification.Condition;
25import android.service.notification.ConditionProviderService;
26import android.service.notification.IConditionProvider;
27import android.service.notification.ZenModeConfig;
28import android.util.Log;
29import android.util.Slog;
30import android.util.TimeUtils;
31
32import com.android.internal.R;
33import com.android.server.notification.NotificationManagerService.DumpFilter;
34
35import java.io.PrintWriter;
36
37/**
38 * Built-in zen condition provider for alarm-clock-based conditions.
39 *
40 * <p>If the user's next alarm is within a lookahead threshold (config, default 12hrs), advertise
41 * it as an exit condition for zen mode (unless the built-in downtime condition is also available).
42 *
43 * <p>When this next alarm is selected as the active exit condition, follow subsequent changes
44 * to the user's next alarm, assuming it remains within the 12-hr window.
45 *
46 * <p>The next alarm is defined as {@link AlarmManager#getNextAlarmClock(int)}, which does not
47 * survive a reboot.  Maintain the illusion of a consistent next alarm value by holding on to
48 * a persisted condition until we receive the first value after reboot, or timeout with no value.
49 */
50public class NextAlarmConditionProvider extends ConditionProviderService {
51    private static final String TAG = "NextAlarmConditions";
52    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53
54    private static final long SECONDS = 1000;
55    private static final long MINUTES = 60 * SECONDS;
56    private static final long HOURS = 60 * MINUTES;
57
58    private static final String NEXT_ALARM_PATH = "next_alarm";
59    public static final ComponentName COMPONENT =
60            new ComponentName("android", NextAlarmConditionProvider.class.getName());
61
62    private final Context mContext = this;
63
64    private NextAlarmTracker mTracker;
65    private boolean mConnected;
66    private long mLookaheadThreshold;
67    private Callback mCallback;
68    private Uri mCurrentSubscription;
69
70    public NextAlarmConditionProvider() {
71        if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()");
72    }
73
74    public void dump(PrintWriter pw, DumpFilter filter) {
75        pw.println("    NextAlarmConditionProvider:");
76        pw.print("      mConnected="); pw.println(mConnected);
77        pw.print("      mLookaheadThreshold="); pw.print(mLookaheadThreshold);
78        pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
79        pw.print("      mCurrentSubscription="); pw.println(mCurrentSubscription);
80    }
81
82    public void setCallback(Callback callback) {
83        mCallback = callback;
84    }
85
86    @Override
87    public void onConnected() {
88        if (DEBUG) Slog.d(TAG, "onConnected");
89        mLookaheadThreshold = mContext.getResources()
90                .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS;
91        mConnected = true;
92        mTracker = mCallback.getNextAlarmTracker();
93        mTracker.addCallback(mTrackerCallback);
94    }
95
96    @Override
97    public void onDestroy() {
98        super.onDestroy();
99        if (DEBUG) Slog.d(TAG, "onDestroy");
100        mTracker.removeCallback(mTrackerCallback);
101        mConnected = false;
102    }
103
104    @Override
105    public void onRequestConditions(int relevance) {
106        if (!mConnected || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return;
107
108        final AlarmClockInfo nextAlarm = mTracker.getNextAlarm();
109        if (nextAlarm == null) return;  // no next alarm
110        if (mCallback != null && mCallback.isInDowntime()) return;  // prefer downtime condition
111        if (!isWithinLookaheadThreshold(nextAlarm)) return;  // alarm not within window
112
113        // next alarm exists, and is within the configured lookahead threshold
114        notifyCondition(newConditionId(), nextAlarm, Condition.STATE_TRUE, "request");
115    }
116
117    @Override
118    public void onSubscribe(Uri conditionId) {
119        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
120        if (!isNextAlarmCondition(conditionId)) {
121            notifyCondition(conditionId, null, Condition.STATE_FALSE, "badCondition");
122            return;
123        }
124        mCurrentSubscription = conditionId;
125        mTracker.evaluate();
126    }
127
128    @Override
129    public void onUnsubscribe(Uri conditionId) {
130        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
131        if (conditionId != null && conditionId.equals(mCurrentSubscription)) {
132            mCurrentSubscription = null;
133        }
134    }
135
136    public void attachBase(Context base) {
137        attachBaseContext(base);
138    }
139
140    public IConditionProvider asInterface() {
141        return (IConditionProvider) onBind(null);
142    }
143
144    private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) {
145        if (alarm == null) return false;
146        final long delta = NextAlarmTracker.getEarlyTriggerTime(alarm) - System.currentTimeMillis();
147        return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold);
148    }
149
150    private void notifyCondition(Uri id, AlarmClockInfo alarm, int state, String reason) {
151        final String formattedAlarm = alarm == null ? "" : mTracker.formatAlarm(alarm);
152        if (DEBUG) Slog.d(TAG, "notifyCondition " + Condition.stateToString(state)
153                + " alarm=" + formattedAlarm + " reason=" + reason);
154        notifyCondition(new Condition(id,
155                mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm),
156                mContext.getString(R.string.zen_mode_next_alarm_line_one),
157                formattedAlarm, 0, state, Condition.FLAG_RELEVANT_NOW));
158    }
159
160    private Uri newConditionId() {
161        return new Uri.Builder().scheme(Condition.SCHEME)
162                .authority(ZenModeConfig.SYSTEM_AUTHORITY)
163                .appendPath(NEXT_ALARM_PATH)
164                .appendPath(Integer.toString(mTracker.getCurrentUserId()))
165                .build();
166    }
167
168    private boolean isNextAlarmCondition(Uri conditionId) {
169        return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME)
170                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
171                && conditionId.getPathSegments().size() == 2
172                && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH)
173                && conditionId.getPathSegments().get(1)
174                        .equals(Integer.toString(mTracker.getCurrentUserId()));
175    }
176
177    private void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
178        final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm);
179        if (DEBUG) Slog.d(TAG, "onEvaluate mCurrentSubscription=" + mCurrentSubscription
180                + " nextAlarmWakeup=" + mTracker.formatAlarmDebug(wakeupTime)
181                + " withinThreshold=" + withinThreshold
182                + " booted=" + booted);
183        if (mCurrentSubscription == null) return;  // no one cares
184        if (!booted) {
185            // we don't know yet
186            notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_UNKNOWN, "!booted");
187            return;
188        }
189        if (!withinThreshold) {
190            // next alarm outside threshold or in the past, condition = false
191            notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_FALSE, "!within");
192            mCurrentSubscription = null;
193            return;
194        }
195        // next alarm in the future and within threshold, condition = true
196        notifyCondition(mCurrentSubscription, nextAlarm, Condition.STATE_TRUE, "within");
197    }
198
199    private final NextAlarmTracker.Callback mTrackerCallback = new NextAlarmTracker.Callback() {
200        @Override
201        public void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
202            NextAlarmConditionProvider.this.onEvaluate(nextAlarm, wakeupTime, booted);
203        }
204    };
205
206    public interface Callback {
207        boolean isInDowntime();
208        NextAlarmTracker getNextAlarmTracker();
209    }
210}
211