1 /* 2 * Copyright (C) 2014 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.services.telephony; 18 19 import android.net.Uri; 20 import android.os.AsyncResult; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.os.SystemClock; 25 import android.telecom.PhoneAccount; 26 import android.telecom.PhoneAccountHandle; 27 import android.telecom.TelecomManager; 28 import android.telephony.ims.ImsCallProfile; 29 import android.text.TextUtils; 30 31 import com.android.ims.ImsCall; 32 import com.android.internal.telephony.Call; 33 import com.android.internal.telephony.CallStateException; 34 import com.android.internal.telephony.Connection; 35 import com.android.internal.telephony.GsmCdmaPhone; 36 import com.android.internal.telephony.LocaleTracker; 37 import com.android.internal.telephony.Phone; 38 import com.android.internal.telephony.PhoneConstants; 39 import com.android.internal.telephony.ServiceStateTracker; 40 import com.android.internal.telephony.cdma.CdmaCallWaitingNotification; 41 import com.android.internal.telephony.imsphone.ImsExternalCallTracker; 42 import com.android.internal.telephony.imsphone.ImsExternalConnection; 43 import com.android.internal.telephony.imsphone.ImsPhone; 44 import com.android.internal.telephony.imsphone.ImsPhoneCallTracker; 45 import com.android.internal.telephony.imsphone.ImsPhoneConnection; 46 import com.android.phone.NumberVerificationManager; 47 import com.android.phone.PhoneUtils; 48 import com.android.phone.callcomposer.CallComposerPictureManager; 49 import com.android.telephony.Rlog; 50 51 import java.util.List; 52 import java.util.Objects; 53 import java.util.stream.Collectors; 54 55 /** 56 * Listens to incoming-call events from the associated phone object and notifies Telecom upon each 57 * occurence. One instance of these exists for each of the telephony-based call services. 58 */ 59 final class PstnIncomingCallNotifier { 60 private static final String LOG_TAG = "PstnIncomingCallNotifier"; 61 62 /** New ringing connection event code. */ 63 private static final int EVENT_NEW_RINGING_CONNECTION = 100; 64 private static final int EVENT_CDMA_CALL_WAITING = 101; 65 private static final int EVENT_UNKNOWN_CONNECTION = 102; 66 67 /** 68 * The max amount of time to wait before hanging up a call that was for number verification. 69 * 70 * The delay is so that the remote end has time to hang up the call after receiving the 71 * verification signal so that the call doesn't go to voicemail. 72 */ 73 private static final int MAX_NUMBER_VERIFICATION_HANGUP_DELAY_MILLIS = 10000; 74 75 /** 76 * Hardcoded extra for a call that's used to provide metrics information to the dialer app. 77 */ 78 private static final String EXTRA_CALL_CREATED_TIME_MILLIS = 79 "android.telecom.extra.CALL_CREATED_TIME_MILLIS"; 80 81 /** The phone object to listen to. */ 82 private final Phone mPhone; 83 84 /** 85 * Used to listen to events from {@link #mPhone}. 86 */ 87 private final Handler mHandler; 88 89 /** 90 * Persists the specified parameters and starts listening to phone events. 91 * 92 * @param phone The phone object for listening to incoming calls. 93 */ PstnIncomingCallNotifier(Phone phone)94 PstnIncomingCallNotifier(Phone phone) { 95 if (phone == null) { 96 throw new NullPointerException(); 97 } 98 99 mPhone = phone; 100 mHandler = new Handler(phone.getLooper()) { 101 @Override 102 public void handleMessage(Message msg) { 103 switch(msg.what) { 104 case EVENT_NEW_RINGING_CONNECTION: 105 handleNewRingingConnection((AsyncResult) msg.obj); 106 break; 107 case EVENT_CDMA_CALL_WAITING: 108 handleCdmaCallWaiting((AsyncResult) msg.obj); 109 break; 110 case EVENT_UNKNOWN_CONNECTION: 111 handleNewUnknownConnection((AsyncResult) msg.obj); 112 break; 113 default: 114 break; 115 } 116 } 117 118 @Override 119 public String toString() { 120 return String.format("[PstnIncomingCallNotifierHandler; phoneId=[%s]", 121 getPhoneIdAsString()); 122 } 123 }; 124 125 registerForNotifications(); 126 } 127 teardown()128 void teardown() { 129 unregisterForNotifications(); 130 } 131 132 /** 133 * Register for notifications from the base phone. 134 */ registerForNotifications()135 private void registerForNotifications() { 136 if (mPhone != null) { 137 Log.i(this, "Registering: [%s]", getPhoneIdAsString()); 138 mPhone.registerForNewRingingConnection(mHandler, EVENT_NEW_RINGING_CONNECTION, null); 139 mPhone.registerForCallWaiting(mHandler, EVENT_CDMA_CALL_WAITING, null); 140 mPhone.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION, null); 141 } 142 } 143 unregisterForNotifications()144 private void unregisterForNotifications() { 145 if (mPhone != null) { 146 Log.i(this, "Unregistering: [%s]", getPhoneIdAsString()); 147 mPhone.unregisterForNewRingingConnection(mHandler); 148 mPhone.unregisterForCallWaiting(mHandler); 149 mPhone.unregisterForUnknownConnection(mHandler); 150 } 151 } 152 153 /** 154 * Note: Same logic as 155 * {@link com.android.phone.PhoneInterfaceManager#getNetworkCountryIsoForPhone(int)}. 156 * @return the network country ISO for the current phone, or {@code null} if not known. 157 */ getNetworkCountryIso()158 private String getNetworkCountryIso() { 159 ServiceStateTracker sst = mPhone.getServiceStateTracker(); 160 if (sst == null) return null; 161 LocaleTracker lt = sst.getLocaleTracker(); 162 if (lt == null) return null; 163 return lt.getCurrentCountry(); 164 } 165 166 /** 167 * Verifies the incoming call and triggers sending the incoming-call intent to Telecom. 168 * 169 * @param asyncResult The result object from the new ringing event. 170 */ handleNewRingingConnection(AsyncResult asyncResult)171 private void handleNewRingingConnection(AsyncResult asyncResult) { 172 Log.i(this, "handleNewRingingConnection: phoneId=[%s]", getPhoneIdAsString()); 173 Connection connection = (Connection) asyncResult.result; 174 if (connection != null) { 175 Call call = connection.getCall(); 176 // Check if we have a pending number verification request. 177 if (connection.getAddress() != null) { 178 if (NumberVerificationManager.getInstance() 179 .checkIncomingCall(connection.getAddress(), getNetworkCountryIso())) { 180 // Disconnect the call if it matches, after a delay 181 mHandler.postDelayed(() -> { 182 try { 183 connection.hangup(); 184 } catch (CallStateException e) { 185 Log.i(this, "Remote end hung up call verification call"); 186 } 187 // TODO: use an app-supplied delay (needs new API), not to exceed the 188 // existing max. 189 }, MAX_NUMBER_VERIFICATION_HANGUP_DELAY_MILLIS); 190 return; 191 } 192 } 193 194 // Final verification of the ringing state before sending the intent to Telecom. 195 if (call != null && call.getState().isRinging()) { 196 sendIncomingCallIntent(connection); 197 } 198 } 199 } 200 handleCdmaCallWaiting(AsyncResult asyncResult)201 private void handleCdmaCallWaiting(AsyncResult asyncResult) { 202 Log.i(this, "handleCdmaCallWaiting: phoneId=[%s]", getPhoneIdAsString()); 203 CdmaCallWaitingNotification ccwi = (CdmaCallWaitingNotification) asyncResult.result; 204 Call call = mPhone.getRingingCall(); 205 if (call.getState() == Call.State.WAITING) { 206 Connection connection = call.getLatestConnection(); 207 if (connection != null) { 208 String number = connection.getAddress(); 209 int presentation = connection.getNumberPresentation(); 210 211 if (presentation != PhoneConstants.PRESENTATION_ALLOWED 212 && presentation == ccwi.numberPresentation) { 213 // Presentation of number not allowed, but the presentation of the Connection 214 // and the call waiting presentation match. 215 Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; " 216 + "presentation = %d", presentation); 217 sendIncomingCallIntent(connection); 218 } else if (!TextUtils.isEmpty(number) && Objects.equals(number, ccwi.number)) { 219 // Presentation of the number is allowed, so we ensure the number matches the 220 // one in the call waiting information. 221 Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; " 222 + "number = %s", Rlog.pii(LOG_TAG, number)); 223 sendIncomingCallIntent(connection); 224 } else { 225 Log.i(this, "handleCdmaCallWaiting: presentation or number do not match, not" 226 + " informing telecom of call: %s", ccwi); 227 } 228 } 229 } 230 } 231 handleNewUnknownConnection(AsyncResult asyncResult)232 private void handleNewUnknownConnection(AsyncResult asyncResult) { 233 Log.i(this, "handleNewUnknownConnection: phoneId=[%s]", getPhoneIdAsString()); 234 if (!(asyncResult.result instanceof Connection)) { 235 Log.i(this, "handleNewUnknownConnection called with non-Connection object"); 236 return; 237 } 238 Connection connection = (Connection) asyncResult.result; 239 if (connection != null) { 240 // Because there is a handler between telephony and here, it causes this action to be 241 // asynchronous which means that the call can switch to DISCONNECTED by the time it gets 242 // to this code. Check here to ensure we are not adding a disconnected or IDLE call. 243 Call.State state = connection.getState(); 244 if (state == Call.State.DISCONNECTED || state == Call.State.IDLE) { 245 Log.i(this, "Skipping new unknown connection because it is idle. " + connection); 246 return; 247 } 248 249 Call call = connection.getCall(); 250 if (call != null && call.getState().isAlive()) { 251 addNewUnknownCall(connection); 252 } else { 253 Log.i(this, "Skipping new unknown connection because its call is null or dead." 254 + " connection=" + connection); 255 } 256 } 257 } 258 addNewUnknownCall(Connection connection)259 private void addNewUnknownCall(Connection connection) { 260 Log.i(this, "addNewUnknownCall, connection is: %s", connection); 261 262 if (!maybeSwapAnyWithUnknownConnection(connection)) { 263 Log.i(this, "determined new connection is: %s", connection); 264 Bundle extras = new Bundle(); 265 if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && 266 !TextUtils.isEmpty(connection.getAddress())) { 267 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null); 268 extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, uri); 269 } 270 // ImsExternalConnections are keyed by a unique mCallId; include this as an extra on 271 // the call to addNewUknownCall in Telecom. This way when the request comes back to the 272 // TelephonyConnectionService, we will be able to determine which unknown connection is 273 // being added. 274 if (connection instanceof ImsExternalConnection) { 275 ImsExternalConnection externalConnection = (ImsExternalConnection) connection; 276 extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID, 277 externalConnection.getCallId()); 278 } 279 280 // Specifies the time the call was added. This is used by the dialer for analytics. 281 extras.putLong(EXTRA_CALL_CREATED_TIME_MILLIS, SystemClock.elapsedRealtime()); 282 283 PhoneAccountHandle handle = findCorrectPhoneAccountHandle(); 284 if (handle == null) { 285 try { 286 connection.hangup(); 287 } catch (CallStateException e) { 288 // connection already disconnected. Do nothing 289 } 290 } else { 291 TelecomManager tm = mPhone.getContext().getSystemService(TelecomManager.class); 292 tm.addNewUnknownCall(handle, extras); 293 } 294 } else { 295 Log.i(this, "swapped an old connection, new one is: %s", connection); 296 } 297 } 298 299 /** 300 * Sends the incoming call intent to telecom. 301 */ sendIncomingCallIntent(Connection connection)302 private void sendIncomingCallIntent(Connection connection) { 303 Bundle extras = new Bundle(); 304 if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && 305 !TextUtils.isEmpty(connection.getAddress())) { 306 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null); 307 extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri); 308 } 309 310 // Specifies the time the call was added. This is used by the dialer for analytics. 311 extras.putLong(EXTRA_CALL_CREATED_TIME_MILLIS, SystemClock.elapsedRealtime()); 312 313 if (connection.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 314 if (((ImsPhoneConnection) connection).isRttEnabledForCall()) { 315 extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true); 316 } 317 if (((ImsPhoneConnection) connection).isIncomingCallAutoRejected()) { 318 extras.putString(TelecomManager.EXTRA_CALL_DISCONNECT_MESSAGE, 319 TelecomManager.CALL_AUTO_DISCONNECT_MESSAGE_STRING); 320 } 321 ImsCall imsCall = ((ImsPhoneConnection) connection).getImsCall(); 322 if (imsCall != null) { 323 ImsCallProfile imsCallProfile = imsCall.getCallProfile(); 324 if (imsCallProfile != null) { 325 if (CallComposerPictureManager.sTestMode) { 326 imsCallProfile.setCallExtra(ImsCallProfile.EXTRA_PICTURE_URL, 327 CallComposerPictureManager.FAKE_SERVER_URL); 328 imsCallProfile.setCallExtraInt(ImsCallProfile.EXTRA_PRIORITY, 329 TelecomManager.PRIORITY_URGENT); 330 imsCallProfile.setCallExtra(ImsCallProfile.EXTRA_CALL_SUBJECT, 331 CallComposerPictureManager.FAKE_SUBJECT); 332 imsCallProfile.setCallExtraParcelable(ImsCallProfile.EXTRA_LOCATION, 333 CallComposerPictureManager.FAKE_LOCATION); 334 } 335 336 extras.putInt(TelecomManager.EXTRA_PRIORITY, 337 imsCallProfile.getCallExtraInt(ImsCallProfile.EXTRA_PRIORITY)); 338 extras.putString(TelecomManager.EXTRA_CALL_SUBJECT, 339 imsCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_SUBJECT)); 340 extras.putParcelable(TelecomManager.EXTRA_LOCATION, 341 imsCallProfile.getCallExtraParcelable(ImsCallProfile.EXTRA_LOCATION)); 342 if (!TextUtils.isEmpty( 343 imsCallProfile.getCallExtra(ImsCallProfile.EXTRA_PICTURE_URL))) { 344 extras.putBoolean(TelecomManager.EXTRA_HAS_PICTURE, true); 345 } 346 } 347 } 348 } 349 350 PhoneAccountHandle handle = findCorrectPhoneAccountHandle(); 351 if (handle == null) { 352 try { 353 connection.hangup(); 354 } catch (CallStateException e) { 355 // connection already disconnected. Do nothing 356 } 357 Log.wtf(LOG_TAG, "sendIncomingCallIntent: failed to add new call because no phone" 358 + " account could be found for the call"); 359 } else { 360 TelecomManager tm = mPhone.getContext().getSystemService(TelecomManager.class); 361 try { 362 if (connection.isMultiparty()) { 363 tm.addNewIncomingConference(handle, extras); 364 } else { 365 tm.addNewIncomingCall(handle, extras); 366 } 367 } catch (SecurityException se) { 368 // If we get a security exception, the most likely cause is: 369 // "This PhoneAccountHandle is not registered for this user" 370 // If this happens, then it means that for whatever reason the phone account which 371 // we are trying to use to add the new incoming call no longer exists in Telecom. 372 // This can happen if the handle of the phone account changes. The likely root 373 // cause of this would be a change in active SIM profile for an MVNO style carrier 374 // which aggregates multiple carriers together. 375 376 // We will log a list of the available handles ourselves here; PhoneAccountHandle 377 // oscures the ID in the toString. Rlog.pii will do a secure hash on userdebug 378 // builds so at least we could tell if the handle we tried is different from the one 379 // which we attempted to use. 380 List<PhoneAccountHandle> handles = tm.getCallCapablePhoneAccounts(); 381 String availableHandles = handles.stream() 382 .map(h -> "[" + h.getComponentName() + " " 383 + Rlog.pii(LOG_TAG, h.getId()) + "]") 384 .collect(Collectors.joining(",")); 385 String attemptedHandle = "[" + handle.getComponentName() + " " 386 + Rlog.pii(LOG_TAG, handle.getId()) + "]"; 387 Log.wtf(LOG_TAG, "sendIncomingCallIntent: failed to add new call " + connection 388 + " because the handle " + attemptedHandle 389 + " is not in the list of registered handles " + availableHandles 390 + " - call was rejected."); 391 392 // Since the phone account handle we're trying to use is not valid, we have no other 393 // recourse but to reject the incoming call. 394 try { 395 connection.hangup(); 396 } catch (CallStateException e) { 397 // connection already disconnected. Do nothing 398 } 399 } 400 } 401 } 402 403 /** 404 * Returns the PhoneAccount associated with this {@code PstnIncomingCallNotifier}'s phone. On a 405 * device with No SIM or in airplane mode, it can return an Emergency-only PhoneAccount. If no 406 * PhoneAccount is registered with telecom, return null. 407 * @return A valid PhoneAccountHandle that is registered to Telecom or null if there is none 408 * registered. 409 */ findCorrectPhoneAccountHandle()410 private PhoneAccountHandle findCorrectPhoneAccountHandle() { 411 TelecomAccountRegistry telecomAccountRegistry = TelecomAccountRegistry.getInstance(null); 412 // Check to see if a SIM PhoneAccountHandle Exists for the Call. 413 PhoneAccountHandle handle = telecomAccountRegistry.getPhoneAccountHandleForSubId( 414 mPhone.getSubId()); 415 if (telecomAccountRegistry.hasAccountEntryForPhoneAccount(handle)) { 416 return handle; 417 } 418 // The PhoneAccountHandle does not match any PhoneAccount registered in Telecom. 419 // This is only known to happen if there is no SIM card in the device and the device 420 // receives an MT call while in ECM. Use the Emergency PhoneAccount to receive the account 421 // if it exists. 422 PhoneAccountHandle emergencyHandle = 423 PhoneUtils.makePstnPhoneAccountHandleWithPrefix(mPhone, 424 "", true, mPhone.getUserHandle()); 425 if(telecomAccountRegistry.hasAccountEntryForPhoneAccount(emergencyHandle)) { 426 Log.i(this, "Receiving MT call in ECM. Using Emergency PhoneAccount Instead."); 427 return emergencyHandle; 428 } 429 Log.i(this, "PhoneAccount not found."); 430 return null; 431 } 432 433 /** 434 * Define cait.Connection := com.android.internal.telephony.Connection 435 * 436 * Given a previously unknown cait.Connection, check to see if it's likely a replacement for 437 * another cait.Connnection we already know about. If it is, then we silently swap it out 438 * underneath within the relevant {@link TelephonyConnection}, using 439 * {@link TelephonyConnection#setOriginalConnection(Connection)}, and return {@code true}. 440 * Otherwise, we return {@code false}. 441 */ maybeSwapAnyWithUnknownConnection(Connection unknown)442 private boolean maybeSwapAnyWithUnknownConnection(Connection unknown) { 443 if (!unknown.isIncoming()) { 444 TelecomAccountRegistry registry = TelecomAccountRegistry.getInstance(null); 445 if (registry != null) { 446 TelephonyConnectionService service = registry.getTelephonyConnectionService(); 447 if (service != null) { 448 for (android.telecom.Connection telephonyConnection : service 449 .getAllConnections()) { 450 if (telephonyConnection instanceof TelephonyConnection) { 451 if (maybeSwapWithUnknownConnection( 452 (TelephonyConnection) telephonyConnection, 453 unknown)) { 454 return true; 455 } 456 } 457 } 458 } 459 } 460 } 461 return false; 462 } 463 maybeSwapWithUnknownConnection( TelephonyConnection telephonyConnection, Connection unknown)464 private boolean maybeSwapWithUnknownConnection( 465 TelephonyConnection telephonyConnection, 466 Connection unknown) { 467 Connection original = telephonyConnection.getOriginalConnection(); 468 if (original != null && !original.isIncoming() 469 && Objects.equals(original.getAddress(), unknown.getAddress())) { 470 // If the new unknown connection is an external connection, don't swap one with an 471 // actual connection. This means a call got pulled away. We want the actual connection 472 // to disconnect. 473 if (unknown instanceof ImsExternalConnection && 474 !(telephonyConnection 475 .getOriginalConnection() instanceof ImsExternalConnection)) { 476 Log.i(this, "maybeSwapWithUnknownConnection - not swapping " 477 + "regular connection with external connection."); 478 return false; 479 } 480 481 Log.i(this, "maybeSwapWithUnknownConnection: swapping %s with %s", original, unknown); 482 telephonyConnection.setOriginalConnection(unknown); 483 484 // Do not call hang up if the original connection is an ImsExternalConnection, it is 485 // not supported. 486 if (original instanceof ImsExternalConnection) { 487 return true; 488 } 489 // If the connection we're replacing was a GSM or CDMA connection, call upon the call 490 // tracker to perform cleanup of calls. This ensures that we don't end up with a 491 // call stuck in the call tracker preventing other calls from being placed. 492 if (original.getCall() != null && original.getCall().getPhone() != null && 493 original.getCall().getPhone() instanceof GsmCdmaPhone) { 494 495 GsmCdmaPhone phone = (GsmCdmaPhone) original.getCall().getPhone(); 496 phone.getCallTracker().cleanupCalls(); 497 Log.i(this, "maybeSwapWithUnknownConnection - Invoking call tracker cleanup " 498 + "for connection: " + original); 499 } else if (original.getCall() != null && original.getCall().getPhone() != null 500 && original.getCall().getPhone() instanceof ImsPhone 501 && original instanceof ImsPhoneConnection) { 502 // We're replacing an existing ImsPhoneConnection; ensure we don't orhan the 503 // original connection. 504 ImsPhone phone = (ImsPhone) original.getCall().getPhone(); 505 ImsPhoneCallTracker tracker = (ImsPhoneCallTracker) phone.getCallTracker(); 506 tracker.cleanupAndRemoveConnection((ImsPhoneConnection) original); 507 Log.i(this, "maybeSwapWithUnknownConnection - cleanup/remove: " + original); 508 } 509 return true; 510 } 511 return false; 512 } 513 getPhoneIdAsString()514 private String getPhoneIdAsString() { 515 if (mPhone == null) { 516 return "-1"; 517 } 518 return String.valueOf(mPhone.getPhoneId()); 519 } 520 }