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