Home | History | Annotate | Download | only in sip
      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 
     17 package com.android.server.sip;
     18 
     19 import android.app.AppOpsManager;
     20 import android.app.PendingIntent;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.net.ConnectivityManager;
     26 import android.net.NetworkInfo;
     27 import android.net.sip.ISipService;
     28 import android.net.sip.ISipSession;
     29 import android.net.sip.ISipSessionListener;
     30 import android.net.sip.SipErrorCode;
     31 import android.net.sip.SipManager;
     32 import android.net.sip.SipProfile;
     33 import android.net.sip.SipSession;
     34 import android.net.sip.SipSessionAdapter;
     35 import android.net.wifi.WifiManager;
     36 import android.os.Binder;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.os.HandlerThread;
     40 import android.os.Looper;
     41 import android.os.Message;
     42 import android.os.PowerManager;
     43 import android.os.Process;
     44 import android.os.RemoteException;
     45 import android.os.ServiceManager;
     46 import android.os.SystemClock;
     47 import android.telephony.Rlog;
     48 
     49 import java.io.IOException;
     50 import java.net.DatagramSocket;
     51 import java.net.InetAddress;
     52 import java.net.UnknownHostException;
     53 import java.util.ArrayList;
     54 import java.util.HashMap;
     55 import java.util.Map;
     56 import java.util.concurrent.Executor;
     57 
     58 import javax.sip.SipException;
     59 
     60 /**
     61  * @hide
     62  */
     63 public final class SipService extends ISipService.Stub {
     64     static final String TAG = "SipService";
     65     static final boolean DBG = true;
     66     private static final int EXPIRY_TIME = 3600;
     67     private static final int SHORT_EXPIRY_TIME = 10;
     68     private static final int MIN_EXPIRY_TIME = 60;
     69     private static final int DEFAULT_KEEPALIVE_INTERVAL = 10; // in seconds
     70     private static final int DEFAULT_MAX_KEEPALIVE_INTERVAL = 120; // in seconds
     71 
     72     private Context mContext;
     73     private String mLocalIp;
     74     private int mNetworkType = -1;
     75     private SipWakeupTimer mTimer;
     76     private WifiManager.WifiLock mWifiLock;
     77     private boolean mSipOnWifiOnly;
     78 
     79     private final AppOpsManager mAppOps;
     80 
     81     private SipKeepAliveProcessCallback mSipKeepAliveProcessCallback;
     82 
     83     private MyExecutor mExecutor = new MyExecutor();
     84 
     85     // SipProfile URI --> group
     86     private Map<String, SipSessionGroupExt> mSipGroups =
     87             new HashMap<String, SipSessionGroupExt>();
     88 
     89     // session ID --> session
     90     private Map<String, ISipSession> mPendingSessions =
     91             new HashMap<String, ISipSession>();
     92 
     93     private ConnectivityReceiver mConnectivityReceiver;
     94     private SipWakeLock mMyWakeLock;
     95     private int mKeepAliveInterval;
     96     private int mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
     97 
     98     /**
     99      * Starts the SIP service. Do nothing if the SIP API is not supported on the
    100      * device.
    101      */
    102     public static void start(Context context) {
    103         if (SipManager.isApiSupported(context)) {
    104             if (ServiceManager.getService("sip") == null) {
    105                 ServiceManager.addService("sip", new SipService(context));
    106                 context.sendBroadcast(new Intent(SipManager.ACTION_SIP_SERVICE_UP));
    107                 if (DBG) slog("start:");
    108             }
    109         }
    110     }
    111 
    112     private SipService(Context context) {
    113         if (DBG) log("SipService: started!");
    114         mContext = context;
    115         mConnectivityReceiver = new ConnectivityReceiver();
    116 
    117         mWifiLock = ((WifiManager)
    118                 context.getSystemService(Context.WIFI_SERVICE))
    119                 .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
    120         mWifiLock.setReferenceCounted(false);
    121         mSipOnWifiOnly = SipManager.isSipWifiOnly(context);
    122 
    123         mMyWakeLock = new SipWakeLock((PowerManager)
    124                 context.getSystemService(Context.POWER_SERVICE));
    125 
    126         mTimer = new SipWakeupTimer(context, mExecutor);
    127         mAppOps = mContext.getSystemService(AppOpsManager.class);
    128     }
    129 
    130     @Override
    131     public synchronized SipProfile[] getListOfProfiles(String opPackageName) {
    132         if (!canUseSip(opPackageName, "getListOfProfiles")) {
    133             return new SipProfile[0];
    134         }
    135         boolean isCallerRadio = isCallerRadio();
    136         ArrayList<SipProfile> profiles = new ArrayList<SipProfile>();
    137         for (SipSessionGroupExt group : mSipGroups.values()) {
    138             if (isCallerRadio || isCallerCreator(group)) {
    139                 profiles.add(group.getLocalProfile());
    140             }
    141         }
    142         return profiles.toArray(new SipProfile[profiles.size()]);
    143     }
    144 
    145     @Override
    146     public synchronized void open(SipProfile localProfile, String opPackageName) {
    147         if (!canUseSip(opPackageName, "open")) {
    148             return;
    149         }
    150         localProfile.setCallingUid(Binder.getCallingUid());
    151         try {
    152             createGroup(localProfile);
    153         } catch (SipException e) {
    154             loge("openToMakeCalls()", e);
    155             // TODO: how to send the exception back
    156         }
    157     }
    158 
    159     @Override
    160     public synchronized void open3(SipProfile localProfile,
    161             PendingIntent incomingCallPendingIntent,
    162             ISipSessionListener listener,
    163             String opPackageName) {
    164         if (!canUseSip(opPackageName, "open3")) {
    165             return;
    166         }
    167         localProfile.setCallingUid(Binder.getCallingUid());
    168         if (incomingCallPendingIntent == null) {
    169             if (DBG) log("open3: incomingCallPendingIntent cannot be null; "
    170                     + "the profile is not opened");
    171             return;
    172         }
    173         if (DBG) log("open3: " + obfuscateSipUri(localProfile.getUriString()) + ": "
    174                 + incomingCallPendingIntent + ": " + listener);
    175         try {
    176             SipSessionGroupExt group = createGroup(localProfile,
    177                     incomingCallPendingIntent, listener);
    178             if (localProfile.getAutoRegistration()) {
    179                 group.openToReceiveCalls();
    180                 updateWakeLocks();
    181             }
    182         } catch (SipException e) {
    183             loge("open3:", e);
    184             // TODO: how to send the exception back
    185         }
    186     }
    187 
    188     private boolean isCallerCreator(SipSessionGroupExt group) {
    189         SipProfile profile = group.getLocalProfile();
    190         return (profile.getCallingUid() == Binder.getCallingUid());
    191     }
    192 
    193     private boolean isCallerCreatorOrRadio(SipSessionGroupExt group) {
    194         return (isCallerRadio() || isCallerCreator(group));
    195     }
    196 
    197     private boolean isCallerRadio() {
    198         return (Binder.getCallingUid() == Process.PHONE_UID);
    199     }
    200 
    201     @Override
    202     public synchronized void close(String localProfileUri, String opPackageName) {
    203         if (!canUseSip(opPackageName, "close")) {
    204             return;
    205         }
    206         SipSessionGroupExt group = mSipGroups.get(localProfileUri);
    207         if (group == null) return;
    208         if (!isCallerCreatorOrRadio(group)) {
    209             if (DBG) log("only creator or radio can close this profile");
    210             return;
    211         }
    212 
    213         group = mSipGroups.remove(localProfileUri);
    214         notifyProfileRemoved(group.getLocalProfile());
    215         group.close();
    216 
    217         updateWakeLocks();
    218     }
    219 
    220     @Override
    221     public synchronized boolean isOpened(String localProfileUri, String opPackageName) {
    222         if (!canUseSip(opPackageName, "isOpened")) {
    223             return false;
    224         }
    225         SipSessionGroupExt group = mSipGroups.get(localProfileUri);
    226         if (group == null) return false;
    227         if (isCallerCreatorOrRadio(group)) {
    228             return true;
    229         } else {
    230             if (DBG) log("only creator or radio can query on the profile");
    231             return false;
    232         }
    233     }
    234 
    235     @Override
    236     public synchronized boolean isRegistered(String localProfileUri, String opPackageName) {
    237         if (!canUseSip(opPackageName, "isRegistered")) {
    238             return false;
    239         }
    240         SipSessionGroupExt group = mSipGroups.get(localProfileUri);
    241         if (group == null) return false;
    242         if (isCallerCreatorOrRadio(group)) {
    243             return group.isRegistered();
    244         } else {
    245             if (DBG) log("only creator or radio can query on the profile");
    246             return false;
    247         }
    248     }
    249 
    250     @Override
    251     public synchronized void setRegistrationListener(String localProfileUri,
    252             ISipSessionListener listener, String opPackageName) {
    253         if (!canUseSip(opPackageName, "setRegistrationListener")) {
    254             return;
    255         }
    256         SipSessionGroupExt group = mSipGroups.get(localProfileUri);
    257         if (group == null) return;
    258         if (isCallerCreator(group)) {
    259             group.setListener(listener);
    260         } else {
    261             if (DBG) log("only creator can set listener on the profile");
    262         }
    263     }
    264 
    265     @Override
    266     public synchronized ISipSession createSession(SipProfile localProfile,
    267             ISipSessionListener listener, String opPackageName) {
    268         if (DBG) log("createSession: profile" + localProfile);
    269         if (!canUseSip(opPackageName, "createSession")) {
    270             return null;
    271         }
    272         localProfile.setCallingUid(Binder.getCallingUid());
    273         if (mNetworkType == -1) {
    274             if (DBG) log("createSession: mNetworkType==-1 ret=null");
    275             return null;
    276         }
    277         try {
    278             SipSessionGroupExt group = createGroup(localProfile);
    279             return group.createSession(listener);
    280         } catch (SipException e) {
    281             if (DBG) loge("createSession;", e);
    282             return null;
    283         }
    284     }
    285 
    286     @Override
    287     public synchronized ISipSession getPendingSession(String callId, String opPackageName) {
    288         if (!canUseSip(opPackageName, "getPendingSession")) {
    289             return null;
    290         }
    291         if (callId == null) return null;
    292         return mPendingSessions.get(callId);
    293     }
    294 
    295     private String determineLocalIp() {
    296         try {
    297             DatagramSocket s = new DatagramSocket();
    298             s.connect(InetAddress.getByName("192.168.1.1"), 80);
    299             return s.getLocalAddress().getHostAddress();
    300         } catch (IOException e) {
    301             if (DBG) loge("determineLocalIp()", e);
    302             // dont do anything; there should be a connectivity change going
    303             return null;
    304         }
    305     }
    306 
    307     private SipSessionGroupExt createGroup(SipProfile localProfile)
    308             throws SipException {
    309         String key = localProfile.getUriString();
    310         SipSessionGroupExt group = mSipGroups.get(key);
    311         if (group == null) {
    312             group = new SipSessionGroupExt(localProfile, null, null);
    313             mSipGroups.put(key, group);
    314             notifyProfileAdded(localProfile);
    315         } else if (!isCallerCreator(group)) {
    316             throw new SipException("only creator can access the profile");
    317         }
    318         return group;
    319     }
    320 
    321     private SipSessionGroupExt createGroup(SipProfile localProfile,
    322             PendingIntent incomingCallPendingIntent,
    323             ISipSessionListener listener) throws SipException {
    324         String key = localProfile.getUriString();
    325         SipSessionGroupExt group = mSipGroups.get(key);
    326         if (group != null) {
    327             if (!isCallerCreator(group)) {
    328                 throw new SipException("only creator can access the profile");
    329             }
    330             group.setIncomingCallPendingIntent(incomingCallPendingIntent);
    331             group.setListener(listener);
    332         } else {
    333             group = new SipSessionGroupExt(localProfile,
    334                     incomingCallPendingIntent, listener);
    335             mSipGroups.put(key, group);
    336             notifyProfileAdded(localProfile);
    337         }
    338         return group;
    339     }
    340 
    341     private void notifyProfileAdded(SipProfile localProfile) {
    342         if (DBG) log("notify: profile added: " + localProfile);
    343         Intent intent = new Intent(SipManager.ACTION_SIP_ADD_PHONE);
    344         intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
    345         mContext.sendBroadcast(intent);
    346         if (mSipGroups.size() == 1) {
    347             registerReceivers();
    348         }
    349     }
    350 
    351     private void notifyProfileRemoved(SipProfile localProfile) {
    352         if (DBG) log("notify: profile removed: " + localProfile);
    353         Intent intent = new Intent(SipManager.ACTION_SIP_REMOVE_PHONE);
    354         intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
    355         mContext.sendBroadcast(intent);
    356         if (mSipGroups.size() == 0) {
    357             unregisterReceivers();
    358         }
    359     }
    360 
    361     private void stopPortMappingMeasurement() {
    362         if (mSipKeepAliveProcessCallback != null) {
    363             mSipKeepAliveProcessCallback.stop();
    364             mSipKeepAliveProcessCallback = null;
    365         }
    366     }
    367 
    368     private void startPortMappingLifetimeMeasurement(
    369             SipProfile localProfile) {
    370         startPortMappingLifetimeMeasurement(localProfile,
    371                 DEFAULT_MAX_KEEPALIVE_INTERVAL);
    372     }
    373 
    374     private void startPortMappingLifetimeMeasurement(
    375             SipProfile localProfile, int maxInterval) {
    376         if ((mSipKeepAliveProcessCallback == null)
    377                 && (mKeepAliveInterval == -1)
    378                 && isBehindNAT(mLocalIp)) {
    379             if (DBG) log("startPortMappingLifetimeMeasurement: profile="
    380                     + localProfile.getUriString());
    381 
    382             int minInterval = mLastGoodKeepAliveInterval;
    383             if (minInterval >= maxInterval) {
    384                 // If mLastGoodKeepAliveInterval also does not work, reset it
    385                 // to the default min
    386                 minInterval = mLastGoodKeepAliveInterval
    387                         = DEFAULT_KEEPALIVE_INTERVAL;
    388                 log("  reset min interval to " + minInterval);
    389             }
    390             mSipKeepAliveProcessCallback = new SipKeepAliveProcessCallback(
    391                     localProfile, minInterval, maxInterval);
    392             mSipKeepAliveProcessCallback.start();
    393         }
    394     }
    395 
    396     private void restartPortMappingLifetimeMeasurement(
    397             SipProfile localProfile, int maxInterval) {
    398         stopPortMappingMeasurement();
    399         mKeepAliveInterval = -1;
    400         startPortMappingLifetimeMeasurement(localProfile, maxInterval);
    401     }
    402 
    403     private synchronized void addPendingSession(ISipSession session) {
    404         try {
    405             cleanUpPendingSessions();
    406             mPendingSessions.put(session.getCallId(), session);
    407             if (DBG) log("#pending sess=" + mPendingSessions.size());
    408         } catch (RemoteException e) {
    409             // should not happen with a local call
    410             loge("addPendingSession()", e);
    411         }
    412     }
    413 
    414     private void cleanUpPendingSessions() throws RemoteException {
    415         Map.Entry<String, ISipSession>[] entries =
    416                 mPendingSessions.entrySet().toArray(
    417                 new Map.Entry[mPendingSessions.size()]);
    418         for (Map.Entry<String, ISipSession> entry : entries) {
    419             if (entry.getValue().getState() != SipSession.State.INCOMING_CALL) {
    420                 mPendingSessions.remove(entry.getKey());
    421             }
    422         }
    423     }
    424 
    425     private synchronized boolean callingSelf(SipSessionGroupExt ringingGroup,
    426             SipSessionGroup.SipSessionImpl ringingSession) {
    427         String callId = ringingSession.getCallId();
    428         for (SipSessionGroupExt group : mSipGroups.values()) {
    429             if ((group != ringingGroup) && group.containsSession(callId)) {
    430                 if (DBG) log("call self: "
    431                         + ringingSession.getLocalProfile().getUriString()
    432                         + " -> " + group.getLocalProfile().getUriString());
    433                 return true;
    434             }
    435         }
    436         return false;
    437     }
    438 
    439     private synchronized void onKeepAliveIntervalChanged() {
    440         for (SipSessionGroupExt group : mSipGroups.values()) {
    441             group.onKeepAliveIntervalChanged();
    442         }
    443     }
    444 
    445     private int getKeepAliveInterval() {
    446         return (mKeepAliveInterval < 0)
    447                 ? mLastGoodKeepAliveInterval
    448                 : mKeepAliveInterval;
    449     }
    450 
    451     private boolean isBehindNAT(String address) {
    452         try {
    453             // TODO: How is isBehindNAT used and why these constanst address:
    454             //       10.x.x.x | 192.168.x.x | 172.16.x.x .. 172.19.x.x
    455             byte[] d = InetAddress.getByName(address).getAddress();
    456             if ((d[0] == 10) ||
    457                     (((0x000000FF & d[0]) == 172) &&
    458                     ((0x000000F0 & d[1]) == 16)) ||
    459                     (((0x000000FF & d[0]) == 192) &&
    460                     ((0x000000FF & d[1]) == 168))) {
    461                 return true;
    462             }
    463         } catch (UnknownHostException e) {
    464             loge("isBehindAT()" + address, e);
    465         }
    466         return false;
    467     }
    468 
    469     private boolean canUseSip(String packageName, String message) {
    470         mContext.enforceCallingOrSelfPermission(
    471                 android.Manifest.permission.USE_SIP, message);
    472 
    473         return mAppOps.noteOp(AppOpsManager.OP_USE_SIP, Binder.getCallingUid(),
    474                 packageName) == AppOpsManager.MODE_ALLOWED;
    475     }
    476 
    477     private class SipSessionGroupExt extends SipSessionAdapter {
    478         private static final String SSGE_TAG = "SipSessionGroupExt";
    479         private static final boolean SSGE_DBG = true;
    480         private SipSessionGroup mSipGroup;
    481         private PendingIntent mIncomingCallPendingIntent;
    482         private boolean mOpenedToReceiveCalls;
    483 
    484         private SipAutoReg mAutoRegistration =
    485                 new SipAutoReg();
    486 
    487         public SipSessionGroupExt(SipProfile localProfile,
    488                 PendingIntent incomingCallPendingIntent,
    489                 ISipSessionListener listener) throws SipException {
    490             if (SSGE_DBG) log("SipSessionGroupExt: profile=" + localProfile);
    491             mSipGroup = new SipSessionGroup(duplicate(localProfile),
    492                     localProfile.getPassword(), mTimer, mMyWakeLock);
    493             mIncomingCallPendingIntent = incomingCallPendingIntent;
    494             mAutoRegistration.setListener(listener);
    495         }
    496 
    497         public SipProfile getLocalProfile() {
    498             return mSipGroup.getLocalProfile();
    499         }
    500 
    501         public boolean containsSession(String callId) {
    502             return mSipGroup.containsSession(callId);
    503         }
    504 
    505         public void onKeepAliveIntervalChanged() {
    506             mAutoRegistration.onKeepAliveIntervalChanged();
    507         }
    508 
    509         // TODO: remove this method once SipWakeupTimer can better handle variety
    510         // of timeout values
    511         void setWakeupTimer(SipWakeupTimer timer) {
    512             mSipGroup.setWakeupTimer(timer);
    513         }
    514 
    515         private SipProfile duplicate(SipProfile p) {
    516             try {
    517                 return new SipProfile.Builder(p).setPassword("*").build();
    518             } catch (Exception e) {
    519                 loge("duplicate()", e);
    520                 throw new RuntimeException("duplicate profile", e);
    521             }
    522         }
    523 
    524         public void setListener(ISipSessionListener listener) {
    525             mAutoRegistration.setListener(listener);
    526         }
    527 
    528         public void setIncomingCallPendingIntent(PendingIntent pIntent) {
    529             mIncomingCallPendingIntent = pIntent;
    530         }
    531 
    532         public void openToReceiveCalls() {
    533             mOpenedToReceiveCalls = true;
    534             if (mNetworkType != -1) {
    535                 mSipGroup.openToReceiveCalls(this);
    536                 mAutoRegistration.start(mSipGroup);
    537             }
    538             if (SSGE_DBG) log("openToReceiveCalls: " + obfuscateSipUri(getUri()) + ": "
    539                     + mIncomingCallPendingIntent);
    540         }
    541 
    542         public void onConnectivityChanged(boolean connected)
    543                 throws SipException {
    544             if (SSGE_DBG) {
    545                 log("onConnectivityChanged: connected=" + connected + " uri="
    546                     + obfuscateSipUri(getUri()) + ": " + mIncomingCallPendingIntent);
    547             }
    548             mSipGroup.onConnectivityChanged();
    549             if (connected) {
    550                 mSipGroup.reset();
    551                 if (mOpenedToReceiveCalls) openToReceiveCalls();
    552             } else {
    553                 mSipGroup.close();
    554                 mAutoRegistration.stop();
    555             }
    556         }
    557 
    558         public void close() {
    559             mOpenedToReceiveCalls = false;
    560             mSipGroup.close();
    561             mAutoRegistration.stop();
    562             if (SSGE_DBG) log("close: " + obfuscateSipUri(getUri()) + ": "
    563                     + mIncomingCallPendingIntent);
    564         }
    565 
    566         public ISipSession createSession(ISipSessionListener listener) {
    567             if (SSGE_DBG) log("createSession");
    568             return mSipGroup.createSession(listener);
    569         }
    570 
    571         @Override
    572         public void onRinging(ISipSession s, SipProfile caller,
    573                 String sessionDescription) {
    574             SipSessionGroup.SipSessionImpl session =
    575                     (SipSessionGroup.SipSessionImpl) s;
    576             synchronized (SipService.this) {
    577                 try {
    578                     if (!isRegistered() || callingSelf(this, session)) {
    579                         if (SSGE_DBG) log("onRinging: end notReg or self");
    580                         session.endCall();
    581                         return;
    582                     }
    583 
    584                     // send out incoming call broadcast
    585                     addPendingSession(session);
    586                     Intent intent = SipManager.createIncomingCallBroadcast(
    587                             session.getCallId(), sessionDescription);
    588                     if (SSGE_DBG) log("onRinging: uri=" + getUri() + ": "
    589                             + caller.getUri() + ": " + session.getCallId()
    590                             + " " + mIncomingCallPendingIntent);
    591                     mIncomingCallPendingIntent.send(mContext,
    592                             SipManager.INCOMING_CALL_RESULT_CODE, intent);
    593                 } catch (PendingIntent.CanceledException e) {
    594                     loge("onRinging: pendingIntent is canceled, drop incoming call", e);
    595                     session.endCall();
    596                 }
    597             }
    598         }
    599 
    600         @Override
    601         public void onError(ISipSession session, int errorCode,
    602                 String message) {
    603             if (SSGE_DBG) log("onError: errorCode=" + errorCode + " desc="
    604                     + SipErrorCode.toString(errorCode) + ": " + message);
    605         }
    606 
    607         public boolean isOpenedToReceiveCalls() {
    608             return mOpenedToReceiveCalls;
    609         }
    610 
    611         public boolean isRegistered() {
    612             return mAutoRegistration.isRegistered();
    613         }
    614 
    615         private String getUri() {
    616             return mSipGroup.getLocalProfileUri();
    617         }
    618 
    619         private void log(String s) {
    620             Rlog.d(SSGE_TAG, s);
    621         }
    622 
    623         private void loge(String s, Throwable t) {
    624             Rlog.e(SSGE_TAG, s, t);
    625         }
    626 
    627     }
    628 
    629     private class SipKeepAliveProcessCallback implements Runnable,
    630             SipSessionGroup.KeepAliveProcessCallback {
    631         private static final String SKAI_TAG = "SipKeepAliveProcessCallback";
    632         private static final boolean SKAI_DBG = true;
    633         private static final int MIN_INTERVAL = 5; // in seconds
    634         private static final int PASS_THRESHOLD = 10;
    635         private static final int NAT_MEASUREMENT_RETRY_INTERVAL = 120; // in seconds
    636         private SipProfile mLocalProfile;
    637         private SipSessionGroupExt mGroup;
    638         private SipSessionGroup.SipSessionImpl mSession;
    639         private int mMinInterval;
    640         private int mMaxInterval;
    641         private int mInterval;
    642         private int mPassCount;
    643 
    644         public SipKeepAliveProcessCallback(SipProfile localProfile,
    645                 int minInterval, int maxInterval) {
    646             mMaxInterval = maxInterval;
    647             mMinInterval = minInterval;
    648             mLocalProfile = localProfile;
    649         }
    650 
    651         public void start() {
    652             synchronized (SipService.this) {
    653                 if (mSession != null) {
    654                     return;
    655                 }
    656 
    657                 mInterval = (mMaxInterval + mMinInterval) / 2;
    658                 mPassCount = 0;
    659 
    660                 // Don't start measurement if the interval is too small
    661                 if (mInterval < DEFAULT_KEEPALIVE_INTERVAL || checkTermination()) {
    662                     if (SKAI_DBG) log("start: measurement aborted; interval=[" +
    663                             mMinInterval + "," + mMaxInterval + "]");
    664                     return;
    665                 }
    666 
    667                 try {
    668                     if (SKAI_DBG) log("start: interval=" + mInterval);
    669 
    670                     mGroup = new SipSessionGroupExt(mLocalProfile, null, null);
    671                     // TODO: remove this line once SipWakeupTimer can better handle
    672                     // variety of timeout values
    673                     mGroup.setWakeupTimer(new SipWakeupTimer(mContext, mExecutor));
    674 
    675                     mSession = (SipSessionGroup.SipSessionImpl)
    676                             mGroup.createSession(null);
    677                     mSession.startKeepAliveProcess(mInterval, this);
    678                 } catch (Throwable t) {
    679                     onError(SipErrorCode.CLIENT_ERROR, t.toString());
    680                 }
    681             }
    682         }
    683 
    684         public void stop() {
    685             synchronized (SipService.this) {
    686                 if (mSession != null) {
    687                     mSession.stopKeepAliveProcess();
    688                     mSession = null;
    689                 }
    690                 if (mGroup != null) {
    691                     mGroup.close();
    692                     mGroup = null;
    693                 }
    694                 mTimer.cancel(this);
    695                 if (SKAI_DBG) log("stop");
    696             }
    697         }
    698 
    699         private void restart() {
    700             synchronized (SipService.this) {
    701                 // Return immediately if the measurement process is stopped
    702                 if (mSession == null) return;
    703 
    704                 if (SKAI_DBG) log("restart: interval=" + mInterval);
    705                 try {
    706                     mSession.stopKeepAliveProcess();
    707                     mPassCount = 0;
    708                     mSession.startKeepAliveProcess(mInterval, this);
    709                 } catch (SipException e) {
    710                     loge("restart", e);
    711                 }
    712             }
    713         }
    714 
    715         private boolean checkTermination() {
    716             return ((mMaxInterval - mMinInterval) < MIN_INTERVAL);
    717         }
    718 
    719         // SipSessionGroup.KeepAliveProcessCallback
    720         @Override
    721         public void onResponse(boolean portChanged) {
    722             synchronized (SipService.this) {
    723                 if (!portChanged) {
    724                     if (++mPassCount != PASS_THRESHOLD) return;
    725                     // update the interval, since the current interval is good to
    726                     // keep the port mapping.
    727                     if (mKeepAliveInterval > 0) {
    728                         mLastGoodKeepAliveInterval = mKeepAliveInterval;
    729                     }
    730                     mKeepAliveInterval = mMinInterval = mInterval;
    731                     if (SKAI_DBG) {
    732                         log("onResponse: portChanged=" + portChanged + " mKeepAliveInterval="
    733                                 + mKeepAliveInterval);
    734                     }
    735                     onKeepAliveIntervalChanged();
    736                 } else {
    737                     // Since the rport is changed, shorten the interval.
    738                     mMaxInterval = mInterval;
    739                 }
    740                 if (checkTermination()) {
    741                     // update mKeepAliveInterval and stop measurement.
    742                     stop();
    743                     // If all the measurements failed, we still set it to
    744                     // mMinInterval; If mMinInterval still doesn't work, a new
    745                     // measurement with min interval=DEFAULT_KEEPALIVE_INTERVAL
    746                     // will be conducted.
    747                     mKeepAliveInterval = mMinInterval;
    748                     if (SKAI_DBG) {
    749                         log("onResponse: checkTermination mKeepAliveInterval="
    750                                 + mKeepAliveInterval);
    751                     }
    752                 } else {
    753                     // calculate the new interval and continue.
    754                     mInterval = (mMaxInterval + mMinInterval) / 2;
    755                     if (SKAI_DBG) {
    756                         log("onResponse: mKeepAliveInterval=" + mKeepAliveInterval
    757                                 + ", new mInterval=" + mInterval);
    758                     }
    759                     restart();
    760                 }
    761             }
    762         }
    763 
    764         // SipSessionGroup.KeepAliveProcessCallback
    765         @Override
    766         public void onError(int errorCode, String description) {
    767             if (SKAI_DBG) loge("onError: errorCode=" + errorCode + " desc=" + description);
    768             restartLater();
    769         }
    770 
    771         // timeout handler
    772         @Override
    773         public void run() {
    774             mTimer.cancel(this);
    775             restart();
    776         }
    777 
    778         private void restartLater() {
    779             synchronized (SipService.this) {
    780                 int interval = NAT_MEASUREMENT_RETRY_INTERVAL;
    781                 mTimer.cancel(this);
    782                 mTimer.set(interval * 1000, this);
    783             }
    784         }
    785 
    786         private void log(String s) {
    787             Rlog.d(SKAI_TAG, s);
    788         }
    789 
    790         private void loge(String s) {
    791             Rlog.d(SKAI_TAG, s);
    792         }
    793 
    794         private void loge(String s, Throwable t) {
    795             Rlog.d(SKAI_TAG, s, t);
    796         }
    797     }
    798 
    799     private class SipAutoReg extends SipSessionAdapter
    800             implements Runnable, SipSessionGroup.KeepAliveProcessCallback {
    801         private String SAR_TAG;
    802         private static final boolean SAR_DBG = true;
    803         private static final int MIN_KEEPALIVE_SUCCESS_COUNT = 10;
    804 
    805         private SipSessionGroup.SipSessionImpl mSession;
    806         private SipSessionGroup.SipSessionImpl mKeepAliveSession;
    807         private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
    808         private int mBackoff = 1;
    809         private boolean mRegistered;
    810         private long mExpiryTime;
    811         private int mErrorCode;
    812         private String mErrorMessage;
    813         private boolean mRunning = false;
    814 
    815         private int mKeepAliveSuccessCount = 0;
    816 
    817         public void start(SipSessionGroup group) {
    818             if (!mRunning) {
    819                 mRunning = true;
    820                 mBackoff = 1;
    821                 mSession = (SipSessionGroup.SipSessionImpl)
    822                         group.createSession(this);
    823                 // return right away if no active network connection.
    824                 if (mSession == null) return;
    825 
    826                 // start unregistration to clear up old registration at server
    827                 // TODO: when rfc5626 is deployed, use reg-id and sip.instance
    828                 // in registration to avoid adding duplicate entries to server
    829                 mMyWakeLock.acquire(mSession);
    830                 mSession.unregister();
    831                 SAR_TAG = "SipAutoReg:" +
    832                         obfuscateSipUri(mSession.getLocalProfile().getUriString());
    833                 if (SAR_DBG) log("start: group=" + group);
    834             }
    835         }
    836 
    837         private void startKeepAliveProcess(int interval) {
    838             if (SAR_DBG) log("startKeepAliveProcess: interval=" + interval);
    839             if (mKeepAliveSession == null) {
    840                 mKeepAliveSession = mSession.duplicate();
    841             } else {
    842                 mKeepAliveSession.stopKeepAliveProcess();
    843             }
    844             try {
    845                 mKeepAliveSession.startKeepAliveProcess(interval, this);
    846             } catch (SipException e) {
    847                 loge("startKeepAliveProcess: interval=" + interval, e);
    848             }
    849         }
    850 
    851         private void stopKeepAliveProcess() {
    852             if (mKeepAliveSession != null) {
    853                 mKeepAliveSession.stopKeepAliveProcess();
    854                 mKeepAliveSession = null;
    855             }
    856             mKeepAliveSuccessCount = 0;
    857         }
    858 
    859         // SipSessionGroup.KeepAliveProcessCallback
    860         @Override
    861         public void onResponse(boolean portChanged) {
    862             synchronized (SipService.this) {
    863                 if (portChanged) {
    864                     int interval = getKeepAliveInterval();
    865                     if (mKeepAliveSuccessCount < MIN_KEEPALIVE_SUCCESS_COUNT) {
    866                         if (SAR_DBG) {
    867                             log("onResponse: keepalive doesn't work with interval "
    868                                     + interval + ", past success count="
    869                                     + mKeepAliveSuccessCount);
    870                         }
    871                         if (interval > DEFAULT_KEEPALIVE_INTERVAL) {
    872                             restartPortMappingLifetimeMeasurement(
    873                                     mSession.getLocalProfile(), interval);
    874                             mKeepAliveSuccessCount = 0;
    875                         }
    876                     } else {
    877                         if (SAR_DBG) {
    878                             log("keep keepalive going with interval "
    879                                     + interval + ", past success count="
    880                                     + mKeepAliveSuccessCount);
    881                         }
    882                         mKeepAliveSuccessCount /= 2;
    883                     }
    884                 } else {
    885                     // Start keep-alive interval measurement on the first
    886                     // successfully kept-alive SipSessionGroup
    887                     startPortMappingLifetimeMeasurement(
    888                             mSession.getLocalProfile());
    889                     mKeepAliveSuccessCount++;
    890                 }
    891 
    892                 if (!mRunning || !portChanged) return;
    893 
    894                 // The keep alive process is stopped when port is changed;
    895                 // Nullify the session so that the process can be restarted
    896                 // again when the re-registration is done
    897                 mKeepAliveSession = null;
    898 
    899                 // Acquire wake lock for the registration process. The
    900                 // lock will be released when registration is complete.
    901                 mMyWakeLock.acquire(mSession);
    902                 mSession.register(EXPIRY_TIME);
    903             }
    904         }
    905 
    906         // SipSessionGroup.KeepAliveProcessCallback
    907         @Override
    908         public void onError(int errorCode, String description) {
    909             if (SAR_DBG) {
    910                 loge("onError: errorCode=" + errorCode + " desc=" + description);
    911             }
    912             onResponse(true); // re-register immediately
    913         }
    914 
    915         public void stop() {
    916             if (!mRunning) return;
    917             mRunning = false;
    918             mMyWakeLock.release(mSession);
    919             if (mSession != null) {
    920                 mSession.setListener(null);
    921                 if (mNetworkType != -1 && mRegistered) mSession.unregister();
    922             }
    923 
    924             mTimer.cancel(this);
    925             stopKeepAliveProcess();
    926 
    927             mRegistered = false;
    928             setListener(mProxy.getListener());
    929         }
    930 
    931         public void onKeepAliveIntervalChanged() {
    932             if (mKeepAliveSession != null) {
    933                 int newInterval = getKeepAliveInterval();
    934                 if (SAR_DBG) {
    935                     log("onKeepAliveIntervalChanged: interval=" + newInterval);
    936                 }
    937                 mKeepAliveSuccessCount = 0;
    938                 startKeepAliveProcess(newInterval);
    939             }
    940         }
    941 
    942         public void setListener(ISipSessionListener listener) {
    943             synchronized (SipService.this) {
    944                 mProxy.setListener(listener);
    945 
    946                 try {
    947                     int state = (mSession == null)
    948                             ? SipSession.State.READY_TO_CALL
    949                             : mSession.getState();
    950                     if ((state == SipSession.State.REGISTERING)
    951                             || (state == SipSession.State.DEREGISTERING)) {
    952                         mProxy.onRegistering(mSession);
    953                     } else if (mRegistered) {
    954                         int duration = (int)
    955                                 (mExpiryTime - SystemClock.elapsedRealtime());
    956                         mProxy.onRegistrationDone(mSession, duration);
    957                     } else if (mErrorCode != SipErrorCode.NO_ERROR) {
    958                         if (mErrorCode == SipErrorCode.TIME_OUT) {
    959                             mProxy.onRegistrationTimeout(mSession);
    960                         } else {
    961                             mProxy.onRegistrationFailed(mSession, mErrorCode,
    962                                     mErrorMessage);
    963                         }
    964                     } else if (mNetworkType == -1) {
    965                         mProxy.onRegistrationFailed(mSession,
    966                                 SipErrorCode.DATA_CONNECTION_LOST,
    967                                 "no data connection");
    968                     } else if (!mRunning) {
    969                         mProxy.onRegistrationFailed(mSession,
    970                                 SipErrorCode.CLIENT_ERROR,
    971                                 "registration not running");
    972                     } else {
    973                         mProxy.onRegistrationFailed(mSession,
    974                                 SipErrorCode.IN_PROGRESS,
    975                                 String.valueOf(state));
    976                     }
    977                 } catch (Throwable t) {
    978                     loge("setListener: ", t);
    979                 }
    980             }
    981         }
    982 
    983         public boolean isRegistered() {
    984             return mRegistered;
    985         }
    986 
    987         // timeout handler: re-register
    988         @Override
    989         public void run() {
    990             synchronized (SipService.this) {
    991                 if (!mRunning) return;
    992 
    993                 mErrorCode = SipErrorCode.NO_ERROR;
    994                 mErrorMessage = null;
    995                 if (SAR_DBG) log("run: registering");
    996                 if (mNetworkType != -1) {
    997                     mMyWakeLock.acquire(mSession);
    998                     mSession.register(EXPIRY_TIME);
    999                 }
   1000             }
   1001         }
   1002 
   1003         private void restart(int duration) {
   1004             if (SAR_DBG) log("restart: duration=" + duration + "s later.");
   1005             mTimer.cancel(this);
   1006             mTimer.set(duration * 1000, this);
   1007         }
   1008 
   1009         private int backoffDuration() {
   1010             int duration = SHORT_EXPIRY_TIME * mBackoff;
   1011             if (duration > 3600) {
   1012                 duration = 3600;
   1013             } else {
   1014                 mBackoff *= 2;
   1015             }
   1016             return duration;
   1017         }
   1018 
   1019         @Override
   1020         public void onRegistering(ISipSession session) {
   1021             if (SAR_DBG) log("onRegistering: " + session);
   1022             synchronized (SipService.this) {
   1023                 if (notCurrentSession(session)) return;
   1024 
   1025                 mRegistered = false;
   1026                 mProxy.onRegistering(session);
   1027             }
   1028         }
   1029 
   1030         private boolean notCurrentSession(ISipSession session) {
   1031             if (session != mSession) {
   1032                 ((SipSessionGroup.SipSessionImpl) session).setListener(null);
   1033                 mMyWakeLock.release(session);
   1034                 return true;
   1035             }
   1036             return !mRunning;
   1037         }
   1038 
   1039         @Override
   1040         public void onRegistrationDone(ISipSession session, int duration) {
   1041             if (SAR_DBG) log("onRegistrationDone: " + session);
   1042             synchronized (SipService.this) {
   1043                 if (notCurrentSession(session)) return;
   1044 
   1045                 mProxy.onRegistrationDone(session, duration);
   1046 
   1047                 if (duration > 0) {
   1048                     mExpiryTime = SystemClock.elapsedRealtime()
   1049                             + (duration * 1000);
   1050 
   1051                     if (!mRegistered) {
   1052                         mRegistered = true;
   1053                         // allow some overlap to avoid call drop during renew
   1054                         duration -= MIN_EXPIRY_TIME;
   1055                         if (duration < MIN_EXPIRY_TIME) {
   1056                             duration = MIN_EXPIRY_TIME;
   1057                         }
   1058                         restart(duration);
   1059 
   1060                         SipProfile localProfile = mSession.getLocalProfile();
   1061                         if ((mKeepAliveSession == null) && (isBehindNAT(mLocalIp)
   1062                                 || localProfile.getSendKeepAlive())) {
   1063                             startKeepAliveProcess(getKeepAliveInterval());
   1064                         }
   1065                     }
   1066                     mMyWakeLock.release(session);
   1067                 } else {
   1068                     mRegistered = false;
   1069                     mExpiryTime = -1L;
   1070                     if (SAR_DBG) log("Refresh registration immediately");
   1071                     run();
   1072                 }
   1073             }
   1074         }
   1075 
   1076         @Override
   1077         public void onRegistrationFailed(ISipSession session, int errorCode,
   1078                 String message) {
   1079             if (SAR_DBG) log("onRegistrationFailed: " + session + ": "
   1080                     + SipErrorCode.toString(errorCode) + ": " + message);
   1081             synchronized (SipService.this) {
   1082                 if (notCurrentSession(session)) return;
   1083 
   1084                 switch (errorCode) {
   1085                     case SipErrorCode.INVALID_CREDENTIALS:
   1086                     case SipErrorCode.SERVER_UNREACHABLE:
   1087                         if (SAR_DBG) log("   pause auto-registration");
   1088                         stop();
   1089                         break;
   1090                     default:
   1091                         restartLater();
   1092                 }
   1093 
   1094                 mErrorCode = errorCode;
   1095                 mErrorMessage = message;
   1096                 mProxy.onRegistrationFailed(session, errorCode, message);
   1097                 mMyWakeLock.release(session);
   1098             }
   1099         }
   1100 
   1101         @Override
   1102         public void onRegistrationTimeout(ISipSession session) {
   1103             if (SAR_DBG) log("onRegistrationTimeout: " + session);
   1104             synchronized (SipService.this) {
   1105                 if (notCurrentSession(session)) return;
   1106 
   1107                 mErrorCode = SipErrorCode.TIME_OUT;
   1108                 mProxy.onRegistrationTimeout(session);
   1109                 restartLater();
   1110                 mMyWakeLock.release(session);
   1111             }
   1112         }
   1113 
   1114         private void restartLater() {
   1115             if (SAR_DBG) loge("restartLater");
   1116             mRegistered = false;
   1117             restart(backoffDuration());
   1118         }
   1119 
   1120         private void log(String s) {
   1121             Rlog.d(SAR_TAG, s);
   1122         }
   1123 
   1124         private void loge(String s) {
   1125             Rlog.e(SAR_TAG, s);
   1126         }
   1127 
   1128         private void loge(String s, Throwable e) {
   1129             Rlog.e(SAR_TAG, s, e);
   1130         }
   1131     }
   1132 
   1133     private class ConnectivityReceiver extends BroadcastReceiver {
   1134         @Override
   1135         public void onReceive(Context context, Intent intent) {
   1136             Bundle bundle = intent.getExtras();
   1137             if (bundle != null) {
   1138                 final NetworkInfo info = (NetworkInfo)
   1139                         bundle.get(ConnectivityManager.EXTRA_NETWORK_INFO);
   1140 
   1141                 // Run the handler in MyExecutor to be protected by wake lock
   1142                 mExecutor.execute(new Runnable() {
   1143                     @Override
   1144                     public void run() {
   1145                         onConnectivityChanged(info);
   1146                     }
   1147                 });
   1148             }
   1149         }
   1150     }
   1151 
   1152     private void registerReceivers() {
   1153         mContext.registerReceiver(mConnectivityReceiver,
   1154                 new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
   1155         if (DBG) log("registerReceivers:");
   1156     }
   1157 
   1158     private void unregisterReceivers() {
   1159         mContext.unregisterReceiver(mConnectivityReceiver);
   1160         if (DBG) log("unregisterReceivers:");
   1161 
   1162         // Reset variables maintained by ConnectivityReceiver.
   1163         mWifiLock.release();
   1164         mNetworkType = -1;
   1165     }
   1166 
   1167     private void updateWakeLocks() {
   1168         for (SipSessionGroupExt group : mSipGroups.values()) {
   1169             if (group.isOpenedToReceiveCalls()) {
   1170                 // Also grab the WifiLock when we are disconnected, so the
   1171                 // system will keep trying to reconnect. It will be released
   1172                 // when the system eventually connects to something else.
   1173                 if (mNetworkType == ConnectivityManager.TYPE_WIFI || mNetworkType == -1) {
   1174                     mWifiLock.acquire();
   1175                 } else {
   1176                     mWifiLock.release();
   1177                 }
   1178                 return;
   1179             }
   1180         }
   1181         mWifiLock.release();
   1182         mMyWakeLock.reset(); // in case there's a leak
   1183     }
   1184 
   1185     private synchronized void onConnectivityChanged(NetworkInfo info) {
   1186         // We only care about the default network, and getActiveNetworkInfo()
   1187         // is the only way to distinguish them. However, as broadcasts are
   1188         // delivered asynchronously, we might miss DISCONNECTED events from
   1189         // getActiveNetworkInfo(), which is critical to our SIP stack. To
   1190         // solve this, if it is a DISCONNECTED event to our current network,
   1191         // respect it. Otherwise get a new one from getActiveNetworkInfo().
   1192         if (info == null || info.isConnected() || info.getType() != mNetworkType) {
   1193             ConnectivityManager cm = (ConnectivityManager)
   1194                     mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
   1195             info = cm.getActiveNetworkInfo();
   1196         }
   1197 
   1198         // Some devices limit SIP on Wi-Fi. In this case, if we are not on
   1199         // Wi-Fi, treat it as a DISCONNECTED event.
   1200         int networkType = (info != null && info.isConnected()) ? info.getType() : -1;
   1201         if (mSipOnWifiOnly && networkType != ConnectivityManager.TYPE_WIFI) {
   1202             networkType = -1;
   1203         }
   1204 
   1205         // Ignore the event if the current active network is not changed.
   1206         if (mNetworkType == networkType) {
   1207             // TODO: Maybe we need to send seq/generation number
   1208             return;
   1209         }
   1210         if (DBG) {
   1211             log("onConnectivityChanged: " + mNetworkType +
   1212                     " -> " + networkType);
   1213         }
   1214 
   1215         try {
   1216             if (mNetworkType != -1) {
   1217                 mLocalIp = null;
   1218                 stopPortMappingMeasurement();
   1219                 for (SipSessionGroupExt group : mSipGroups.values()) {
   1220                     group.onConnectivityChanged(false);
   1221                 }
   1222             }
   1223             mNetworkType = networkType;
   1224 
   1225             if (mNetworkType != -1) {
   1226                 mLocalIp = determineLocalIp();
   1227                 mKeepAliveInterval = -1;
   1228                 mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
   1229                 for (SipSessionGroupExt group : mSipGroups.values()) {
   1230                     group.onConnectivityChanged(true);
   1231                 }
   1232             }
   1233             updateWakeLocks();
   1234         } catch (SipException e) {
   1235             loge("onConnectivityChanged()", e);
   1236         }
   1237     }
   1238 
   1239     private static Looper createLooper() {
   1240         HandlerThread thread = new HandlerThread("SipService.Executor");
   1241         thread.start();
   1242         return thread.getLooper();
   1243     }
   1244 
   1245     // Executes immediate tasks in a single thread.
   1246     // Hold/release wake lock for running tasks
   1247     private class MyExecutor extends Handler implements Executor {
   1248         MyExecutor() {
   1249             super(createLooper());
   1250         }
   1251 
   1252         @Override
   1253         public void execute(Runnable task) {
   1254             mMyWakeLock.acquire(task);
   1255             Message.obtain(this, 0/* don't care */, task).sendToTarget();
   1256         }
   1257 
   1258         @Override
   1259         public void handleMessage(Message msg) {
   1260             if (msg.obj instanceof Runnable) {
   1261                 executeInternal((Runnable) msg.obj);
   1262             } else {
   1263                 if (DBG) log("handleMessage: not Runnable ignore msg=" + msg);
   1264             }
   1265         }
   1266 
   1267         private void executeInternal(Runnable task) {
   1268             try {
   1269                 task.run();
   1270             } catch (Throwable t) {
   1271                 loge("run task: " + task, t);
   1272             } finally {
   1273                 mMyWakeLock.release(task);
   1274             }
   1275         }
   1276     }
   1277 
   1278     private void log(String s) {
   1279         Rlog.d(TAG, s);
   1280     }
   1281 
   1282     private static void slog(String s) {
   1283         Rlog.d(TAG, s);
   1284     }
   1285 
   1286     private void loge(String s, Throwable e) {
   1287         Rlog.e(TAG, s, e);
   1288     }
   1289 
   1290     public static String obfuscateSipUri(String sipUri) {
   1291         StringBuilder sb = new StringBuilder();
   1292         int start = 0;
   1293         sipUri = sipUri.trim();
   1294         if (sipUri.startsWith("sip:")) {
   1295             start = 4;
   1296             sb.append("sip:");
   1297         }
   1298 
   1299         char prevC = '\0';
   1300         int len = sipUri.length();
   1301         for (int i = start; i < len; i++) {
   1302             char c = sipUri.charAt(i);
   1303             char nextC = (i + 1 < len) ? sipUri.charAt(i + 1) : '\0';
   1304             char charToAppend = '*';
   1305 
   1306             // This logic allows the first and last letter before an '@' sign to show up without
   1307             // obfuscation as well as the first and last letter an '@' sign.
   1308             // e.g.: brad (at) comment.it => b**d@c******.*t
   1309             if ((i - start < 1) ||
   1310                     (i + 1 == len) ||
   1311                     isAllowedCharacter(c) ||
   1312                     (prevC == '@') ||
   1313                     (nextC == '@')) {
   1314                 charToAppend = c;
   1315             }
   1316             sb.append(charToAppend);
   1317             prevC = c;
   1318         }
   1319         return sb.toString();
   1320     }
   1321 
   1322     private static boolean isAllowedCharacter(char c) {
   1323         return c == '@' || c == '.';
   1324     }
   1325 }
   1326