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