/* * 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 mCallbacks = new ArrayList(); 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); } }