/* * Copyright (C) 2011, 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.sip; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.SystemClock; import android.telephony.Rlog; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; import java.util.concurrent.Executor; /** * Timer that can schedule events to occur even when the device is in sleep. */ class SipWakeupTimer extends BroadcastReceiver { private static final String TAG = "SipWakeupTimer"; private static final boolean DBG = SipService.DBG && false; // STOPSHIP if true private static final String TRIGGER_TIME = "TriggerTime"; private Context mContext; private AlarmManager mAlarmManager; // runnable --> time to execute in SystemClock private TreeSet mEventQueue = new TreeSet(new MyEventComparator()); private PendingIntent mPendingIntent; private Executor mExecutor; public SipWakeupTimer(Context context, Executor executor) { mContext = context; mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); IntentFilter filter = new IntentFilter(getAction()); context.registerReceiver(this, filter); mExecutor = executor; } /** * Stops the timer. No event can be scheduled after this method is called. */ public synchronized void stop() { mContext.unregisterReceiver(this); if (mPendingIntent != null) { mAlarmManager.cancel(mPendingIntent); mPendingIntent = null; } mEventQueue.clear(); mEventQueue = null; } private boolean stopped() { if (mEventQueue == null) { if (DBG) log("Timer stopped"); return true; } else { return false; } } private void cancelAlarm() { mAlarmManager.cancel(mPendingIntent); mPendingIntent = null; } private void recalculatePeriods() { if (mEventQueue.isEmpty()) return; MyEvent firstEvent = mEventQueue.first(); int minPeriod = firstEvent.mMaxPeriod; long minTriggerTime = firstEvent.mTriggerTime; for (MyEvent e : mEventQueue) { e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod; int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod - minTriggerTime); interval = interval / minPeriod * minPeriod; e.mTriggerTime = minTriggerTime + interval; } TreeSet newQueue = new TreeSet( mEventQueue.comparator()); newQueue.addAll(mEventQueue); mEventQueue.clear(); mEventQueue = newQueue; if (DBG) { log("queue re-calculated"); printQueue(); } } // Determines the period and the trigger time of the new event and insert it // to the queue. private void insertEvent(MyEvent event) { long now = SystemClock.elapsedRealtime(); if (mEventQueue.isEmpty()) { event.mTriggerTime = now + event.mPeriod; mEventQueue.add(event); return; } MyEvent firstEvent = mEventQueue.first(); int minPeriod = firstEvent.mPeriod; if (minPeriod <= event.mMaxPeriod) { event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod; int interval = event.mMaxPeriod; interval -= (int) (firstEvent.mTriggerTime - now); interval = interval / minPeriod * minPeriod; event.mTriggerTime = firstEvent.mTriggerTime + interval; mEventQueue.add(event); } else { long triggerTime = now + event.mPeriod; if (firstEvent.mTriggerTime < triggerTime) { event.mTriggerTime = firstEvent.mTriggerTime; event.mLastTriggerTime -= event.mPeriod; } else { event.mTriggerTime = triggerTime; } mEventQueue.add(event); recalculatePeriods(); } } /** * Sets a periodic timer. * * @param period the timer period; in milli-second * @param callback is called back when the timer goes off; the same callback * can be specified in multiple timer events */ public synchronized void set(int period, Runnable callback) { if (stopped()) return; long now = SystemClock.elapsedRealtime(); MyEvent event = new MyEvent(period, callback, now); insertEvent(event); if (mEventQueue.first() == event) { if (mEventQueue.size() > 1) cancelAlarm(); scheduleNext(); } long triggerTime = event.mTriggerTime; if (DBG) { log("set: add event " + event + " scheduled on " + showTime(triggerTime) + " at " + showTime(now) + ", #events=" + mEventQueue.size()); printQueue(); } } /** * Cancels all the timer events with the specified callback. * * @param callback the callback */ public synchronized void cancel(Runnable callback) { if (stopped() || mEventQueue.isEmpty()) return; if (DBG) log("cancel:" + callback); MyEvent firstEvent = mEventQueue.first(); for (Iterator iter = mEventQueue.iterator(); iter.hasNext();) { MyEvent event = iter.next(); if (event.mCallback == callback) { iter.remove(); if (DBG) log(" cancel found:" + event); } } if (mEventQueue.isEmpty()) { cancelAlarm(); } else if (mEventQueue.first() != firstEvent) { cancelAlarm(); firstEvent = mEventQueue.first(); firstEvent.mPeriod = firstEvent.mMaxPeriod; firstEvent.mTriggerTime = firstEvent.mLastTriggerTime + firstEvent.mPeriod; recalculatePeriods(); scheduleNext(); } if (DBG) { log("cancel: X"); printQueue(); } } private void scheduleNext() { if (stopped() || mEventQueue.isEmpty()) return; if (mPendingIntent != null) { throw new RuntimeException("pendingIntent is not null!"); } MyEvent event = mEventQueue.first(); Intent intent = new Intent(getAction()); intent.putExtra(TRIGGER_TIME, event.mTriggerTime); PendingIntent pendingIntent = mPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, event.mTriggerTime, pendingIntent); } @Override public synchronized void onReceive(Context context, Intent intent) { // This callback is already protected by AlarmManager's wake lock. String action = intent.getAction(); if (getAction().equals(action) && intent.getExtras().containsKey(TRIGGER_TIME)) { mPendingIntent = null; long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L); execute(triggerTime); } else { log("onReceive: unrecognized intent: " + intent); } } private void printQueue() { int count = 0; for (MyEvent event : mEventQueue) { log(" " + event + ": scheduled at " + showTime(event.mTriggerTime) + ": last at " + showTime(event.mLastTriggerTime)); if (++count >= 5) break; } if (mEventQueue.size() > count) { log(" ....."); } else if (count == 0) { log(" "); } } private void execute(long triggerTime) { if (DBG) log("time's up, triggerTime = " + showTime(triggerTime) + ": " + mEventQueue.size()); if (stopped() || mEventQueue.isEmpty()) return; for (MyEvent event : mEventQueue) { if (event.mTriggerTime != triggerTime) continue; if (DBG) log("execute " + event); event.mLastTriggerTime = triggerTime; event.mTriggerTime += event.mPeriod; // run the callback in the handler thread to prevent deadlock mExecutor.execute(event.mCallback); } if (DBG) { log("after timeout execution"); printQueue(); } scheduleNext(); } private String getAction() { return toString(); } private String showTime(long time) { int ms = (int) (time % 1000); int s = (int) (time / 1000); int m = s / 60; s %= 60; return String.format("%d.%d.%d", m, s, ms); } private static class MyEvent { int mPeriod; int mMaxPeriod; long mTriggerTime; long mLastTriggerTime; Runnable mCallback; MyEvent(int period, Runnable callback, long now) { mPeriod = mMaxPeriod = period; mCallback = callback; mLastTriggerTime = now; } @Override public String toString() { String s = super.toString(); s = s.substring(s.indexOf("@")); return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":" + toString(mCallback); } private String toString(Object o) { String s = o.toString(); int index = s.indexOf("$"); if (index > 0) s = s.substring(index + 1); return s; } } // Sort the events by mMaxPeriod so that the first event can be used to // align events with larger periods private static class MyEventComparator implements Comparator { @Override public int compare(MyEvent e1, MyEvent e2) { if (e1 == e2) return 0; int diff = e1.mMaxPeriod - e2.mMaxPeriod; if (diff == 0) diff = -1; return diff; } @Override public boolean equals(Object that) { return (this == that); } } private void log(String s) { Rlog.d(TAG, s); } }