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