1/*
2 * Copyright (C) 2011, 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.sip;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.os.SystemClock;
26import android.telephony.Rlog;
27
28import java.util.Comparator;
29import java.util.Iterator;
30import java.util.TreeSet;
31import java.util.concurrent.Executor;
32
33/**
34 * Timer that can schedule events to occur even when the device is in sleep.
35 */
36class SipWakeupTimer extends BroadcastReceiver {
37    private static final String TAG = "SipWakeupTimer";
38    private static final boolean DBG = SipService.DBG && true; // STOPSHIP if true
39    private static final String TRIGGER_TIME = "TriggerTime";
40
41    private Context mContext;
42    private AlarmManager mAlarmManager;
43
44    // runnable --> time to execute in SystemClock
45    private TreeSet<MyEvent> mEventQueue =
46            new TreeSet<MyEvent>(new MyEventComparator());
47
48    private PendingIntent mPendingIntent;
49
50    private Executor mExecutor;
51
52    public SipWakeupTimer(Context context, Executor executor) {
53        mContext = context;
54        mAlarmManager = (AlarmManager)
55                context.getSystemService(Context.ALARM_SERVICE);
56
57        IntentFilter filter = new IntentFilter(getAction());
58        context.registerReceiver(this, filter);
59        mExecutor = executor;
60    }
61
62    /**
63     * Stops the timer. No event can be scheduled after this method is called.
64     */
65    public synchronized void stop() {
66        mContext.unregisterReceiver(this);
67        if (mPendingIntent != null) {
68            mAlarmManager.cancel(mPendingIntent);
69            mPendingIntent = null;
70        }
71        mEventQueue.clear();
72        mEventQueue = null;
73    }
74
75    private boolean stopped() {
76        if (mEventQueue == null) {
77            if (DBG) log("Timer stopped");
78            return true;
79        } else {
80            return false;
81        }
82    }
83
84    private void cancelAlarm() {
85        mAlarmManager.cancel(mPendingIntent);
86        mPendingIntent = null;
87    }
88
89    private void recalculatePeriods() {
90        if (mEventQueue.isEmpty()) return;
91
92        MyEvent firstEvent = mEventQueue.first();
93        int minPeriod = firstEvent.mMaxPeriod;
94        long minTriggerTime = firstEvent.mTriggerTime;
95        for (MyEvent e : mEventQueue) {
96            e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
97            int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
98                    - minTriggerTime);
99            interval = interval / minPeriod * minPeriod;
100            e.mTriggerTime = minTriggerTime + interval;
101        }
102        TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
103                mEventQueue.comparator());
104        newQueue.addAll(mEventQueue);
105        mEventQueue.clear();
106        mEventQueue = newQueue;
107        if (DBG) {
108            log("queue re-calculated");
109            printQueue();
110        }
111    }
112
113    // Determines the period and the trigger time of the new event and insert it
114    // to the queue.
115    private void insertEvent(MyEvent event) {
116        long now = SystemClock.elapsedRealtime();
117        if (mEventQueue.isEmpty()) {
118            event.mTriggerTime = now + event.mPeriod;
119            mEventQueue.add(event);
120            return;
121        }
122        MyEvent firstEvent = mEventQueue.first();
123        int minPeriod = firstEvent.mPeriod;
124        if (minPeriod <= event.mMaxPeriod) {
125            event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
126            int interval = event.mMaxPeriod;
127            interval -= (int) (firstEvent.mTriggerTime - now);
128            interval = interval / minPeriod * minPeriod;
129            event.mTriggerTime = firstEvent.mTriggerTime + interval;
130            mEventQueue.add(event);
131        } else {
132            long triggerTime = now + event.mPeriod;
133            if (firstEvent.mTriggerTime < triggerTime) {
134                event.mTriggerTime = firstEvent.mTriggerTime;
135                event.mLastTriggerTime -= event.mPeriod;
136            } else {
137                event.mTriggerTime = triggerTime;
138            }
139            mEventQueue.add(event);
140            recalculatePeriods();
141        }
142    }
143
144    /**
145     * Sets a periodic timer.
146     *
147     * @param period the timer period; in milli-second
148     * @param callback is called back when the timer goes off; the same callback
149     *      can be specified in multiple timer events
150     */
151    public synchronized void set(int period, Runnable callback) {
152        if (stopped()) return;
153
154        long now = SystemClock.elapsedRealtime();
155        MyEvent event = new MyEvent(period, callback, now);
156        insertEvent(event);
157
158        if (mEventQueue.first() == event) {
159            if (mEventQueue.size() > 1) cancelAlarm();
160            scheduleNext();
161        }
162
163        long triggerTime = event.mTriggerTime;
164        if (DBG) {
165            log("set: add event " + event + " scheduled on "
166                    + showTime(triggerTime) + " at " + showTime(now)
167                    + ", #events=" + mEventQueue.size());
168            printQueue();
169        }
170    }
171
172    /**
173     * Cancels all the timer events with the specified callback.
174     *
175     * @param callback the callback
176     */
177    public synchronized void cancel(Runnable callback) {
178        if (stopped() || mEventQueue.isEmpty()) return;
179        if (DBG) log("cancel:" + callback);
180
181        MyEvent firstEvent = mEventQueue.first();
182        for (Iterator<MyEvent> iter = mEventQueue.iterator();
183                iter.hasNext();) {
184            MyEvent event = iter.next();
185            if (event.mCallback == callback) {
186                iter.remove();
187                if (DBG) log("    cancel found:" + event);
188            }
189        }
190        if (mEventQueue.isEmpty()) {
191            cancelAlarm();
192        } else if (mEventQueue.first() != firstEvent) {
193            cancelAlarm();
194            firstEvent = mEventQueue.first();
195            firstEvent.mPeriod = firstEvent.mMaxPeriod;
196            firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
197                    + firstEvent.mPeriod;
198            recalculatePeriods();
199            scheduleNext();
200        }
201        if (DBG) {
202            log("cancel: X");
203            printQueue();
204        }
205    }
206
207    private void scheduleNext() {
208        if (stopped() || mEventQueue.isEmpty()) return;
209
210        if (mPendingIntent != null) {
211            throw new RuntimeException("pendingIntent is not null!");
212        }
213
214        MyEvent event = mEventQueue.first();
215        Intent intent = new Intent(getAction());
216        intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
217        PendingIntent pendingIntent = mPendingIntent =
218                PendingIntent.getBroadcast(mContext, 0, intent,
219                        PendingIntent.FLAG_UPDATE_CURRENT);
220        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
221                event.mTriggerTime, pendingIntent);
222    }
223
224    @Override
225    public synchronized void onReceive(Context context, Intent intent) {
226        // This callback is already protected by AlarmManager's wake lock.
227        String action = intent.getAction();
228        if (getAction().equals(action)
229                && intent.getExtras().containsKey(TRIGGER_TIME)) {
230            mPendingIntent = null;
231            long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
232            execute(triggerTime);
233        } else {
234            log("onReceive: unrecognized intent: " + intent);
235        }
236    }
237
238    private void printQueue() {
239        int count = 0;
240        for (MyEvent event : mEventQueue) {
241            log("     " + event + ": scheduled at "
242                    + showTime(event.mTriggerTime) + ": last at "
243                    + showTime(event.mLastTriggerTime));
244            if (++count >= 5) break;
245        }
246        if (mEventQueue.size() > count) {
247            log("     .....");
248        } else if (count == 0) {
249            log("     <empty>");
250        }
251    }
252
253    private void execute(long triggerTime) {
254        if (DBG) log("time's up, triggerTime = "
255                + showTime(triggerTime) + ": " + mEventQueue.size());
256        if (stopped() || mEventQueue.isEmpty()) return;
257
258        for (MyEvent event : mEventQueue) {
259            if (event.mTriggerTime != triggerTime) continue;
260            if (DBG) log("execute " + event);
261
262            event.mLastTriggerTime = triggerTime;
263            event.mTriggerTime += event.mPeriod;
264
265            // run the callback in the handler thread to prevent deadlock
266            mExecutor.execute(event.mCallback);
267        }
268        if (DBG) {
269            log("after timeout execution");
270            printQueue();
271        }
272        scheduleNext();
273    }
274
275    private String getAction() {
276        return toString();
277    }
278
279    private String showTime(long time) {
280        int ms = (int) (time % 1000);
281        int s = (int) (time / 1000);
282        int m = s / 60;
283        s %= 60;
284        return String.format("%d.%d.%d", m, s, ms);
285    }
286
287    private static class MyEvent {
288        int mPeriod;
289        int mMaxPeriod;
290        long mTriggerTime;
291        long mLastTriggerTime;
292        Runnable mCallback;
293
294        MyEvent(int period, Runnable callback, long now) {
295            mPeriod = mMaxPeriod = period;
296            mCallback = callback;
297            mLastTriggerTime = now;
298        }
299
300        @Override
301        public String toString() {
302            String s = super.toString();
303            s = s.substring(s.indexOf("@"));
304            return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
305                    + toString(mCallback);
306        }
307
308        private String toString(Object o) {
309            String s = o.toString();
310            int index = s.indexOf("$");
311            if (index > 0) s = s.substring(index + 1);
312            return s;
313        }
314    }
315
316    // Sort the events by mMaxPeriod so that the first event can be used to
317    // align events with larger periods
318    private static class MyEventComparator implements Comparator<MyEvent> {
319        @Override
320        public int compare(MyEvent e1, MyEvent e2) {
321            if (e1 == e2) return 0;
322            int diff = e1.mMaxPeriod - e2.mMaxPeriod;
323            if (diff == 0) diff = -1;
324            return diff;
325        }
326
327        @Override
328        public boolean equals(Object that) {
329            return (this == that);
330        }
331    }
332
333    private void log(String s) {
334        Rlog.d(TAG, s);
335    }
336}
337