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