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