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