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