1 /* 2 * Copyright (C) 2016 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.internal.telephony.imsphone; 18 19 import android.os.AsyncResult; 20 import android.os.Bundle; 21 import android.os.Handler; 22 import android.os.Message; 23 import android.telecom.PhoneAccountHandle; 24 import android.telecom.VideoProfile; 25 import android.telephony.ims.ImsCallProfile; 26 import android.telephony.ims.ImsExternalCallState; 27 import android.util.ArrayMap; 28 import android.util.Log; 29 30 import com.android.ims.ImsExternalCallStateListener; 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.telephony.Call; 33 import com.android.internal.telephony.Connection; 34 import com.android.internal.telephony.Phone; 35 import com.android.internal.telephony.PhoneConstants; 36 37 import java.util.Iterator; 38 import java.util.List; 39 import java.util.Map; 40 41 /** 42 * Responsible for tracking external calls known to the system. 43 */ 44 public class ImsExternalCallTracker implements ImsPhoneCallTracker.PhoneStateListener { 45 46 /** 47 * Interface implemented by modules which are capable of notifying interested parties of new 48 * unknown connections, and changes to call state. 49 * This is used to break the dependency between {@link ImsExternalCallTracker} and 50 * {@link ImsPhone}. 51 * 52 * @hide 53 */ 54 public static interface ImsCallNotify { 55 /** 56 * Notifies that an unknown connection has been added. 57 * @param c The new unknown connection. 58 */ notifyUnknownConnection(Connection c)59 void notifyUnknownConnection(Connection c); 60 61 /** 62 * Notifies of a change to call state. 63 */ notifyPreciseCallStateChanged()64 void notifyPreciseCallStateChanged(); 65 } 66 67 68 /** 69 * Implements the {@link ImsExternalCallStateListener}, which is responsible for receiving 70 * external call state updates from the IMS framework. 71 */ 72 public class ExternalCallStateListener extends ImsExternalCallStateListener { 73 @Override onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState)74 public void onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState) { 75 refreshExternalCallState(externalCallState); 76 } 77 } 78 79 /** 80 * Receives callbacks from {@link ImsExternalConnection}s when a call pull has been initiated. 81 */ 82 public class ExternalConnectionListener implements ImsExternalConnection.Listener { 83 @Override onPullExternalCall(ImsExternalConnection connection)84 public void onPullExternalCall(ImsExternalConnection connection) { 85 Log.d(TAG, "onPullExternalCall: connection = " + connection); 86 if (mCallPuller == null) { 87 Log.e(TAG, "onPullExternalCall : No call puller defined"); 88 return; 89 } 90 mCallPuller.pullExternalCall(connection.getAddress(), connection.getVideoState(), 91 connection.getCallId()); 92 } 93 } 94 95 public final static String TAG = "ImsExternalCallTracker"; 96 97 private static final int EVENT_VIDEO_CAPABILITIES_CHANGED = 1; 98 99 /** 100 * Extra key used when informing telecom of a new external call using the 101 * {@link android.telecom.TelecomManager#addNewUnknownCall(PhoneAccountHandle, Bundle)} API. 102 * Used to ensure that when Telecom requests the {@link android.telecom.ConnectionService} to 103 * create the connection for the unknown call that we can determine which 104 * {@link ImsExternalConnection} in {@link #mExternalConnections} is the one being requested. 105 */ 106 public final static String EXTRA_IMS_EXTERNAL_CALL_ID = 107 "android.telephony.ImsExternalCallTracker.extra.EXTERNAL_CALL_ID"; 108 109 /** 110 * Contains a list of the external connections known by the ImsExternalCallTracker. These are 111 * connections which originated from a dialog event package and reside on another device. 112 * Used in multi-endpoint (VoLTE for internet connected endpoints) scenarios. 113 */ 114 private Map<Integer, ImsExternalConnection> mExternalConnections = 115 new ArrayMap<>(); 116 117 /** 118 * Tracks whether each external connection tracked in 119 * {@link #mExternalConnections} can be pulled, as reported by the latest dialog event package 120 * received from the network. We need to know this because the pull state of a call can be 121 * overridden based on the following factors: 122 * 1) An external video call cannot be pulled if the current device does not have video 123 * capability. 124 * 2) If the device has any active or held calls locally, no external calls may be pulled to 125 * the local device. 126 */ 127 private Map<Integer, Boolean> mExternalCallPullableState = new ArrayMap<>(); 128 private final ImsPhone mPhone; 129 private final ImsCallNotify mCallStateNotifier; 130 private final ExternalCallStateListener mExternalCallStateListener; 131 private final ExternalConnectionListener mExternalConnectionListener = 132 new ExternalConnectionListener(); 133 private ImsPullCall mCallPuller; 134 private boolean mIsVideoCapable; 135 private boolean mHasActiveCalls; 136 137 private final Handler mHandler = new Handler() { 138 @Override 139 public void handleMessage(Message msg) { 140 switch (msg.what) { 141 case EVENT_VIDEO_CAPABILITIES_CHANGED: 142 handleVideoCapabilitiesChanged((AsyncResult) msg.obj); 143 break; 144 default: 145 break; 146 } 147 } 148 }; 149 150 @VisibleForTesting ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller, ImsCallNotify callNotifier)151 public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller, 152 ImsCallNotify callNotifier) { 153 154 mPhone = phone; 155 mCallStateNotifier = callNotifier; 156 mExternalCallStateListener = new ExternalCallStateListener(); 157 mCallPuller = callPuller; 158 } 159 ImsExternalCallTracker(ImsPhone phone)160 public ImsExternalCallTracker(ImsPhone phone) { 161 mPhone = phone; 162 mCallStateNotifier = new ImsCallNotify() { 163 @Override 164 public void notifyUnknownConnection(Connection c) { 165 mPhone.notifyUnknownConnection(c); 166 } 167 168 @Override 169 public void notifyPreciseCallStateChanged() { 170 mPhone.notifyPreciseCallStateChanged(); 171 } 172 }; 173 mExternalCallStateListener = new ExternalCallStateListener(); 174 registerForNotifications(); 175 } 176 177 /** 178 * Performs any cleanup required before the ImsExternalCallTracker is destroyed. 179 */ tearDown()180 public void tearDown() { 181 unregisterForNotifications(); 182 } 183 184 /** 185 * Sets the implementation of {@link ImsPullCall} which is responsible for pulling calls. 186 * 187 * @param callPuller The pull call implementation. 188 */ setCallPuller(ImsPullCall callPuller)189 public void setCallPuller(ImsPullCall callPuller) { 190 mCallPuller = callPuller; 191 } 192 getExternalCallStateListener()193 public ExternalCallStateListener getExternalCallStateListener() { 194 return mExternalCallStateListener; 195 } 196 197 /** 198 * Handles changes to the phone state as notified by the {@link ImsPhoneCallTracker}. 199 * 200 * @param oldState The previous phone state. 201 * @param newState The new phone state. 202 */ 203 @Override onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState)204 public void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState) { 205 mHasActiveCalls = newState != PhoneConstants.State.IDLE; 206 Log.i(TAG, "onPhoneStateChanged : hasActiveCalls = " + mHasActiveCalls); 207 208 refreshCallPullState(); 209 } 210 211 /** 212 * Registers for video capability changes. 213 */ registerForNotifications()214 private void registerForNotifications() { 215 if (mPhone != null) { 216 Log.d(TAG, "Registering: " + mPhone); 217 mPhone.getDefaultPhone().registerForVideoCapabilityChanged(mHandler, 218 EVENT_VIDEO_CAPABILITIES_CHANGED, null); 219 } 220 } 221 222 /** 223 * Unregisters for video capability changes. 224 */ unregisterForNotifications()225 private void unregisterForNotifications() { 226 if (mPhone != null) { 227 Log.d(TAG, "Unregistering: " + mPhone); 228 mPhone.getDefaultPhone().unregisterForVideoCapabilityChanged(mHandler); 229 } 230 } 231 232 233 /** 234 * Called when the IMS stack receives a new dialog event package. Triggers the creation and 235 * update of {@link ImsExternalConnection}s to represent the dialogs in the dialog event 236 * package data. 237 * 238 * @param externalCallStates the {@link ImsExternalCallState} information for the dialog event 239 * package. 240 */ refreshExternalCallState(List<ImsExternalCallState> externalCallStates)241 public void refreshExternalCallState(List<ImsExternalCallState> externalCallStates) { 242 Log.d(TAG, "refreshExternalCallState"); 243 244 // Check to see if any call Ids are no longer present in the external call state. If they 245 // are, the calls are terminated and should be removed. 246 Iterator<Map.Entry<Integer, ImsExternalConnection>> connectionIterator = 247 mExternalConnections.entrySet().iterator(); 248 boolean wasCallRemoved = false; 249 while (connectionIterator.hasNext()) { 250 Map.Entry<Integer, ImsExternalConnection> entry = connectionIterator.next(); 251 int callId = entry.getKey().intValue(); 252 253 if (!containsCallId(externalCallStates, callId)) { 254 ImsExternalConnection externalConnection = entry.getValue(); 255 externalConnection.setTerminated(); 256 externalConnection.removeListener(mExternalConnectionListener); 257 connectionIterator.remove(); 258 wasCallRemoved = true; 259 } 260 } 261 // If one or more calls were removed, trigger a notification that will cause the 262 // TelephonyConnection instancse to refresh their state with Telecom. 263 if (wasCallRemoved) { 264 mCallStateNotifier.notifyPreciseCallStateChanged(); 265 } 266 267 // Check for new calls, and updates to existing ones. 268 if (externalCallStates != null && !externalCallStates.isEmpty()) { 269 for (ImsExternalCallState callState : externalCallStates) { 270 if (!mExternalConnections.containsKey(callState.getCallId())) { 271 Log.d(TAG, "refreshExternalCallState: got = " + callState); 272 // If there is a new entry and it is already terminated, don't bother adding it to 273 // telecom. 274 if (callState.getCallState() != ImsExternalCallState.CALL_STATE_CONFIRMED) { 275 continue; 276 } 277 createExternalConnection(callState); 278 } else { 279 updateExistingConnection(mExternalConnections.get(callState.getCallId()), 280 callState); 281 } 282 } 283 } 284 } 285 286 /** 287 * Finds an external connection given a call Id. 288 * 289 * @param callId The call Id. 290 * @return The {@link Connection}, or {@code null} if no match found. 291 */ getConnectionById(int callId)292 public Connection getConnectionById(int callId) { 293 return mExternalConnections.get(callId); 294 } 295 296 /** 297 * Given an {@link ImsExternalCallState} instance obtained from a dialog event package, 298 * creates a new instance of {@link ImsExternalConnection} to represent the connection, and 299 * initiates the addition of the new call to Telecom as an unknown call. 300 * 301 * @param state External call state from a dialog event package. 302 */ createExternalConnection(ImsExternalCallState state)303 private void createExternalConnection(ImsExternalCallState state) { 304 Log.i(TAG, "createExternalConnection : state = " + state); 305 306 int videoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType()); 307 308 boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), videoState); 309 ImsExternalConnection connection = new ImsExternalConnection(mPhone, 310 state.getCallId(), /* Dialog event package call id */ 311 state.getAddress() /* phone number */, 312 isCallPullPermitted); 313 connection.setVideoState(videoState); 314 connection.addListener(mExternalConnectionListener); 315 316 Log.d(TAG, 317 "createExternalConnection - pullable state : externalCallId = " 318 + connection.getCallId() 319 + " ; isPullable = " + isCallPullPermitted 320 + " ; networkPullable = " + state.isCallPullable() 321 + " ; isVideo = " + VideoProfile.isVideo(videoState) 322 + " ; videoEnabled = " + mIsVideoCapable 323 + " ; hasActiveCalls = " + mHasActiveCalls); 324 325 // Add to list of tracked connections. 326 mExternalConnections.put(connection.getCallId(), connection); 327 mExternalCallPullableState.put(connection.getCallId(), state.isCallPullable()); 328 329 // Note: The notification of unknown connection is ultimately handled by 330 // PstnIncomingCallNotifier#addNewUnknownCall. That method will ensure that an extra is set 331 // containing the ImsExternalConnection#mCallId so that we have a means of reconciling which 332 // unknown call was added. 333 mCallStateNotifier.notifyUnknownConnection(connection); 334 } 335 336 /** 337 * Given an existing {@link ImsExternalConnection}, applies any changes found found in a 338 * {@link ImsExternalCallState} instance received from a dialog event package to the connection. 339 * 340 * @param connection The connection to apply changes to. 341 * @param state The new dialog state for the connection. 342 */ updateExistingConnection(ImsExternalConnection connection, ImsExternalCallState state)343 private void updateExistingConnection(ImsExternalConnection connection, 344 ImsExternalCallState state) { 345 346 Log.i(TAG, "updateExistingConnection : state = " + state); 347 Call.State existingState = connection.getState(); 348 Call.State newState = state.getCallState() == ImsExternalCallState.CALL_STATE_CONFIRMED ? 349 Call.State.ACTIVE : Call.State.DISCONNECTED; 350 351 if (existingState != newState) { 352 if (newState == Call.State.ACTIVE) { 353 connection.setActive(); 354 } else { 355 connection.setTerminated(); 356 connection.removeListener(mExternalConnectionListener); 357 mExternalConnections.remove(connection.getCallId()); 358 mExternalCallPullableState.remove(connection.getCallId()); 359 mCallStateNotifier.notifyPreciseCallStateChanged(); 360 } 361 } 362 363 int newVideoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType()); 364 if (newVideoState != connection.getVideoState()) { 365 connection.setVideoState(newVideoState); 366 } 367 368 mExternalCallPullableState.put(state.getCallId(), state.isCallPullable()); 369 boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), newVideoState); 370 Log.d(TAG, 371 "updateExistingConnection - pullable state : externalCallId = " + connection 372 .getCallId() 373 + " ; isPullable = " + isCallPullPermitted 374 + " ; networkPullable = " + state.isCallPullable() 375 + " ; isVideo = " 376 + VideoProfile.isVideo(connection.getVideoState()) 377 + " ; videoEnabled = " + mIsVideoCapable 378 + " ; hasActiveCalls = " + mHasActiveCalls); 379 380 connection.setIsPullable(isCallPullPermitted); 381 } 382 383 /** 384 * Update whether the external calls known can be pulled. Combines the last known network 385 * pullable state with local device conditions to determine if each call can be pulled. 386 */ refreshCallPullState()387 private void refreshCallPullState() { 388 Log.d(TAG, "refreshCallPullState"); 389 390 for (ImsExternalConnection imsExternalConnection : mExternalConnections.values()) { 391 boolean isNetworkPullable = 392 mExternalCallPullableState.get(imsExternalConnection.getCallId()) 393 .booleanValue(); 394 boolean isCallPullPermitted = 395 isCallPullPermitted(isNetworkPullable, imsExternalConnection.getVideoState()); 396 Log.d(TAG, 397 "refreshCallPullState : externalCallId = " + imsExternalConnection.getCallId() 398 + " ; isPullable = " + isCallPullPermitted 399 + " ; networkPullable = " + isNetworkPullable 400 + " ; isVideo = " 401 + VideoProfile.isVideo(imsExternalConnection.getVideoState()) 402 + " ; videoEnabled = " + mIsVideoCapable 403 + " ; hasActiveCalls = " + mHasActiveCalls); 404 imsExternalConnection.setIsPullable(isCallPullPermitted); 405 } 406 } 407 408 /** 409 * Determines if a list of call states obtained from a dialog event package contacts an existing 410 * call Id. 411 * 412 * @param externalCallStates The dialog event package state information. 413 * @param callId The call Id. 414 * @return {@code true} if the state information contains the call Id, {@code false} otherwise. 415 */ containsCallId(List<ImsExternalCallState> externalCallStates, int callId)416 private boolean containsCallId(List<ImsExternalCallState> externalCallStates, int callId) { 417 if (externalCallStates == null) { 418 return false; 419 } 420 421 for (ImsExternalCallState state : externalCallStates) { 422 if (state.getCallId() == callId) { 423 return true; 424 } 425 } 426 427 return false; 428 } 429 430 /** 431 * Handles a change to the video capabilities reported by 432 * {@link Phone#notifyForVideoCapabilityChanged(boolean)}. 433 * 434 * @param ar The AsyncResult containing the new video capability of the device. 435 */ handleVideoCapabilitiesChanged(AsyncResult ar)436 private void handleVideoCapabilitiesChanged(AsyncResult ar) { 437 mIsVideoCapable = (Boolean) ar.result; 438 Log.i(TAG, "handleVideoCapabilitiesChanged : isVideoCapable = " + mIsVideoCapable); 439 440 // Refresh pullable state if video capability changed. 441 refreshCallPullState(); 442 } 443 444 /** 445 * Determines whether an external call can be pulled based on the pullability state enforced 446 * by the network, as well as local device rules. 447 * 448 * @param isNetworkPullable {@code true} if the network indicates the call can be pulled, 449 * {@code false} otherwise. 450 * @param videoState the VideoState of the external call. 451 * @return {@code true} if the external call can be pulled, {@code false} otherwise. 452 */ isCallPullPermitted(boolean isNetworkPullable, int videoState)453 private boolean isCallPullPermitted(boolean isNetworkPullable, int videoState) { 454 if (VideoProfile.isVideo(videoState) && !mIsVideoCapable) { 455 // If the external call is a video call and the local device does not have video 456 // capability at this time, it cannot be pulled. 457 return false; 458 } 459 460 if (mHasActiveCalls) { 461 // If there are active calls on the local device, the call cannot be pulled. 462 return false; 463 } 464 465 return isNetworkPullable; 466 } 467 } 468