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