NextAlarmConditionProvider.java revision 37bc92cc2332eb6f864977381135c19d6a081a92
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/** Built-in zen condition provider for alarm clock conditions */
49public class NextAlarmConditionProvider extends ConditionProviderService {
50    private static final String TAG = "NextAlarmConditions";
51    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
52
53    private static final String ACTION_TRIGGER = TAG + ".trigger";
54    private static final String EXTRA_TRIGGER = "trigger";
55    private static final int REQUEST_CODE = 100;
56    private static final long SECONDS = 1000;
57    private static final long HOURS = 60 * 60 * SECONDS;
58    private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS;  // treat clear+set as update
59    private static final long EARLY = 5 * SECONDS;  // fire early, ensure alarm stream is unmuted
60    private static final String NEXT_ALARM_PATH = "next_alarm";
61    public static final ComponentName COMPONENT =
62            new ComponentName("android", NextAlarmConditionProvider.class.getName());
63
64    private final Context mContext = this;
65    private final H mHandler = new H();
66
67    private boolean mConnected;
68    private boolean mRegistered;
69    private AlarmManager mAlarmManager;
70    private int mCurrentUserId;
71    private long mLookaheadThreshold;
72    private long mScheduledAlarmTime;
73    private Callback mCallback;
74    private Uri mCurrentSubscription;
75    private PowerManager.WakeLock mWakeLock;
76
77    public NextAlarmConditionProvider() {
78        if (DEBUG) Slog.d(TAG, "new NextAlarmConditionProvider()");
79    }
80
81    public void dump(PrintWriter pw, DumpFilter filter) {
82        pw.println("    NextAlarmConditionProvider:");
83        pw.print("      mConnected="); pw.println(mConnected);
84        pw.print("      mRegistered="); pw.println(mRegistered);
85        pw.print("      mCurrentUserId="); pw.println(mCurrentUserId);
86        pw.print("      mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime));
87        pw.print("      mLookaheadThreshold="); pw.print(mLookaheadThreshold);
88        pw.print(" ("); TimeUtils.formatDuration(mLookaheadThreshold, pw); pw.println(")");
89        pw.print("      mCurrentSubscription="); pw.println(mCurrentSubscription);
90        pw.print("      mWakeLock="); pw.println(mWakeLock);
91    }
92
93    public void setCallback(Callback callback) {
94        mCallback = callback;
95    }
96
97    @Override
98    public void onConnected() {
99        if (DEBUG) Slog.d(TAG, "onConnected");
100        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
101        final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
102        mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
103        mLookaheadThreshold = mContext.getResources()
104                .getInteger(R.integer.config_next_alarm_condition_lookahead_threshold_hrs) * HOURS;
105        init();
106        mConnected = true;
107    }
108
109    public void onUserSwitched() {
110        if (DEBUG) Slog.d(TAG, "onUserSwitched");
111        if (mConnected) {
112            init();
113        }
114    }
115
116    @Override
117    public void onDestroy() {
118        super.onDestroy();
119        if (DEBUG) Slog.d(TAG, "onDestroy");
120        if (mConnected) {
121            mContext.unregisterReceiver(mReceiver);
122        }
123        mConnected = false;
124    }
125
126    @Override
127    public void onRequestConditions(int relevance) {
128        if (!mConnected || (relevance & Condition.FLAG_RELEVANT_NOW) == 0) return;
129
130        final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId);
131        if (nextAlarm == null) return;  // no next alarm
132        if (mCallback != null && mCallback.isInDowntime()) return;  // prefer downtime condition
133        if (!isWithinLookaheadThreshold(nextAlarm)) return;  // alarm not within window
134
135        // next alarm exists, and is within the configured lookahead threshold
136        notifyCondition(newConditionId(), nextAlarm, true, "request");
137    }
138
139    private boolean isWithinLookaheadThreshold(AlarmClockInfo alarm) {
140        if (alarm == null) return false;
141        final long delta = getEarlyTriggerTime(alarm) - System.currentTimeMillis();
142        return delta > 0 && (mLookaheadThreshold <= 0 || delta < mLookaheadThreshold);
143    }
144
145    @Override
146    public void onSubscribe(Uri conditionId) {
147        if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
148        if (!isNextAlarmCondition(conditionId)) {
149            notifyCondition(conditionId, null, false, "badCondition");
150            return;
151        }
152        mCurrentSubscription = conditionId;
153        mHandler.postEvaluate(0);
154    }
155
156    private static long getEarlyTriggerTime(AlarmClockInfo alarm) {
157        return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0;
158    }
159
160    private void handleEvaluate() {
161        final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId);
162        final long triggerTime = getEarlyTriggerTime(nextAlarm);
163        final boolean withinThreshold = isWithinLookaheadThreshold(nextAlarm);
164        if (DEBUG) Slog.d(TAG, "handleEvaluate mCurrentSubscription=" + mCurrentSubscription
165                + " nextAlarm=" + formatAlarmDebug(triggerTime)
166                + " withinThreshold=" + withinThreshold);
167        if (mCurrentSubscription == null) return;  // no one cares
168        if (!withinThreshold) {
169            // triggertime invalid or in the past, condition = false
170            notifyCondition(mCurrentSubscription, nextAlarm, false, "!withinThreshold");
171            mCurrentSubscription = null;
172            return;
173        }
174        // triggertime in the future, condition = true, schedule alarm
175        notifyCondition(mCurrentSubscription, nextAlarm, true, "withinThreshold");
176        rescheduleAlarm(triggerTime);
177    }
178
179    private static String formatDuration(long millis) {
180        final StringBuilder sb = new StringBuilder();
181        TimeUtils.formatDuration(millis, sb);
182        return sb.toString();
183    }
184
185    private void rescheduleAlarm(long time) {
186        if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time);
187        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
188        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
189                new Intent(ACTION_TRIGGER)
190                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
191                        .putExtra(EXTRA_TRIGGER, time),
192                PendingIntent.FLAG_UPDATE_CURRENT);
193        alarms.cancel(pendingIntent);
194        mScheduledAlarmTime = time;
195        if (time > 0) {
196            if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)",
197                    formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis())));
198            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
199        }
200    }
201
202    private void notifyCondition(Uri id, AlarmClockInfo alarm, boolean state, String reason) {
203        final String formattedAlarm = alarm == null ? "" : formatAlarm(alarm.getTriggerTime());
204        if (DEBUG) Slog.d(TAG, "notifyCondition " + state + " alarm=" + formattedAlarm + " reason="
205                + reason);
206        notifyCondition(new Condition(id,
207                mContext.getString(R.string.zen_mode_next_alarm_summary, formattedAlarm),
208                mContext.getString(R.string.zen_mode_next_alarm_line_one),
209                formattedAlarm, 0,
210                state ? Condition.STATE_TRUE : Condition.STATE_FALSE,
211                Condition.FLAG_RELEVANT_NOW));
212    }
213
214    @Override
215    public void onUnsubscribe(Uri conditionId) {
216        if (DEBUG) Slog.d(TAG, "onUnsubscribe " + conditionId);
217        if (conditionId != null && conditionId.equals(mCurrentSubscription)) {
218            mCurrentSubscription = null;
219            rescheduleAlarm(0);
220        }
221    }
222
223    public void attachBase(Context base) {
224        attachBaseContext(base);
225    }
226
227    public IConditionProvider asInterface() {
228        return (IConditionProvider) onBind(null);
229    }
230
231    private Uri newConditionId() {
232        return new Uri.Builder().scheme(Condition.SCHEME)
233                .authority(ZenModeConfig.SYSTEM_AUTHORITY)
234                .appendPath(NEXT_ALARM_PATH)
235                .appendPath(Integer.toString(mCurrentUserId))
236                .build();
237    }
238
239    private boolean isNextAlarmCondition(Uri conditionId) {
240        return conditionId != null && conditionId.getScheme().equals(Condition.SCHEME)
241                && conditionId.getAuthority().equals(ZenModeConfig.SYSTEM_AUTHORITY)
242                && conditionId.getPathSegments().size() == 2
243                && conditionId.getPathSegments().get(0).equals(NEXT_ALARM_PATH)
244                && conditionId.getPathSegments().get(1).equals(Integer.toString(mCurrentUserId));
245    }
246
247    private void init() {
248        if (mRegistered) {
249            mContext.unregisterReceiver(mReceiver);
250        }
251        mCurrentUserId = ActivityManager.getCurrentUser();
252        final IntentFilter filter = new IntentFilter();
253        filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
254        filter.addAction(ACTION_TRIGGER);
255        filter.addAction(Intent.ACTION_TIME_CHANGED);
256        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
257        mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null,
258                null);
259        mRegistered = true;
260        mHandler.postEvaluate(0);
261    }
262
263    private String formatAlarm(long time) {
264        return formatAlarm(time, "Hm", "hma");
265    }
266
267    private String formatAlarm(long time, String skeleton24, String skeleton12) {
268        final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12;
269        final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
270        return DateFormat.format(pattern, time).toString();
271    }
272
273    private String formatAlarmDebug(AlarmClockInfo alarm) {
274        return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0);
275    }
276
277    private String formatAlarmDebug(long time) {
278        if (time <= 0) return Long.toString(time);
279        return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa"));
280    }
281
282    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
283        @Override
284        public void onReceive(Context context, Intent intent) {
285            final String action = intent.getAction();
286            if (DEBUG) Slog.d(TAG, "onReceive " + action);
287            long delay = 0;
288            if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
289                delay = NEXT_ALARM_UPDATE_DELAY;
290                if (DEBUG) Slog.d(TAG, String.format("  next alarm for user %s: %s",
291                        mCurrentUserId,
292                        formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId))));
293            }
294            mHandler.postEvaluate(delay);
295            mWakeLock.acquire(delay + 5000);  // stay awake during evaluate
296        }
297    };
298
299    public interface Callback {
300        boolean isInDowntime();
301    }
302
303    private class H extends Handler {
304        private static final int MSG_EVALUATE = 1;
305
306        public void postEvaluate(long delay) {
307            removeMessages(MSG_EVALUATE);
308            sendEmptyMessageDelayed(MSG_EVALUATE, delay);
309        }
310
311        @Override
312        public void handleMessage(Message msg) {
313            if (msg.what == MSG_EVALUATE) {
314                handleEvaluate();
315            }
316        }
317    }
318}
319