blob: 234f5455774f9813908757e21ea69185eabedfc4 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.notification;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AlarmManager.AlarmClockInfo;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.UserHandle;
import android.text.format.DateFormat;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Locale;
/** Helper for tracking updates to the current user's next alarm. */
public class NextAlarmTracker {
private static final String TAG = "NextAlarmTracker";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String ACTION_TRIGGER = TAG + ".trigger";
private static final String EXTRA_TRIGGER = "trigger";
private static final int REQUEST_CODE = 100;
private static final long SECONDS = 1000;
private static final long MINUTES = 60 * SECONDS;
private static final long NEXT_ALARM_UPDATE_DELAY = 1 * SECONDS; // treat clear+set as update
private static final long EARLY = 5 * SECONDS; // fire early, ensure alarm stream is unmuted
private static final long WAIT_AFTER_INIT = 5 * MINUTES;// for initial alarm re-registration
private static final long WAIT_AFTER_BOOT = 20 * SECONDS; // for initial alarm re-registration
private final Context mContext;
private final H mHandler = new H();
private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
private long mInit;
private boolean mRegistered;
private AlarmManager mAlarmManager;
private int mCurrentUserId;
private long mScheduledAlarmTime;
private long mBootCompleted;
private PowerManager.WakeLock mWakeLock;
public NextAlarmTracker(Context context) {
mContext = context;
}
public void dump(PrintWriter pw, DumpFilter filter) {
pw.println(" NextAlarmTracker:");
pw.print(" len(mCallbacks)="); pw.println(mCallbacks.size());
pw.print(" mRegistered="); pw.println(mRegistered);
pw.print(" mInit="); pw.println(mInit);
pw.print(" mBootCompleted="); pw.println(mBootCompleted);
pw.print(" mCurrentUserId="); pw.println(mCurrentUserId);
pw.print(" mScheduledAlarmTime="); pw.println(formatAlarmDebug(mScheduledAlarmTime));
pw.print(" mWakeLock="); pw.println(mWakeLock);
}
public void addCallback(Callback callback) {
mCallbacks.add(callback);
}
public void removeCallback(Callback callback) {
mCallbacks.remove(callback);
}
public int getCurrentUserId() {
return mCurrentUserId;
}
public AlarmClockInfo getNextAlarm() {
return mAlarmManager.getNextAlarmClock(mCurrentUserId);
}
public void onUserSwitched() {
reset();
}
public void init() {
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PowerManager p = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mWakeLock = p.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mInit = System.currentTimeMillis();
reset();
}
public void reset() {
if (mRegistered) {
mContext.unregisterReceiver(mReceiver);
}
mCurrentUserId = ActivityManager.getCurrentUser();
final IntentFilter filter = new IntentFilter();
filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
filter.addAction(ACTION_TRIGGER);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
filter.addAction(Intent.ACTION_BOOT_COMPLETED);
mContext.registerReceiverAsUser(mReceiver, new UserHandle(mCurrentUserId), filter, null,
null);
mRegistered = true;
evaluate();
}
public void destroy() {
if (mRegistered) {
mContext.unregisterReceiver(mReceiver);
mRegistered = false;
}
}
public void evaluate() {
mHandler.postEvaluate(0);
}
private void fireEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted) {
for (Callback callback : mCallbacks) {
callback.onEvaluate(nextAlarm, wakeupTime, booted);
}
}
private void handleEvaluate() {
final AlarmClockInfo nextAlarm = mAlarmManager.getNextAlarmClock(mCurrentUserId);
final long triggerTime = getEarlyTriggerTime(nextAlarm);
final long now = System.currentTimeMillis();
final boolean alarmUpcoming = triggerTime > now;
final boolean booted = isDoneWaitingAfterBoot(now);
if (DEBUG) Slog.d(TAG, "handleEvaluate nextAlarm=" + formatAlarmDebug(triggerTime)
+ " alarmUpcoming=" + alarmUpcoming
+ " booted=" + booted);
fireEvaluate(nextAlarm, triggerTime, booted);
if (!booted) {
// recheck after boot
final long recheckTime = (mBootCompleted > 0 ? mBootCompleted : now) + WAIT_AFTER_BOOT;
rescheduleAlarm(recheckTime);
return;
}
if (alarmUpcoming) {
// wake up just before the next alarm
rescheduleAlarm(triggerTime);
}
}
public static long getEarlyTriggerTime(AlarmClockInfo alarm) {
return alarm != null ? (alarm.getTriggerTime() - EARLY) : 0;
}
private boolean isDoneWaitingAfterBoot(long time) {
if (mBootCompleted > 0) return (time - mBootCompleted) > WAIT_AFTER_BOOT;
if (mInit > 0) return (time - mInit) > WAIT_AFTER_INIT;
return true;
}
public static String formatDuration(long millis) {
final StringBuilder sb = new StringBuilder();
TimeUtils.formatDuration(millis, sb);
return sb.toString();
}
public String formatAlarm(AlarmClockInfo alarm) {
return alarm != null ? formatAlarm(alarm.getTriggerTime()) : null;
}
private String formatAlarm(long time) {
return formatAlarm(time, "Hm", "hma");
}
private String formatAlarm(long time, String skeleton24, String skeleton12) {
final String skeleton = DateFormat.is24HourFormat(mContext) ? skeleton24 : skeleton12;
final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
return DateFormat.format(pattern, time).toString();
}
public String formatAlarmDebug(AlarmClockInfo alarm) {
return formatAlarmDebug(alarm != null ? alarm.getTriggerTime() : 0);
}
public String formatAlarmDebug(long time) {
if (time <= 0) return Long.toString(time);
return String.format("%s (%s)", time, formatAlarm(time, "Hms", "hmsa"));
}
private void rescheduleAlarm(long time) {
if (DEBUG) Slog.d(TAG, "rescheduleAlarm " + time);
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
new Intent(ACTION_TRIGGER)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_TRIGGER, time),
PendingIntent.FLAG_UPDATE_CURRENT);
alarms.cancel(pendingIntent);
mScheduledAlarmTime = time;
if (time > 0) {
if (DEBUG) Slog.d(TAG, String.format("Scheduling alarm for %s (in %s)",
formatAlarmDebug(time), formatDuration(time - System.currentTimeMillis())));
alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (DEBUG) Slog.d(TAG, "onReceive " + action);
long delay = 0;
if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
delay = NEXT_ALARM_UPDATE_DELAY;
if (DEBUG) Slog.d(TAG, String.format(" next alarm for user %s: %s",
mCurrentUserId,
formatAlarmDebug(mAlarmManager.getNextAlarmClock(mCurrentUserId))));
} else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
mBootCompleted = System.currentTimeMillis();
}
mHandler.postEvaluate(delay);
mWakeLock.acquire(delay + 5000); // stay awake during evaluate
}
};
private class H extends Handler {
private static final int MSG_EVALUATE = 1;
public void postEvaluate(long delay) {
removeMessages(MSG_EVALUATE);
sendEmptyMessageDelayed(MSG_EVALUATE, delay);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_EVALUATE) {
handleEvaluate();
}
}
}
public interface Callback {
void onEvaluate(AlarmClockInfo nextAlarm, long wakeupTime, boolean booted);
}
}