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