1/*
2 * Copyright (C) 2010, 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.net.ConnectivityManager;
26import android.net.NetworkInfo;
27import android.net.sip.ISipService;
28import android.net.sip.ISipSession;
29import android.net.sip.ISipSessionListener;
30import android.net.sip.SipErrorCode;
31import android.net.sip.SipManager;
32import android.net.sip.SipProfile;
33import android.net.sip.SipSession;
34import android.net.sip.SipSessionAdapter;
35import android.net.wifi.WifiManager;
36import android.os.Binder;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.HandlerThread;
40import android.os.Looper;
41import android.os.Message;
42import android.os.PowerManager;
43import android.os.Process;
44import android.os.RemoteException;
45import android.os.ServiceManager;
46import android.os.SystemClock;
47import android.text.TextUtils;
48import android.util.Log;
49
50import java.io.IOException;
51import java.net.DatagramSocket;
52import java.net.InetAddress;
53import java.net.UnknownHostException;
54import java.util.ArrayList;
55import java.util.Collection;
56import java.util.Comparator;
57import java.util.HashMap;
58import java.util.Iterator;
59import java.util.Map;
60import java.util.Timer;
61import java.util.TimerTask;
62import java.util.TreeSet;
63import javax.sip.SipException;
64
65/**
66 * @hide
67 */
68public final class SipService extends ISipService.Stub {
69    static final String TAG = "SipService";
70    static final boolean DEBUGV = false;
71    private static final boolean DEBUG = false;
72    private static final boolean DEBUG_TIMER = DEBUG && false;
73    private static final int EXPIRY_TIME = 3600;
74    private static final int SHORT_EXPIRY_TIME = 10;
75    private static final int MIN_EXPIRY_TIME = 60;
76
77    private Context mContext;
78    private String mLocalIp;
79    private String mNetworkType;
80    private boolean mConnected;
81    private WakeupTimer mTimer;
82    private WifiScanProcess mWifiScanProcess;
83    private WifiManager.WifiLock mWifiLock;
84    private boolean mWifiOnly;
85
86    private MyExecutor mExecutor;
87
88    // SipProfile URI --> group
89    private Map<String, SipSessionGroupExt> mSipGroups =
90            new HashMap<String, SipSessionGroupExt>();
91
92    // session ID --> session
93    private Map<String, ISipSession> mPendingSessions =
94            new HashMap<String, ISipSession>();
95
96    private ConnectivityReceiver mConnectivityReceiver;
97    private boolean mWifiEnabled;
98    private SipWakeLock mMyWakeLock;
99
100    /**
101     * Starts the SIP service. Do nothing if the SIP API is not supported on the
102     * device.
103     */
104    public static void start(Context context) {
105        if (SipManager.isApiSupported(context)) {
106            ServiceManager.addService("sip", new SipService(context));
107            context.sendBroadcast(new Intent(SipManager.ACTION_SIP_SERVICE_UP));
108            if (DEBUG) Log.d(TAG, "SIP service started");
109        }
110    }
111
112    private SipService(Context context) {
113        if (DEBUG) Log.d(TAG, " service started!");
114        mContext = context;
115        mConnectivityReceiver = new ConnectivityReceiver();
116        mMyWakeLock = new SipWakeLock((PowerManager)
117                context.getSystemService(Context.POWER_SERVICE));
118
119        mTimer = new WakeupTimer(context);
120        mWifiOnly = SipManager.isSipWifiOnly(context);
121    }
122
123    private BroadcastReceiver mWifiStateReceiver = new BroadcastReceiver() {
124        @Override
125        public void onReceive(Context context, Intent intent) {
126            String action = intent.getAction();
127            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
128                int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
129                        WifiManager.WIFI_STATE_UNKNOWN);
130                synchronized (SipService.this) {
131                    switch (state) {
132                        case WifiManager.WIFI_STATE_ENABLED:
133                            mWifiEnabled = true;
134                            if (anyOpenedToReceiveCalls()) grabWifiLock();
135                            break;
136                        case WifiManager.WIFI_STATE_DISABLED:
137                            mWifiEnabled = false;
138                            releaseWifiLock();
139                            break;
140                    }
141                }
142            }
143        }
144    };
145
146    private void registerReceivers() {
147        mContext.registerReceiver(mConnectivityReceiver,
148                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
149        mContext.registerReceiver(mWifiStateReceiver,
150                new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
151        if (DEBUG) Log.d(TAG, " +++ register receivers");
152    }
153
154    private void unregisterReceivers() {
155        mContext.unregisterReceiver(mConnectivityReceiver);
156        mContext.unregisterReceiver(mWifiStateReceiver);
157        if (DEBUG) Log.d(TAG, " --- unregister receivers");
158    }
159
160    private MyExecutor getExecutor() {
161        // create mExecutor lazily
162        if (mExecutor == null) mExecutor = new MyExecutor();
163        return mExecutor;
164    }
165
166    public synchronized SipProfile[] getListOfProfiles() {
167        mContext.enforceCallingOrSelfPermission(
168                android.Manifest.permission.USE_SIP, null);
169        boolean isCallerRadio = isCallerRadio();
170        ArrayList<SipProfile> profiles = new ArrayList<SipProfile>();
171        for (SipSessionGroupExt group : mSipGroups.values()) {
172            if (isCallerRadio || isCallerCreator(group)) {
173                profiles.add(group.getLocalProfile());
174            }
175        }
176        return profiles.toArray(new SipProfile[profiles.size()]);
177    }
178
179    public synchronized void open(SipProfile localProfile) {
180        mContext.enforceCallingOrSelfPermission(
181                android.Manifest.permission.USE_SIP, null);
182        localProfile.setCallingUid(Binder.getCallingUid());
183        try {
184            boolean addingFirstProfile = mSipGroups.isEmpty();
185            createGroup(localProfile);
186            if (addingFirstProfile && !mSipGroups.isEmpty()) registerReceivers();
187        } catch (SipException e) {
188            Log.e(TAG, "openToMakeCalls()", e);
189            // TODO: how to send the exception back
190        }
191    }
192
193    public synchronized void open3(SipProfile localProfile,
194            PendingIntent incomingCallPendingIntent,
195            ISipSessionListener listener) {
196        mContext.enforceCallingOrSelfPermission(
197                android.Manifest.permission.USE_SIP, null);
198        localProfile.setCallingUid(Binder.getCallingUid());
199        if (incomingCallPendingIntent == null) {
200            Log.w(TAG, "incomingCallPendingIntent cannot be null; "
201                    + "the profile is not opened");
202            return;
203        }
204        if (DEBUG) Log.d(TAG, "open3: " + localProfile.getUriString() + ": "
205                + incomingCallPendingIntent + ": " + listener);
206        try {
207            boolean addingFirstProfile = mSipGroups.isEmpty();
208            SipSessionGroupExt group = createGroup(localProfile,
209                    incomingCallPendingIntent, listener);
210            if (addingFirstProfile && !mSipGroups.isEmpty()) registerReceivers();
211            if (localProfile.getAutoRegistration()) {
212                group.openToReceiveCalls();
213                if (mWifiEnabled) grabWifiLock();
214            }
215        } catch (SipException e) {
216            Log.e(TAG, "openToReceiveCalls()", e);
217            // TODO: how to send the exception back
218        }
219    }
220
221    private boolean isCallerCreator(SipSessionGroupExt group) {
222        SipProfile profile = group.getLocalProfile();
223        return (profile.getCallingUid() == Binder.getCallingUid());
224    }
225
226    private boolean isCallerCreatorOrRadio(SipSessionGroupExt group) {
227        return (isCallerRadio() || isCallerCreator(group));
228    }
229
230    private boolean isCallerRadio() {
231        return (Binder.getCallingUid() == Process.PHONE_UID);
232    }
233
234    public synchronized void close(String localProfileUri) {
235        mContext.enforceCallingOrSelfPermission(
236                android.Manifest.permission.USE_SIP, null);
237        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
238        if (group == null) return;
239        if (!isCallerCreatorOrRadio(group)) {
240            Log.w(TAG, "only creator or radio can close this profile");
241            return;
242        }
243
244        group = mSipGroups.remove(localProfileUri);
245        notifyProfileRemoved(group.getLocalProfile());
246        group.close();
247
248        if (!anyOpenedToReceiveCalls()) {
249            releaseWifiLock();
250            mMyWakeLock.reset(); // in case there's leak
251        }
252        if (mSipGroups.isEmpty()) unregisterReceivers();
253    }
254
255    public synchronized boolean isOpened(String localProfileUri) {
256        mContext.enforceCallingOrSelfPermission(
257                android.Manifest.permission.USE_SIP, null);
258        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
259        if (group == null) return false;
260        if (isCallerCreatorOrRadio(group)) {
261            return true;
262        } else {
263            Log.w(TAG, "only creator or radio can query on the profile");
264            return false;
265        }
266    }
267
268    public synchronized boolean isRegistered(String localProfileUri) {
269        mContext.enforceCallingOrSelfPermission(
270                android.Manifest.permission.USE_SIP, null);
271        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
272        if (group == null) return false;
273        if (isCallerCreatorOrRadio(group)) {
274            return group.isRegistered();
275        } else {
276            Log.w(TAG, "only creator or radio can query on the profile");
277            return false;
278        }
279    }
280
281    public synchronized void setRegistrationListener(String localProfileUri,
282            ISipSessionListener listener) {
283        mContext.enforceCallingOrSelfPermission(
284                android.Manifest.permission.USE_SIP, null);
285        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
286        if (group == null) return;
287        if (isCallerCreator(group)) {
288            group.setListener(listener);
289        } else {
290            Log.w(TAG, "only creator can set listener on the profile");
291        }
292    }
293
294    public synchronized ISipSession createSession(SipProfile localProfile,
295            ISipSessionListener listener) {
296        mContext.enforceCallingOrSelfPermission(
297                android.Manifest.permission.USE_SIP, null);
298        localProfile.setCallingUid(Binder.getCallingUid());
299        if (!mConnected) return null;
300        try {
301            SipSessionGroupExt group = createGroup(localProfile);
302            return group.createSession(listener);
303        } catch (SipException e) {
304            if (DEBUG) Log.d(TAG, "createSession()", e);
305            return null;
306        }
307    }
308
309    public synchronized ISipSession getPendingSession(String callId) {
310        mContext.enforceCallingOrSelfPermission(
311                android.Manifest.permission.USE_SIP, null);
312        if (callId == null) return null;
313        return mPendingSessions.get(callId);
314    }
315
316    private String determineLocalIp() {
317        try {
318            DatagramSocket s = new DatagramSocket();
319            s.connect(InetAddress.getByName("192.168.1.1"), 80);
320            return s.getLocalAddress().getHostAddress();
321        } catch (IOException e) {
322            if (DEBUG) Log.d(TAG, "determineLocalIp()", e);
323            // dont do anything; there should be a connectivity change going
324            return null;
325        }
326    }
327
328    private SipSessionGroupExt createGroup(SipProfile localProfile)
329            throws SipException {
330        String key = localProfile.getUriString();
331        SipSessionGroupExt group = mSipGroups.get(key);
332        if (group == null) {
333            group = new SipSessionGroupExt(localProfile, null, null);
334            mSipGroups.put(key, group);
335            notifyProfileAdded(localProfile);
336        } else if (!isCallerCreator(group)) {
337            throw new SipException("only creator can access the profile");
338        }
339        return group;
340    }
341
342    private SipSessionGroupExt createGroup(SipProfile localProfile,
343            PendingIntent incomingCallPendingIntent,
344            ISipSessionListener listener) throws SipException {
345        String key = localProfile.getUriString();
346        SipSessionGroupExt group = mSipGroups.get(key);
347        if (group != null) {
348            if (!isCallerCreator(group)) {
349                throw new SipException("only creator can access the profile");
350            }
351            group.setIncomingCallPendingIntent(incomingCallPendingIntent);
352            group.setListener(listener);
353        } else {
354            group = new SipSessionGroupExt(localProfile,
355                    incomingCallPendingIntent, listener);
356            mSipGroups.put(key, group);
357            notifyProfileAdded(localProfile);
358        }
359        return group;
360    }
361
362    private void notifyProfileAdded(SipProfile localProfile) {
363        if (DEBUG) Log.d(TAG, "notify: profile added: " + localProfile);
364        Intent intent = new Intent(SipManager.ACTION_SIP_ADD_PHONE);
365        intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
366        mContext.sendBroadcast(intent);
367    }
368
369    private void notifyProfileRemoved(SipProfile localProfile) {
370        if (DEBUG) Log.d(TAG, "notify: profile removed: " + localProfile);
371        Intent intent = new Intent(SipManager.ACTION_SIP_REMOVE_PHONE);
372        intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
373        mContext.sendBroadcast(intent);
374    }
375
376    private boolean anyOpenedToReceiveCalls() {
377        for (SipSessionGroupExt group : mSipGroups.values()) {
378            if (group.isOpenedToReceiveCalls()) return true;
379        }
380        return false;
381    }
382
383    private void grabWifiLock() {
384        if (mWifiLock == null) {
385            if (DEBUG) Log.d(TAG, "~~~~~~~~~~~~~~~~~~~~~ acquire wifi lock");
386            mWifiLock = ((WifiManager)
387                    mContext.getSystemService(Context.WIFI_SERVICE))
388                    .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
389            mWifiLock.acquire();
390            if (!mConnected) startWifiScanner();
391        }
392    }
393
394    private void releaseWifiLock() {
395        if (mWifiLock != null) {
396            if (DEBUG) Log.d(TAG, "~~~~~~~~~~~~~~~~~~~~~ release wifi lock");
397            mWifiLock.release();
398            mWifiLock = null;
399            stopWifiScanner();
400        }
401    }
402
403    private synchronized void startWifiScanner() {
404        if (mWifiScanProcess == null) {
405            mWifiScanProcess = new WifiScanProcess();
406        }
407        mWifiScanProcess.start();
408    }
409
410    private synchronized void stopWifiScanner() {
411        if (mWifiScanProcess != null) {
412            mWifiScanProcess.stop();
413        }
414    }
415
416    private synchronized void onConnectivityChanged(
417            String type, boolean connected) {
418        if (DEBUG) Log.d(TAG, "onConnectivityChanged(): "
419                + mNetworkType + (mConnected? " CONNECTED" : " DISCONNECTED")
420                + " --> " + type + (connected? " CONNECTED" : " DISCONNECTED"));
421
422        boolean sameType = type.equals(mNetworkType);
423        if (!sameType && !connected) return;
424
425        boolean wasWifi = "WIFI".equalsIgnoreCase(mNetworkType);
426        boolean isWifi = "WIFI".equalsIgnoreCase(type);
427        boolean wifiOff = (isWifi && !connected) || (wasWifi && !sameType);
428        boolean wifiOn = isWifi && connected;
429
430        try {
431            boolean wasConnected = mConnected;
432            mNetworkType = type;
433            mConnected = connected;
434
435            if (wasConnected) {
436                mLocalIp = null;
437                for (SipSessionGroupExt group : mSipGroups.values()) {
438                    group.onConnectivityChanged(false);
439                }
440            }
441
442            if (connected) {
443                mLocalIp = determineLocalIp();
444                for (SipSessionGroupExt group : mSipGroups.values()) {
445                    group.onConnectivityChanged(true);
446                }
447                if (isWifi && (mWifiLock != null)) stopWifiScanner();
448            } else {
449                mMyWakeLock.reset(); // in case there's a leak
450                if (isWifi && (mWifiLock != null)) startWifiScanner();
451            }
452        } catch (SipException e) {
453            Log.e(TAG, "onConnectivityChanged()", e);
454        }
455    }
456
457    private synchronized void addPendingSession(ISipSession session) {
458        try {
459            cleanUpPendingSessions();
460            mPendingSessions.put(session.getCallId(), session);
461            if (DEBUG) Log.d(TAG, "#pending sess=" + mPendingSessions.size());
462        } catch (RemoteException e) {
463            // should not happen with a local call
464            Log.e(TAG, "addPendingSession()", e);
465        }
466    }
467
468    private void cleanUpPendingSessions() throws RemoteException {
469        Map.Entry<String, ISipSession>[] entries =
470                mPendingSessions.entrySet().toArray(
471                new Map.Entry[mPendingSessions.size()]);
472        for (Map.Entry<String, ISipSession> entry : entries) {
473            if (entry.getValue().getState() != SipSession.State.INCOMING_CALL) {
474                mPendingSessions.remove(entry.getKey());
475            }
476        }
477    }
478
479    private synchronized boolean callingSelf(SipSessionGroupExt ringingGroup,
480            SipSessionGroup.SipSessionImpl ringingSession) {
481        String callId = ringingSession.getCallId();
482        for (SipSessionGroupExt group : mSipGroups.values()) {
483            if ((group != ringingGroup) && group.containsSession(callId)) {
484                if (DEBUG) Log.d(TAG, "call self: "
485                        + ringingSession.getLocalProfile().getUriString()
486                        + " -> " + group.getLocalProfile().getUriString());
487                return true;
488            }
489        }
490        return false;
491    }
492
493
494    private class SipSessionGroupExt extends SipSessionAdapter {
495        private SipSessionGroup mSipGroup;
496        private PendingIntent mIncomingCallPendingIntent;
497        private boolean mOpenedToReceiveCalls;
498
499        private AutoRegistrationProcess mAutoRegistration =
500                new AutoRegistrationProcess();
501
502        public SipSessionGroupExt(SipProfile localProfile,
503                PendingIntent incomingCallPendingIntent,
504                ISipSessionListener listener) throws SipException {
505            String password = localProfile.getPassword();
506            SipProfile p = duplicate(localProfile);
507            mSipGroup = createSipSessionGroup(mLocalIp, p, password);
508            mIncomingCallPendingIntent = incomingCallPendingIntent;
509            mAutoRegistration.setListener(listener);
510        }
511
512        public SipProfile getLocalProfile() {
513            return mSipGroup.getLocalProfile();
514        }
515
516        public boolean containsSession(String callId) {
517            return mSipGroup.containsSession(callId);
518        }
519
520        // network connectivity is tricky because network can be disconnected
521        // at any instant so need to deal with exceptions carefully even when
522        // you think you are connected
523        private SipSessionGroup createSipSessionGroup(String localIp,
524                SipProfile localProfile, String password) throws SipException {
525            try {
526                return new SipSessionGroup(localIp, localProfile, password,
527                        mMyWakeLock);
528            } catch (IOException e) {
529                // network disconnected
530                Log.w(TAG, "createSipSessionGroup(): network disconnected?");
531                if (localIp != null) {
532                    return createSipSessionGroup(null, localProfile, password);
533                } else {
534                    // recursive
535                    Log.wtf(TAG, "impossible! recursive!");
536                    throw new RuntimeException("createSipSessionGroup");
537                }
538            }
539        }
540
541        private SipProfile duplicate(SipProfile p) {
542            try {
543                return new SipProfile.Builder(p).setPassword("*").build();
544            } catch (Exception e) {
545                Log.wtf(TAG, "duplicate()", e);
546                throw new RuntimeException("duplicate profile", e);
547            }
548        }
549
550        public void setListener(ISipSessionListener listener) {
551            mAutoRegistration.setListener(listener);
552        }
553
554        public void setIncomingCallPendingIntent(PendingIntent pIntent) {
555            mIncomingCallPendingIntent = pIntent;
556        }
557
558        public void openToReceiveCalls() throws SipException {
559            mOpenedToReceiveCalls = true;
560            if (mConnected) {
561                mSipGroup.openToReceiveCalls(this);
562                mAutoRegistration.start(mSipGroup);
563            }
564            if (DEBUG) Log.d(TAG, "  openToReceiveCalls: " + getUri() + ": "
565                    + mIncomingCallPendingIntent);
566        }
567
568        public void onConnectivityChanged(boolean connected)
569                throws SipException {
570            mSipGroup.onConnectivityChanged();
571            if (connected) {
572                resetGroup(mLocalIp);
573                if (mOpenedToReceiveCalls) openToReceiveCalls();
574            } else {
575                // close mSipGroup but remember mOpenedToReceiveCalls
576                if (DEBUG) Log.d(TAG, "  close auto reg temporarily: "
577                        + getUri() + ": " + mIncomingCallPendingIntent);
578                mSipGroup.close();
579                mAutoRegistration.stop();
580            }
581        }
582
583        private void resetGroup(String localIp) throws SipException {
584            try {
585                mSipGroup.reset(localIp);
586            } catch (IOException e) {
587                // network disconnected
588                Log.w(TAG, "resetGroup(): network disconnected?");
589                if (localIp != null) {
590                    resetGroup(null); // reset w/o local IP
591                } else {
592                    // recursive
593                    Log.wtf(TAG, "impossible!");
594                    throw new RuntimeException("resetGroup");
595                }
596            }
597        }
598
599        public void close() {
600            mOpenedToReceiveCalls = false;
601            mSipGroup.close();
602            mAutoRegistration.stop();
603            if (DEBUG) Log.d(TAG, "   close: " + getUri() + ": "
604                    + mIncomingCallPendingIntent);
605        }
606
607        public ISipSession createSession(ISipSessionListener listener) {
608            return mSipGroup.createSession(listener);
609        }
610
611        @Override
612        public void onRinging(ISipSession s, SipProfile caller,
613                String sessionDescription) {
614            if (DEBUGV) Log.d(TAG, "<<<<< onRinging()");
615            SipSessionGroup.SipSessionImpl session =
616                    (SipSessionGroup.SipSessionImpl) s;
617            synchronized (SipService.this) {
618                try {
619                    if (!isRegistered() || callingSelf(this, session)) {
620                        session.endCall();
621                        return;
622                    }
623
624                    // send out incoming call broadcast
625                    addPendingSession(session);
626                    Intent intent = SipManager.createIncomingCallBroadcast(
627                            session.getCallId(), sessionDescription);
628                    if (DEBUG) Log.d(TAG, " ringing~~ " + getUri() + ": "
629                            + caller.getUri() + ": " + session.getCallId()
630                            + " " + mIncomingCallPendingIntent);
631                    mIncomingCallPendingIntent.send(mContext,
632                            SipManager.INCOMING_CALL_RESULT_CODE, intent);
633                } catch (PendingIntent.CanceledException e) {
634                    Log.w(TAG, "pendingIntent is canceled, drop incoming call");
635                    session.endCall();
636                }
637            }
638        }
639
640        @Override
641        public void onError(ISipSession session, int errorCode,
642                String message) {
643            if (DEBUG) Log.d(TAG, "sip session error: "
644                    + SipErrorCode.toString(errorCode) + ": " + message);
645        }
646
647        public boolean isOpenedToReceiveCalls() {
648            return mOpenedToReceiveCalls;
649        }
650
651        public boolean isRegistered() {
652            return mAutoRegistration.isRegistered();
653        }
654
655        private String getUri() {
656            return mSipGroup.getLocalProfileUri();
657        }
658    }
659
660    private class WifiScanProcess implements Runnable {
661        private static final String TAG = "\\WIFI_SCAN/";
662        private static final int INTERVAL = 60;
663        private boolean mRunning = false;
664
665        private WifiManager mWifiManager;
666
667        public void start() {
668            if (mRunning) return;
669            mRunning = true;
670            mTimer.set(INTERVAL * 1000, this);
671        }
672
673        WifiScanProcess() {
674            mWifiManager = (WifiManager)
675                    mContext.getSystemService(Context.WIFI_SERVICE);
676        }
677
678        public void run() {
679            // scan and associate now
680            if (DEBUGV) Log.v(TAG, "just wake up here for wifi scanning...");
681            mWifiManager.startScanActive();
682        }
683
684        public void stop() {
685            mRunning = false;
686            mTimer.cancel(this);
687        }
688    }
689
690    // KeepAliveProcess is controlled by AutoRegistrationProcess.
691    // All methods will be invoked in sync with SipService.this.
692    private class KeepAliveProcess implements Runnable {
693        private static final String TAG = "\\KEEPALIVE/";
694        private static final int INTERVAL = 10;
695        private SipSessionGroup.SipSessionImpl mSession;
696        private boolean mRunning = false;
697
698        public KeepAliveProcess(SipSessionGroup.SipSessionImpl session) {
699            mSession = session;
700        }
701
702        public void start() {
703            if (mRunning) return;
704            mRunning = true;
705            mTimer.set(INTERVAL * 1000, this);
706        }
707
708        // timeout handler
709        public void run() {
710            synchronized (SipService.this) {
711                if (!mRunning) return;
712
713                if (DEBUGV) Log.v(TAG, "~~~ keepalive: "
714                        + mSession.getLocalProfile().getUriString());
715                SipSessionGroup.SipSessionImpl session = mSession.duplicate();
716                try {
717                    session.sendKeepAlive();
718                    if (session.isReRegisterRequired()) {
719                        // Acquire wake lock for the registration process. The
720                        // lock will be released when registration is complete.
721                        mMyWakeLock.acquire(mSession);
722                        mSession.register(EXPIRY_TIME);
723                    }
724                } catch (Throwable t) {
725                    Log.w(TAG, "keepalive error: " + t);
726                }
727            }
728        }
729
730        public void stop() {
731            if (DEBUGV && (mSession != null)) Log.v(TAG, "stop keepalive:"
732                    + mSession.getLocalProfile().getUriString());
733            mRunning = false;
734            mSession = null;
735            mTimer.cancel(this);
736        }
737    }
738
739    private class AutoRegistrationProcess extends SipSessionAdapter
740            implements Runnable {
741        private SipSessionGroup.SipSessionImpl mSession;
742        private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
743        private KeepAliveProcess mKeepAliveProcess;
744        private int mBackoff = 1;
745        private boolean mRegistered;
746        private long mExpiryTime;
747        private int mErrorCode;
748        private String mErrorMessage;
749        private boolean mRunning = false;
750
751        private String getAction() {
752            return toString();
753        }
754
755        public void start(SipSessionGroup group) {
756            if (!mRunning) {
757                mRunning = true;
758                mBackoff = 1;
759                mSession = (SipSessionGroup.SipSessionImpl)
760                        group.createSession(this);
761                // return right away if no active network connection.
762                if (mSession == null) return;
763
764                // start unregistration to clear up old registration at server
765                // TODO: when rfc5626 is deployed, use reg-id and sip.instance
766                // in registration to avoid adding duplicate entries to server
767                mMyWakeLock.acquire(mSession);
768                mSession.unregister();
769                if (DEBUG) Log.d(TAG, "start AutoRegistrationProcess for "
770                        + mSession.getLocalProfile().getUriString());
771            }
772        }
773
774        public void stop() {
775            if (!mRunning) return;
776            mRunning = false;
777            mMyWakeLock.release(mSession);
778            if (mSession != null) {
779                mSession.setListener(null);
780                if (mConnected && mRegistered) mSession.unregister();
781            }
782
783            mTimer.cancel(this);
784            if (mKeepAliveProcess != null) {
785                mKeepAliveProcess.stop();
786                mKeepAliveProcess = null;
787            }
788
789            mRegistered = false;
790            setListener(mProxy.getListener());
791        }
792
793        public void setListener(ISipSessionListener listener) {
794            synchronized (SipService.this) {
795                mProxy.setListener(listener);
796
797                try {
798                    int state = (mSession == null)
799                            ? SipSession.State.READY_TO_CALL
800                            : mSession.getState();
801                    if ((state == SipSession.State.REGISTERING)
802                            || (state == SipSession.State.DEREGISTERING)) {
803                        mProxy.onRegistering(mSession);
804                    } else if (mRegistered) {
805                        int duration = (int)
806                                (mExpiryTime - SystemClock.elapsedRealtime());
807                        mProxy.onRegistrationDone(mSession, duration);
808                    } else if (mErrorCode != SipErrorCode.NO_ERROR) {
809                        if (mErrorCode == SipErrorCode.TIME_OUT) {
810                            mProxy.onRegistrationTimeout(mSession);
811                        } else {
812                            mProxy.onRegistrationFailed(mSession, mErrorCode,
813                                    mErrorMessage);
814                        }
815                    } else if (!mConnected) {
816                        mProxy.onRegistrationFailed(mSession,
817                                SipErrorCode.DATA_CONNECTION_LOST,
818                                "no data connection");
819                    } else if (!mRunning) {
820                        mProxy.onRegistrationFailed(mSession,
821                                SipErrorCode.CLIENT_ERROR,
822                                "registration not running");
823                    } else {
824                        mProxy.onRegistrationFailed(mSession,
825                                SipErrorCode.IN_PROGRESS,
826                                String.valueOf(state));
827                    }
828                } catch (Throwable t) {
829                    Log.w(TAG, "setListener(): " + t);
830                }
831            }
832        }
833
834        public boolean isRegistered() {
835            return mRegistered;
836        }
837
838        // timeout handler: re-register
839        public void run() {
840            synchronized (SipService.this) {
841                if (!mRunning) return;
842
843                mErrorCode = SipErrorCode.NO_ERROR;
844                mErrorMessage = null;
845                if (DEBUG) Log.d(TAG, "~~~ registering");
846                if (mConnected) {
847                    mMyWakeLock.acquire(mSession);
848                    mSession.register(EXPIRY_TIME);
849                }
850            }
851        }
852
853        private boolean isBehindNAT(String address) {
854            try {
855                byte[] d = InetAddress.getByName(address).getAddress();
856                if ((d[0] == 10) ||
857                        (((0x000000FF & ((int)d[0])) == 172) &&
858                        ((0x000000F0 & ((int)d[1])) == 16)) ||
859                        (((0x000000FF & ((int)d[0])) == 192) &&
860                        ((0x000000FF & ((int)d[1])) == 168))) {
861                    return true;
862                }
863            } catch (UnknownHostException e) {
864                Log.e(TAG, "isBehindAT()" + address, e);
865            }
866            return false;
867        }
868
869        private void restart(int duration) {
870            if (DEBUG) Log.d(TAG, "Refresh registration " + duration + "s later.");
871            mTimer.cancel(this);
872            mTimer.set(duration * 1000, this);
873        }
874
875        private int backoffDuration() {
876            int duration = SHORT_EXPIRY_TIME * mBackoff;
877            if (duration > 3600) {
878                duration = 3600;
879            } else {
880                mBackoff *= 2;
881            }
882            return duration;
883        }
884
885        @Override
886        public void onRegistering(ISipSession session) {
887            if (DEBUG) Log.d(TAG, "onRegistering(): " + session);
888            synchronized (SipService.this) {
889                if (notCurrentSession(session)) return;
890
891                mRegistered = false;
892                mProxy.onRegistering(session);
893            }
894        }
895
896        private boolean notCurrentSession(ISipSession session) {
897            if (session != mSession) {
898                ((SipSessionGroup.SipSessionImpl) session).setListener(null);
899                mMyWakeLock.release(session);
900                return true;
901            }
902            return !mRunning;
903        }
904
905        @Override
906        public void onRegistrationDone(ISipSession session, int duration) {
907            if (DEBUG) Log.d(TAG, "onRegistrationDone(): " + session);
908            synchronized (SipService.this) {
909                if (notCurrentSession(session)) return;
910
911                mProxy.onRegistrationDone(session, duration);
912
913                if (duration > 0) {
914                    mSession.clearReRegisterRequired();
915                    mExpiryTime = SystemClock.elapsedRealtime()
916                            + (duration * 1000);
917
918                    if (!mRegistered) {
919                        mRegistered = true;
920                        // allow some overlap to avoid call drop during renew
921                        duration -= MIN_EXPIRY_TIME;
922                        if (duration < MIN_EXPIRY_TIME) {
923                            duration = MIN_EXPIRY_TIME;
924                        }
925                        restart(duration);
926
927                        if (isBehindNAT(mLocalIp) ||
928                                mSession.getLocalProfile().getSendKeepAlive()) {
929                            if (mKeepAliveProcess == null) {
930                                mKeepAliveProcess =
931                                        new KeepAliveProcess(mSession);
932                            }
933                            mKeepAliveProcess.start();
934                        }
935                    }
936                    mMyWakeLock.release(session);
937                } else {
938                    mRegistered = false;
939                    mExpiryTime = -1L;
940                    if (DEBUG) Log.d(TAG, "Refresh registration immediately");
941                    run();
942                }
943            }
944        }
945
946        @Override
947        public void onRegistrationFailed(ISipSession session, int errorCode,
948                String message) {
949            if (DEBUG) Log.d(TAG, "onRegistrationFailed(): " + session + ": "
950                    + SipErrorCode.toString(errorCode) + ": " + message);
951            synchronized (SipService.this) {
952                if (notCurrentSession(session)) return;
953
954                switch (errorCode) {
955                    case SipErrorCode.INVALID_CREDENTIALS:
956                    case SipErrorCode.SERVER_UNREACHABLE:
957                        if (DEBUG) Log.d(TAG, "   pause auto-registration");
958                        stop();
959                        break;
960                    default:
961                        restartLater();
962                }
963
964                mErrorCode = errorCode;
965                mErrorMessage = message;
966                mProxy.onRegistrationFailed(session, errorCode, message);
967                mMyWakeLock.release(session);
968            }
969        }
970
971        @Override
972        public void onRegistrationTimeout(ISipSession session) {
973            if (DEBUG) Log.d(TAG, "onRegistrationTimeout(): " + session);
974            synchronized (SipService.this) {
975                if (notCurrentSession(session)) return;
976
977                mErrorCode = SipErrorCode.TIME_OUT;
978                mProxy.onRegistrationTimeout(session);
979                restartLater();
980                mMyWakeLock.release(session);
981            }
982        }
983
984        private void restartLater() {
985            mRegistered = false;
986            restart(backoffDuration());
987            if (mKeepAliveProcess != null) {
988                mKeepAliveProcess.stop();
989                mKeepAliveProcess = null;
990            }
991        }
992    }
993
994    private class ConnectivityReceiver extends BroadcastReceiver {
995        private Timer mTimer = new Timer();
996        private MyTimerTask mTask;
997
998        @Override
999        public void onReceive(final Context context, final Intent intent) {
1000            // Run the handler in MyExecutor to be protected by wake lock
1001            getExecutor().execute(new Runnable() {
1002                public void run() {
1003                    onReceiveInternal(context, intent);
1004                }
1005            });
1006        }
1007
1008        private void onReceiveInternal(Context context, Intent intent) {
1009            String action = intent.getAction();
1010            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
1011                Bundle b = intent.getExtras();
1012                if (b != null) {
1013                    NetworkInfo netInfo = (NetworkInfo)
1014                            b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
1015                    String type = netInfo.getTypeName();
1016                    NetworkInfo.State state = netInfo.getState();
1017
1018                    if (mWifiOnly && (netInfo.getType() !=
1019                            ConnectivityManager.TYPE_WIFI)) {
1020                        if (DEBUG) {
1021                            Log.d(TAG, "Wifi only, other connectivity ignored: "
1022                                    + type);
1023                        }
1024                        return;
1025                    }
1026
1027                    NetworkInfo activeNetInfo = getActiveNetworkInfo();
1028                    if (DEBUG) {
1029                        if (activeNetInfo != null) {
1030                            Log.d(TAG, "active network: "
1031                                    + activeNetInfo.getTypeName()
1032                                    + ((activeNetInfo.getState() == NetworkInfo.State.CONNECTED)
1033                                            ? " CONNECTED" : " DISCONNECTED"));
1034                        } else {
1035                            Log.d(TAG, "active network: null");
1036                        }
1037                    }
1038                    if ((state == NetworkInfo.State.CONNECTED)
1039                            && (activeNetInfo != null)
1040                            && (activeNetInfo.getType() != netInfo.getType())) {
1041                        if (DEBUG) Log.d(TAG, "ignore connect event: " + type
1042                                + ", active: " + activeNetInfo.getTypeName());
1043                        return;
1044                    }
1045
1046                    if (state == NetworkInfo.State.CONNECTED) {
1047                        if (DEBUG) Log.d(TAG, "Connectivity alert: CONNECTED " + type);
1048                        onChanged(type, true);
1049                    } else if (state == NetworkInfo.State.DISCONNECTED) {
1050                        if (DEBUG) Log.d(TAG, "Connectivity alert: DISCONNECTED " + type);
1051                        onChanged(type, false);
1052                    } else {
1053                        if (DEBUG) Log.d(TAG, "Connectivity alert not processed: "
1054                                + state + " " + type);
1055                    }
1056                }
1057            }
1058        }
1059
1060        private NetworkInfo getActiveNetworkInfo() {
1061            ConnectivityManager cm = (ConnectivityManager)
1062                    mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
1063            return cm.getActiveNetworkInfo();
1064        }
1065
1066        private void onChanged(String type, boolean connected) {
1067            synchronized (SipService.this) {
1068                // When turning on WIFI, it needs some time for network
1069                // connectivity to get stabile so we defer good news (because
1070                // we want to skip the interim ones) but deliver bad news
1071                // immediately
1072                if (connected) {
1073                    if (mTask != null) {
1074                        mTask.cancel();
1075                        mMyWakeLock.release(mTask);
1076                    }
1077                    mTask = new MyTimerTask(type, connected);
1078                    mTimer.schedule(mTask, 2 * 1000L);
1079                    // hold wakup lock so that we can finish changes before the
1080                    // device goes to sleep
1081                    mMyWakeLock.acquire(mTask);
1082                } else {
1083                    if ((mTask != null) && mTask.mNetworkType.equals(type)) {
1084                        mTask.cancel();
1085                        mMyWakeLock.release(mTask);
1086                    }
1087                    onConnectivityChanged(type, false);
1088                }
1089            }
1090        }
1091
1092        private class MyTimerTask extends TimerTask {
1093            private boolean mConnected;
1094            private String mNetworkType;
1095
1096            public MyTimerTask(String type, boolean connected) {
1097                mNetworkType = type;
1098                mConnected = connected;
1099            }
1100
1101            // timeout handler
1102            @Override
1103            public void run() {
1104                // delegate to mExecutor
1105                getExecutor().execute(new Runnable() {
1106                    public void run() {
1107                        realRun();
1108                    }
1109                });
1110            }
1111
1112            private void realRun() {
1113                synchronized (SipService.this) {
1114                    if (mTask != this) {
1115                        Log.w(TAG, "  unexpected task: " + mNetworkType
1116                                + (mConnected ? " CONNECTED" : "DISCONNECTED"));
1117                        mMyWakeLock.release(this);
1118                        return;
1119                    }
1120                    mTask = null;
1121                    if (DEBUG) Log.d(TAG, " deliver change for " + mNetworkType
1122                            + (mConnected ? " CONNECTED" : "DISCONNECTED"));
1123                    onConnectivityChanged(mNetworkType, mConnected);
1124                    mMyWakeLock.release(this);
1125                }
1126            }
1127        }
1128    }
1129
1130    /**
1131     * Timer that can schedule events to occur even when the device is in sleep.
1132     * Only used internally in this package.
1133     */
1134    class WakeupTimer extends BroadcastReceiver {
1135        private static final String TAG = "_SIP.WkTimer_";
1136        private static final String TRIGGER_TIME = "TriggerTime";
1137
1138        private Context mContext;
1139        private AlarmManager mAlarmManager;
1140
1141        // runnable --> time to execute in SystemClock
1142        private TreeSet<MyEvent> mEventQueue =
1143                new TreeSet<MyEvent>(new MyEventComparator());
1144
1145        private PendingIntent mPendingIntent;
1146
1147        public WakeupTimer(Context context) {
1148            mContext = context;
1149            mAlarmManager = (AlarmManager)
1150                    context.getSystemService(Context.ALARM_SERVICE);
1151
1152            IntentFilter filter = new IntentFilter(getAction());
1153            context.registerReceiver(this, filter);
1154        }
1155
1156        /**
1157         * Stops the timer. No event can be scheduled after this method is called.
1158         */
1159        public synchronized void stop() {
1160            mContext.unregisterReceiver(this);
1161            if (mPendingIntent != null) {
1162                mAlarmManager.cancel(mPendingIntent);
1163                mPendingIntent = null;
1164            }
1165            mEventQueue.clear();
1166            mEventQueue = null;
1167        }
1168
1169        private synchronized boolean stopped() {
1170            if (mEventQueue == null) {
1171                Log.w(TAG, "Timer stopped");
1172                return true;
1173            } else {
1174                return false;
1175            }
1176        }
1177
1178        private void cancelAlarm() {
1179            mAlarmManager.cancel(mPendingIntent);
1180            mPendingIntent = null;
1181        }
1182
1183        private void recalculatePeriods() {
1184            if (mEventQueue.isEmpty()) return;
1185
1186            MyEvent firstEvent = mEventQueue.first();
1187            int minPeriod = firstEvent.mMaxPeriod;
1188            long minTriggerTime = firstEvent.mTriggerTime;
1189            for (MyEvent e : mEventQueue) {
1190                e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
1191                int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
1192                        - minTriggerTime);
1193                interval = interval / minPeriod * minPeriod;
1194                e.mTriggerTime = minTriggerTime + interval;
1195            }
1196            TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
1197                    mEventQueue.comparator());
1198            newQueue.addAll((Collection<MyEvent>) mEventQueue);
1199            mEventQueue.clear();
1200            mEventQueue = newQueue;
1201            if (DEBUG_TIMER) {
1202                Log.d(TAG, "queue re-calculated");
1203                printQueue();
1204            }
1205        }
1206
1207        // Determines the period and the trigger time of the new event and insert it
1208        // to the queue.
1209        private void insertEvent(MyEvent event) {
1210            long now = SystemClock.elapsedRealtime();
1211            if (mEventQueue.isEmpty()) {
1212                event.mTriggerTime = now + event.mPeriod;
1213                mEventQueue.add(event);
1214                return;
1215            }
1216            MyEvent firstEvent = mEventQueue.first();
1217            int minPeriod = firstEvent.mPeriod;
1218            if (minPeriod <= event.mMaxPeriod) {
1219                event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
1220                int interval = event.mMaxPeriod;
1221                interval -= (int) (firstEvent.mTriggerTime - now);
1222                interval = interval / minPeriod * minPeriod;
1223                event.mTriggerTime = firstEvent.mTriggerTime + interval;
1224                mEventQueue.add(event);
1225            } else {
1226                long triggerTime = now + event.mPeriod;
1227                if (firstEvent.mTriggerTime < triggerTime) {
1228                    event.mTriggerTime = firstEvent.mTriggerTime;
1229                    event.mLastTriggerTime -= event.mPeriod;
1230                } else {
1231                    event.mTriggerTime = triggerTime;
1232                }
1233                mEventQueue.add(event);
1234                recalculatePeriods();
1235            }
1236        }
1237
1238        /**
1239         * Sets a periodic timer.
1240         *
1241         * @param period the timer period; in milli-second
1242         * @param callback is called back when the timer goes off; the same callback
1243         *      can be specified in multiple timer events
1244         */
1245        public synchronized void set(int period, Runnable callback) {
1246            if (stopped()) return;
1247
1248            long now = SystemClock.elapsedRealtime();
1249            MyEvent event = new MyEvent(period, callback, now);
1250            insertEvent(event);
1251
1252            if (mEventQueue.first() == event) {
1253                if (mEventQueue.size() > 1) cancelAlarm();
1254                scheduleNext();
1255            }
1256
1257            long triggerTime = event.mTriggerTime;
1258            if (DEBUG_TIMER) {
1259                Log.d(TAG, " add event " + event + " scheduled at "
1260                        + showTime(triggerTime) + " at " + showTime(now)
1261                        + ", #events=" + mEventQueue.size());
1262                printQueue();
1263            }
1264        }
1265
1266        /**
1267         * Cancels all the timer events with the specified callback.
1268         *
1269         * @param callback the callback
1270         */
1271        public synchronized void cancel(Runnable callback) {
1272            if (stopped() || mEventQueue.isEmpty()) return;
1273            if (DEBUG_TIMER) Log.d(TAG, "cancel:" + callback);
1274
1275            MyEvent firstEvent = mEventQueue.first();
1276            for (Iterator<MyEvent> iter = mEventQueue.iterator();
1277                    iter.hasNext();) {
1278                MyEvent event = iter.next();
1279                if (event.mCallback == callback) {
1280                    iter.remove();
1281                    if (DEBUG_TIMER) Log.d(TAG, "    cancel found:" + event);
1282                }
1283            }
1284            if (mEventQueue.isEmpty()) {
1285                cancelAlarm();
1286            } else if (mEventQueue.first() != firstEvent) {
1287                cancelAlarm();
1288                firstEvent = mEventQueue.first();
1289                firstEvent.mPeriod = firstEvent.mMaxPeriod;
1290                firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
1291                        + firstEvent.mPeriod;
1292                recalculatePeriods();
1293                scheduleNext();
1294            }
1295            if (DEBUG_TIMER) {
1296                Log.d(TAG, "after cancel:");
1297                printQueue();
1298            }
1299        }
1300
1301        private void scheduleNext() {
1302            if (stopped() || mEventQueue.isEmpty()) return;
1303
1304            if (mPendingIntent != null) {
1305                throw new RuntimeException("pendingIntent is not null!");
1306            }
1307
1308            MyEvent event = mEventQueue.first();
1309            Intent intent = new Intent(getAction());
1310            intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
1311            PendingIntent pendingIntent = mPendingIntent =
1312                    PendingIntent.getBroadcast(mContext, 0, intent,
1313                            PendingIntent.FLAG_UPDATE_CURRENT);
1314            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1315                    event.mTriggerTime, pendingIntent);
1316        }
1317
1318        @Override
1319        public void onReceive(Context context, Intent intent) {
1320            // This callback is already protected by AlarmManager's wake lock.
1321            String action = intent.getAction();
1322            if (getAction().equals(action)
1323                    && intent.getExtras().containsKey(TRIGGER_TIME)) {
1324                mPendingIntent = null;
1325                long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
1326                execute(triggerTime);
1327            } else {
1328                Log.d(TAG, "unrecognized intent: " + intent);
1329            }
1330        }
1331
1332        private void printQueue() {
1333            int count = 0;
1334            for (MyEvent event : mEventQueue) {
1335                Log.d(TAG, "     " + event + ": scheduled at "
1336                        + showTime(event.mTriggerTime) + ": last at "
1337                        + showTime(event.mLastTriggerTime));
1338                if (++count >= 5) break;
1339            }
1340            if (mEventQueue.size() > count) {
1341                Log.d(TAG, "     .....");
1342            } else if (count == 0) {
1343                Log.d(TAG, "     <empty>");
1344            }
1345        }
1346
1347        private synchronized void execute(long triggerTime) {
1348            if (DEBUG_TIMER) Log.d(TAG, "time's up, triggerTime = "
1349                    + showTime(triggerTime) + ": " + mEventQueue.size());
1350            if (stopped() || mEventQueue.isEmpty()) return;
1351
1352            for (MyEvent event : mEventQueue) {
1353                if (event.mTriggerTime != triggerTime) break;
1354                if (DEBUG_TIMER) Log.d(TAG, "execute " + event);
1355
1356                event.mLastTriggerTime = event.mTriggerTime;
1357                event.mTriggerTime += event.mPeriod;
1358
1359                // run the callback in the handler thread to prevent deadlock
1360                getExecutor().execute(event.mCallback);
1361            }
1362            if (DEBUG_TIMER) {
1363                Log.d(TAG, "after timeout execution");
1364                printQueue();
1365            }
1366            scheduleNext();
1367        }
1368
1369        private String getAction() {
1370            return toString();
1371        }
1372
1373        private String showTime(long time) {
1374            int ms = (int) (time % 1000);
1375            int s = (int) (time / 1000);
1376            int m = s / 60;
1377            s %= 60;
1378            return String.format("%d.%d.%d", m, s, ms);
1379        }
1380    }
1381
1382    private static class MyEvent {
1383        int mPeriod;
1384        int mMaxPeriod;
1385        long mTriggerTime;
1386        long mLastTriggerTime;
1387        Runnable mCallback;
1388
1389        MyEvent(int period, Runnable callback, long now) {
1390            mPeriod = mMaxPeriod = period;
1391            mCallback = callback;
1392            mLastTriggerTime = now;
1393        }
1394
1395        @Override
1396        public String toString() {
1397            String s = super.toString();
1398            s = s.substring(s.indexOf("@"));
1399            return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
1400                    + toString(mCallback);
1401        }
1402
1403        private String toString(Object o) {
1404            String s = o.toString();
1405            int index = s.indexOf("$");
1406            if (index > 0) s = s.substring(index + 1);
1407            return s;
1408        }
1409    }
1410
1411    private static class MyEventComparator implements Comparator<MyEvent> {
1412        public int compare(MyEvent e1, MyEvent e2) {
1413            if (e1 == e2) return 0;
1414            int diff = e1.mMaxPeriod - e2.mMaxPeriod;
1415            if (diff == 0) diff = -1;
1416            return diff;
1417        }
1418
1419        public boolean equals(Object that) {
1420            return (this == that);
1421        }
1422    }
1423
1424    private static Looper createLooper() {
1425        HandlerThread thread = new HandlerThread("SipService.Executor");
1426        thread.start();
1427        return thread.getLooper();
1428    }
1429
1430    // Executes immediate tasks in a single thread.
1431    // Hold/release wake lock for running tasks
1432    private class MyExecutor extends Handler {
1433        MyExecutor() {
1434            super(createLooper());
1435        }
1436
1437        void execute(Runnable task) {
1438            mMyWakeLock.acquire(task);
1439            Message.obtain(this, 0/* don't care */, task).sendToTarget();
1440        }
1441
1442        @Override
1443        public void handleMessage(Message msg) {
1444            if (msg.obj instanceof Runnable) {
1445                executeInternal((Runnable) msg.obj);
1446            } else {
1447                Log.w(TAG, "can't handle msg: " + msg);
1448            }
1449        }
1450
1451        private void executeInternal(Runnable task) {
1452            try {
1453                task.run();
1454            } catch (Throwable t) {
1455                Log.e(TAG, "run task: " + task, t);
1456            } finally {
1457                mMyWakeLock.release(task);
1458            }
1459        }
1460    }
1461}
1462