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