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.text.TextUtils; 29 30 import com.android.internal.telephony.Call; 31 import com.android.internal.telephony.CallStateException; 32 import com.android.internal.telephony.Connection; 33 import com.android.internal.telephony.GsmCdmaPhone; 34 import com.android.internal.telephony.Phone; 35 import com.android.internal.telephony.PhoneConstants; 36 import com.android.internal.telephony.cdma.CdmaCallWaitingNotification; 37 import com.android.internal.telephony.imsphone.ImsExternalCallTracker; 38 import com.android.internal.telephony.imsphone.ImsExternalConnection; 39 import com.android.internal.telephony.imsphone.ImsPhoneConnection; 40 import com.android.phone.NumberVerificationManager; 41 import com.android.phone.PhoneUtils; 42 43 import com.google.common.base.Preconditions; 44 45 import java.util.Objects; 46 47 /** 48 * Listens to incoming-call events from the associated phone object and notifies Telecom upon each 49 * occurence. One instance of these exists for each of the telephony-based call services. 50 */ 51 final class PstnIncomingCallNotifier { 52 /** New ringing connection event code. */ 53 private static final int EVENT_NEW_RINGING_CONNECTION = 100; 54 private static final int EVENT_CDMA_CALL_WAITING = 101; 55 private static final int EVENT_UNKNOWN_CONNECTION = 102; 56 57 /** The phone object to listen to. */ 58 private final Phone mPhone; 59 60 /** 61 * Used to listen to events from {@link #mPhone}. 62 */ 63 private final Handler mHandler = new Handler() { 64 @Override 65 public void handleMessage(Message msg) { 66 switch(msg.what) { 67 case EVENT_NEW_RINGING_CONNECTION: 68 handleNewRingingConnection((AsyncResult) msg.obj); 69 break; 70 case EVENT_CDMA_CALL_WAITING: 71 handleCdmaCallWaiting((AsyncResult) msg.obj); 72 break; 73 case EVENT_UNKNOWN_CONNECTION: 74 handleNewUnknownConnection((AsyncResult) msg.obj); 75 break; 76 default: 77 break; 78 } 79 } 80 }; 81 82 /** 83 * Persists the specified parameters and starts listening to phone events. 84 * 85 * @param phone The phone object for listening to incoming calls. 86 */ PstnIncomingCallNotifier(Phone phone)87 PstnIncomingCallNotifier(Phone phone) { 88 Preconditions.checkNotNull(phone); 89 90 mPhone = phone; 91 92 registerForNotifications(); 93 } 94 teardown()95 void teardown() { 96 unregisterForNotifications(); 97 } 98 99 /** 100 * Register for notifications from the base phone. 101 */ registerForNotifications()102 private void registerForNotifications() { 103 if (mPhone != null) { 104 Log.i(this, "Registering: %s", mPhone); 105 mPhone.registerForNewRingingConnection(mHandler, EVENT_NEW_RINGING_CONNECTION, null); 106 mPhone.registerForCallWaiting(mHandler, EVENT_CDMA_CALL_WAITING, null); 107 mPhone.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION, null); 108 } 109 } 110 unregisterForNotifications()111 private void unregisterForNotifications() { 112 if (mPhone != null) { 113 Log.i(this, "Unregistering: %s", mPhone); 114 mPhone.unregisterForNewRingingConnection(mHandler); 115 mPhone.unregisterForCallWaiting(mHandler); 116 mPhone.unregisterForUnknownConnection(mHandler); 117 } 118 } 119 120 /** 121 * Verifies the incoming call and triggers sending the incoming-call intent to Telecom. 122 * 123 * @param asyncResult The result object from the new ringing event. 124 */ handleNewRingingConnection(AsyncResult asyncResult)125 private void handleNewRingingConnection(AsyncResult asyncResult) { 126 Log.d(this, "handleNewRingingConnection"); 127 Connection connection = (Connection) asyncResult.result; 128 if (connection != null) { 129 Call call = connection.getCall(); 130 // Check if we have a pending number verification request. 131 if (connection.getAddress() != null) { 132 if (NumberVerificationManager.getInstance() 133 .checkIncomingCall(connection.getAddress())) { 134 // Disconnect the call if it matches 135 try { 136 connection.hangup(); 137 } catch (CallStateException e) { 138 Log.e(this, e, "Error hanging up potential number verification call"); 139 } 140 return; 141 } 142 } 143 144 // Final verification of the ringing state before sending the intent to Telecom. 145 if (call != null && call.getState().isRinging()) { 146 sendIncomingCallIntent(connection); 147 } 148 } 149 } 150 handleCdmaCallWaiting(AsyncResult asyncResult)151 private void handleCdmaCallWaiting(AsyncResult asyncResult) { 152 Log.d(this, "handleCdmaCallWaiting"); 153 CdmaCallWaitingNotification ccwi = (CdmaCallWaitingNotification) asyncResult.result; 154 Call call = mPhone.getRingingCall(); 155 if (call.getState() == Call.State.WAITING) { 156 Connection connection = call.getLatestConnection(); 157 if (connection != null) { 158 String number = connection.getAddress(); 159 int presentation = connection.getNumberPresentation(); 160 161 if (presentation != PhoneConstants.PRESENTATION_ALLOWED 162 && presentation == ccwi.numberPresentation) { 163 // Presentation of number not allowed, but the presentation of the Connection 164 // and the call waiting presentation match. 165 Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; " 166 + "presentation = %d", presentation); 167 sendIncomingCallIntent(connection); 168 } else if (!TextUtils.isEmpty(number) && Objects.equals(number, ccwi.number)) { 169 // Presentation of the number is allowed, so we ensure the number matches the 170 // one in the call waiting information. 171 Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; " 172 + "number = %s", Log.pii(number)); 173 sendIncomingCallIntent(connection); 174 } else { 175 Log.w(this, "handleCdmaCallWaiting: presentation or number do not match, not" 176 + " informing telecom of call: %s", ccwi); 177 } 178 } 179 } 180 } 181 handleNewUnknownConnection(AsyncResult asyncResult)182 private void handleNewUnknownConnection(AsyncResult asyncResult) { 183 Log.i(this, "handleNewUnknownConnection"); 184 if (!(asyncResult.result instanceof Connection)) { 185 Log.w(this, "handleNewUnknownConnection called with non-Connection object"); 186 return; 187 } 188 Connection connection = (Connection) asyncResult.result; 189 if (connection != null) { 190 // Because there is a handler between telephony and here, it causes this action to be 191 // asynchronous which means that the call can switch to DISCONNECTED by the time it gets 192 // to this code. Check here to ensure we are not adding a disconnected or IDLE call. 193 Call.State state = connection.getState(); 194 if (state == Call.State.DISCONNECTED || state == Call.State.IDLE) { 195 Log.i(this, "Skipping new unknown connection because it is idle. " + connection); 196 return; 197 } 198 199 Call call = connection.getCall(); 200 if (call != null && call.getState().isAlive()) { 201 addNewUnknownCall(connection); 202 } 203 } 204 } 205 addNewUnknownCall(Connection connection)206 private void addNewUnknownCall(Connection connection) { 207 Log.i(this, "addNewUnknownCall, connection is: %s", connection); 208 209 if (!maybeSwapAnyWithUnknownConnection(connection)) { 210 Log.i(this, "determined new connection is: %s", connection); 211 Bundle extras = new Bundle(); 212 if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && 213 !TextUtils.isEmpty(connection.getAddress())) { 214 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null); 215 extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, uri); 216 } 217 // ImsExternalConnections are keyed by a unique mCallId; include this as an extra on 218 // the call to addNewUknownCall in Telecom. This way when the request comes back to the 219 // TelephonyConnectionService, we will be able to determine which unknown connection is 220 // being added. 221 if (connection instanceof ImsExternalConnection) { 222 ImsExternalConnection externalConnection = (ImsExternalConnection) connection; 223 extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID, 224 externalConnection.getCallId()); 225 } 226 227 // Specifies the time the call was added. This is used by the dialer for analytics. 228 extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS, 229 SystemClock.elapsedRealtime()); 230 231 PhoneAccountHandle handle = findCorrectPhoneAccountHandle(); 232 if (handle == null) { 233 try { 234 connection.hangup(); 235 } catch (CallStateException e) { 236 // connection already disconnected. Do nothing 237 } 238 } else { 239 TelecomManager.from(mPhone.getContext()).addNewUnknownCall(handle, extras); 240 } 241 } else { 242 Log.i(this, "swapped an old connection, new one is: %s", connection); 243 } 244 } 245 246 /** 247 * Sends the incoming call intent to telecom. 248 */ sendIncomingCallIntent(Connection connection)249 private void sendIncomingCallIntent(Connection connection) { 250 Bundle extras = new Bundle(); 251 if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && 252 !TextUtils.isEmpty(connection.getAddress())) { 253 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null); 254 extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri); 255 } 256 257 // Specifies the time the call was added. This is used by the dialer for analytics. 258 extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS, 259 SystemClock.elapsedRealtime()); 260 261 if (connection.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 262 if (((ImsPhoneConnection) connection).isRttEnabledForCall()) { 263 extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true); 264 } 265 } 266 PhoneAccountHandle handle = findCorrectPhoneAccountHandle(); 267 if (handle == null) { 268 try { 269 connection.hangup(); 270 } catch (CallStateException e) { 271 // connection already disconnected. Do nothing 272 } 273 } else { 274 TelecomManager.from(mPhone.getContext()).addNewIncomingCall(handle, extras); 275 } 276 } 277 278 /** 279 * Returns the PhoneAccount associated with this {@code PstnIncomingCallNotifier}'s phone. On a 280 * device with No SIM or in airplane mode, it can return an Emergency-only PhoneAccount. If no 281 * PhoneAccount is registered with telecom, return null. 282 * @return A valid PhoneAccountHandle that is registered to Telecom or null if there is none 283 * registered. 284 */ findCorrectPhoneAccountHandle()285 private PhoneAccountHandle findCorrectPhoneAccountHandle() { 286 TelecomAccountRegistry telecomAccountRegistry = TelecomAccountRegistry.getInstance(null); 287 // Check to see if a the SIM PhoneAccountHandle Exists for the Call. 288 PhoneAccountHandle handle = PhoneUtils.makePstnPhoneAccountHandle(mPhone); 289 if (telecomAccountRegistry.hasAccountEntryForPhoneAccount(handle)) { 290 return handle; 291 } 292 // The PhoneAccountHandle does not match any PhoneAccount registered in Telecom. 293 // This is only known to happen if there is no SIM card in the device and the device 294 // receives an MT call while in ECM. Use the Emergency PhoneAccount to receive the account 295 // if it exists. 296 PhoneAccountHandle emergencyHandle = 297 PhoneUtils.makePstnPhoneAccountHandleWithPrefix(mPhone, "", true); 298 if(telecomAccountRegistry.hasAccountEntryForPhoneAccount(emergencyHandle)) { 299 Log.i(this, "Receiving MT call in ECM. Using Emergency PhoneAccount Instead."); 300 return emergencyHandle; 301 } 302 Log.w(this, "PhoneAccount not found."); 303 return null; 304 } 305 306 /** 307 * Define cait.Connection := com.android.internal.telephony.Connection 308 * 309 * Given a previously unknown cait.Connection, check to see if it's likely a replacement for 310 * another cait.Connnection we already know about. If it is, then we silently swap it out 311 * underneath within the relevant {@link TelephonyConnection}, using 312 * {@link TelephonyConnection#setOriginalConnection(Connection)}, and return {@code true}. 313 * Otherwise, we return {@code false}. 314 */ maybeSwapAnyWithUnknownConnection(Connection unknown)315 private boolean maybeSwapAnyWithUnknownConnection(Connection unknown) { 316 if (!unknown.isIncoming()) { 317 TelecomAccountRegistry registry = TelecomAccountRegistry.getInstance(null); 318 if (registry != null) { 319 TelephonyConnectionService service = registry.getTelephonyConnectionService(); 320 if (service != null) { 321 for (android.telecom.Connection telephonyConnection : service 322 .getAllConnections()) { 323 if (telephonyConnection instanceof TelephonyConnection) { 324 if (maybeSwapWithUnknownConnection( 325 (TelephonyConnection) telephonyConnection, 326 unknown)) { 327 return true; 328 } 329 } 330 } 331 } 332 } 333 } 334 return false; 335 } 336 maybeSwapWithUnknownConnection( TelephonyConnection telephonyConnection, Connection unknown)337 private boolean maybeSwapWithUnknownConnection( 338 TelephonyConnection telephonyConnection, 339 Connection unknown) { 340 Connection original = telephonyConnection.getOriginalConnection(); 341 if (original != null && !original.isIncoming() 342 && Objects.equals(original.getAddress(), unknown.getAddress())) { 343 // If the new unknown connection is an external connection, don't swap one with an 344 // actual connection. This means a call got pulled away. We want the actual connection 345 // to disconnect. 346 if (unknown instanceof ImsExternalConnection && 347 !(telephonyConnection 348 .getOriginalConnection() instanceof ImsExternalConnection)) { 349 Log.v(this, "maybeSwapWithUnknownConnection - not swapping regular connection " + 350 "with external connection."); 351 return false; 352 } 353 354 telephonyConnection.setOriginalConnection(unknown); 355 356 // Do not call hang up if the original connection is an ImsExternalConnection, it is 357 // not supported. 358 if (original instanceof ImsExternalConnection) { 359 return true; 360 } 361 // If the connection we're replacing was a GSM or CDMA connection, call upon the call 362 // tracker to perform cleanup of calls. This ensures that we don't end up with a 363 // call stuck in the call tracker preventing other calls from being placed. 364 if (original.getCall() != null && original.getCall().getPhone() != null && 365 original.getCall().getPhone() instanceof GsmCdmaPhone) { 366 367 GsmCdmaPhone phone = (GsmCdmaPhone) original.getCall().getPhone(); 368 phone.getCallTracker().cleanupCalls(); 369 Log.i(this, "maybeSwapWithUnknownConnection - Invoking call tracker cleanup " 370 + "for connection: " + original); 371 } 372 return true; 373 } 374 return false; 375 } 376 } 377