blob: 0c46a355abdbaa15f8c9bda51e8a2e91c77a18ab [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.telephony.imsphone;
import android.os.AsyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.telecom.PhoneAccountHandle;
import android.telecom.VideoProfile;
import android.telephony.ims.ImsCallProfile;
import android.telephony.ims.ImsExternalCallState;
import android.util.ArrayMap;
import android.util.Log;
import com.android.ims.ImsExternalCallStateListener;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Responsible for tracking external calls known to the system.
*/
public class ImsExternalCallTracker implements ImsPhoneCallTracker.PhoneStateListener {
/**
* Interface implemented by modules which are capable of notifying interested parties of new
* unknown connections, and changes to call state.
* This is used to break the dependency between {@link ImsExternalCallTracker} and
* {@link ImsPhone}.
*
* @hide
*/
public static interface ImsCallNotify {
/**
* Notifies that an unknown connection has been added.
* @param c The new unknown connection.
*/
void notifyUnknownConnection(Connection c);
/**
* Notifies of a change to call state.
*/
void notifyPreciseCallStateChanged();
}
/**
* Implements the {@link ImsExternalCallStateListener}, which is responsible for receiving
* external call state updates from the IMS framework.
*/
public class ExternalCallStateListener extends ImsExternalCallStateListener {
@Override
public void onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState) {
refreshExternalCallState(externalCallState);
}
}
/**
* Receives callbacks from {@link ImsExternalConnection}s when a call pull has been initiated.
*/
public class ExternalConnectionListener implements ImsExternalConnection.Listener {
@Override
public void onPullExternalCall(ImsExternalConnection connection) {
Log.d(TAG, "onPullExternalCall: connection = " + connection);
if (mCallPuller == null) {
Log.e(TAG, "onPullExternalCall : No call puller defined");
return;
}
mCallPuller.pullExternalCall(connection.getAddress(), connection.getVideoState(),
connection.getCallId());
}
}
public final static String TAG = "ImsExternalCallTracker";
private static final int EVENT_VIDEO_CAPABILITIES_CHANGED = 1;
/**
* Extra key used when informing telecom of a new external call using the
* {@link android.telecom.TelecomManager#addNewUnknownCall(PhoneAccountHandle, Bundle)} API.
* Used to ensure that when Telecom requests the {@link android.telecom.ConnectionService} to
* create the connection for the unknown call that we can determine which
* {@link ImsExternalConnection} in {@link #mExternalConnections} is the one being requested.
*/
public final static String EXTRA_IMS_EXTERNAL_CALL_ID =
"android.telephony.ImsExternalCallTracker.extra.EXTERNAL_CALL_ID";
/**
* Contains a list of the external connections known by the ImsExternalCallTracker. These are
* connections which originated from a dialog event package and reside on another device.
* Used in multi-endpoint (VoLTE for internet connected endpoints) scenarios.
*/
private Map<Integer, ImsExternalConnection> mExternalConnections =
new ArrayMap<>();
/**
* Tracks whether each external connection tracked in
* {@link #mExternalConnections} can be pulled, as reported by the latest dialog event package
* received from the network. We need to know this because the pull state of a call can be
* overridden based on the following factors:
* 1) An external video call cannot be pulled if the current device does not have video
* capability.
* 2) If the device has any active or held calls locally, no external calls may be pulled to
* the local device.
*/
private Map<Integer, Boolean> mExternalCallPullableState = new ArrayMap<>();
private final ImsPhone mPhone;
private final ImsCallNotify mCallStateNotifier;
private final ExternalCallStateListener mExternalCallStateListener;
private final ExternalConnectionListener mExternalConnectionListener =
new ExternalConnectionListener();
private ImsPullCall mCallPuller;
private boolean mIsVideoCapable;
private boolean mHasActiveCalls;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_VIDEO_CAPABILITIES_CHANGED:
handleVideoCapabilitiesChanged((AsyncResult) msg.obj);
break;
default:
break;
}
}
};
@VisibleForTesting
public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller,
ImsCallNotify callNotifier) {
mPhone = phone;
mCallStateNotifier = callNotifier;
mExternalCallStateListener = new ExternalCallStateListener();
mCallPuller = callPuller;
}
public ImsExternalCallTracker(ImsPhone phone) {
mPhone = phone;
mCallStateNotifier = new ImsCallNotify() {
@Override
public void notifyUnknownConnection(Connection c) {
mPhone.notifyUnknownConnection(c);
}
@Override
public void notifyPreciseCallStateChanged() {
mPhone.notifyPreciseCallStateChanged();
}
};
mExternalCallStateListener = new ExternalCallStateListener();
registerForNotifications();
}
/**
* Performs any cleanup required before the ImsExternalCallTracker is destroyed.
*/
public void tearDown() {
unregisterForNotifications();
}
/**
* Sets the implementation of {@link ImsPullCall} which is responsible for pulling calls.
*
* @param callPuller The pull call implementation.
*/
public void setCallPuller(ImsPullCall callPuller) {
mCallPuller = callPuller;
}
public ExternalCallStateListener getExternalCallStateListener() {
return mExternalCallStateListener;
}
/**
* Handles changes to the phone state as notified by the {@link ImsPhoneCallTracker}.
*
* @param oldState The previous phone state.
* @param newState The new phone state.
*/
@Override
public void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState) {
mHasActiveCalls = newState != PhoneConstants.State.IDLE;
Log.i(TAG, "onPhoneStateChanged : hasActiveCalls = " + mHasActiveCalls);
refreshCallPullState();
}
/**
* Registers for video capability changes.
*/
private void registerForNotifications() {
if (mPhone != null) {
Log.d(TAG, "Registering: " + mPhone);
mPhone.getDefaultPhone().registerForVideoCapabilityChanged(mHandler,
EVENT_VIDEO_CAPABILITIES_CHANGED, null);
}
}
/**
* Unregisters for video capability changes.
*/
private void unregisterForNotifications() {
if (mPhone != null) {
Log.d(TAG, "Unregistering: " + mPhone);
mPhone.getDefaultPhone().unregisterForVideoCapabilityChanged(mHandler);
}
}
/**
* Called when the IMS stack receives a new dialog event package. Triggers the creation and
* update of {@link ImsExternalConnection}s to represent the dialogs in the dialog event
* package data.
*
* @param externalCallStates the {@link ImsExternalCallState} information for the dialog event
* package.
*/
public void refreshExternalCallState(List<ImsExternalCallState> externalCallStates) {
Log.d(TAG, "refreshExternalCallState");
// Check to see if any call Ids are no longer present in the external call state. If they
// are, the calls are terminated and should be removed.
Iterator<Map.Entry<Integer, ImsExternalConnection>> connectionIterator =
mExternalConnections.entrySet().iterator();
boolean wasCallRemoved = false;
while (connectionIterator.hasNext()) {
Map.Entry<Integer, ImsExternalConnection> entry = connectionIterator.next();
int callId = entry.getKey().intValue();
if (!containsCallId(externalCallStates, callId)) {
ImsExternalConnection externalConnection = entry.getValue();
externalConnection.setTerminated();
externalConnection.removeListener(mExternalConnectionListener);
connectionIterator.remove();
wasCallRemoved = true;
}
}
// If one or more calls were removed, trigger a notification that will cause the
// TelephonyConnection instancse to refresh their state with Telecom.
if (wasCallRemoved) {
mCallStateNotifier.notifyPreciseCallStateChanged();
}
// Check for new calls, and updates to existing ones.
if (externalCallStates != null && !externalCallStates.isEmpty()) {
for (ImsExternalCallState callState : externalCallStates) {
if (!mExternalConnections.containsKey(callState.getCallId())) {
Log.d(TAG, "refreshExternalCallState: got = " + callState);
// If there is a new entry and it is already terminated, don't bother adding it to
// telecom.
if (callState.getCallState() != ImsExternalCallState.CALL_STATE_CONFIRMED) {
continue;
}
createExternalConnection(callState);
} else {
updateExistingConnection(mExternalConnections.get(callState.getCallId()),
callState);
}
}
}
}
/**
* Finds an external connection given a call Id.
*
* @param callId The call Id.
* @return The {@link Connection}, or {@code null} if no match found.
*/
public Connection getConnectionById(int callId) {
return mExternalConnections.get(callId);
}
/**
* Given an {@link ImsExternalCallState} instance obtained from a dialog event package,
* creates a new instance of {@link ImsExternalConnection} to represent the connection, and
* initiates the addition of the new call to Telecom as an unknown call.
*
* @param state External call state from a dialog event package.
*/
private void createExternalConnection(ImsExternalCallState state) {
Log.i(TAG, "createExternalConnection : state = " + state);
int videoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), videoState);
ImsExternalConnection connection = new ImsExternalConnection(mPhone,
state.getCallId(), /* Dialog event package call id */
state.getAddress() /* phone number */,
isCallPullPermitted);
connection.setVideoState(videoState);
connection.addListener(mExternalConnectionListener);
Log.d(TAG,
"createExternalConnection - pullable state : externalCallId = "
+ connection.getCallId()
+ " ; isPullable = " + isCallPullPermitted
+ " ; networkPullable = " + state.isCallPullable()
+ " ; isVideo = " + VideoProfile.isVideo(videoState)
+ " ; videoEnabled = " + mIsVideoCapable
+ " ; hasActiveCalls = " + mHasActiveCalls);
// Add to list of tracked connections.
mExternalConnections.put(connection.getCallId(), connection);
mExternalCallPullableState.put(connection.getCallId(), state.isCallPullable());
// Note: The notification of unknown connection is ultimately handled by
// PstnIncomingCallNotifier#addNewUnknownCall. That method will ensure that an extra is set
// containing the ImsExternalConnection#mCallId so that we have a means of reconciling which
// unknown call was added.
mCallStateNotifier.notifyUnknownConnection(connection);
}
/**
* Given an existing {@link ImsExternalConnection}, applies any changes found found in a
* {@link ImsExternalCallState} instance received from a dialog event package to the connection.
*
* @param connection The connection to apply changes to.
* @param state The new dialog state for the connection.
*/
private void updateExistingConnection(ImsExternalConnection connection,
ImsExternalCallState state) {
Log.i(TAG, "updateExistingConnection : state = " + state);
Call.State existingState = connection.getState();
Call.State newState = state.getCallState() == ImsExternalCallState.CALL_STATE_CONFIRMED ?
Call.State.ACTIVE : Call.State.DISCONNECTED;
if (existingState != newState) {
if (newState == Call.State.ACTIVE) {
connection.setActive();
} else {
connection.setTerminated();
connection.removeListener(mExternalConnectionListener);
mExternalConnections.remove(connection.getCallId());
mExternalCallPullableState.remove(connection.getCallId());
mCallStateNotifier.notifyPreciseCallStateChanged();
}
}
int newVideoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
if (newVideoState != connection.getVideoState()) {
connection.setVideoState(newVideoState);
}
mExternalCallPullableState.put(state.getCallId(), state.isCallPullable());
boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), newVideoState);
Log.d(TAG,
"updateExistingConnection - pullable state : externalCallId = " + connection
.getCallId()
+ " ; isPullable = " + isCallPullPermitted
+ " ; networkPullable = " + state.isCallPullable()
+ " ; isVideo = "
+ VideoProfile.isVideo(connection.getVideoState())
+ " ; videoEnabled = " + mIsVideoCapable
+ " ; hasActiveCalls = " + mHasActiveCalls);
connection.setIsPullable(isCallPullPermitted);
}
/**
* Update whether the external calls known can be pulled. Combines the last known network
* pullable state with local device conditions to determine if each call can be pulled.
*/
private void refreshCallPullState() {
Log.d(TAG, "refreshCallPullState");
for (ImsExternalConnection imsExternalConnection : mExternalConnections.values()) {
boolean isNetworkPullable =
mExternalCallPullableState.get(imsExternalConnection.getCallId())
.booleanValue();
boolean isCallPullPermitted =
isCallPullPermitted(isNetworkPullable, imsExternalConnection.getVideoState());
Log.d(TAG,
"refreshCallPullState : externalCallId = " + imsExternalConnection.getCallId()
+ " ; isPullable = " + isCallPullPermitted
+ " ; networkPullable = " + isNetworkPullable
+ " ; isVideo = "
+ VideoProfile.isVideo(imsExternalConnection.getVideoState())
+ " ; videoEnabled = " + mIsVideoCapable
+ " ; hasActiveCalls = " + mHasActiveCalls);
imsExternalConnection.setIsPullable(isCallPullPermitted);
}
}
/**
* Determines if a list of call states obtained from a dialog event package contacts an existing
* call Id.
*
* @param externalCallStates The dialog event package state information.
* @param callId The call Id.
* @return {@code true} if the state information contains the call Id, {@code false} otherwise.
*/
private boolean containsCallId(List<ImsExternalCallState> externalCallStates, int callId) {
if (externalCallStates == null) {
return false;
}
for (ImsExternalCallState state : externalCallStates) {
if (state.getCallId() == callId) {
return true;
}
}
return false;
}
/**
* Handles a change to the video capabilities reported by
* {@link Phone#notifyForVideoCapabilityChanged(boolean)}.
*
* @param ar The AsyncResult containing the new video capability of the device.
*/
private void handleVideoCapabilitiesChanged(AsyncResult ar) {
mIsVideoCapable = (Boolean) ar.result;
Log.i(TAG, "handleVideoCapabilitiesChanged : isVideoCapable = " + mIsVideoCapable);
// Refresh pullable state if video capability changed.
refreshCallPullState();
}
/**
* Determines whether an external call can be pulled based on the pullability state enforced
* by the network, as well as local device rules.
*
* @param isNetworkPullable {@code true} if the network indicates the call can be pulled,
* {@code false} otherwise.
* @param videoState the VideoState of the external call.
* @return {@code true} if the external call can be pulled, {@code false} otherwise.
*/
private boolean isCallPullPermitted(boolean isNetworkPullable, int videoState) {
if (VideoProfile.isVideo(videoState) && !mIsVideoCapable) {
// If the external call is a video call and the local device does not have video
// capability at this time, it cannot be pulled.
return false;
}
if (mHasActiveCalls) {
// If there are active calls on the local device, the call cannot be pulled.
return false;
}
return isNetworkPullable;
}
}