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