| /* |
| * 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; |
| } |
| } |