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