| /* |
| * Copyright (C) 2014 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.services.telephony; |
| |
| import com.android.ims.ImsReasonInfo; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.PhoneConstants; |
| import com.android.phone.PhoneUtils; |
| |
| import android.telecom.Conference; |
| import android.telecom.Connection; |
| import android.telecom.ConnectionService; |
| import android.telecom.DisconnectCause; |
| import android.telecom.Conferenceable; |
| import android.telecom.PhoneAccountHandle; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Manages conferences for IMS connections. |
| */ |
| public class ImsConferenceController { |
| |
| /** |
| * Conference listener; used to receive notification when a conference has been disconnected. |
| */ |
| private final Conference.Listener mConferenceListener = new Conference.Listener() { |
| @Override |
| public void onDestroyed(Conference conference) { |
| if (Log.VERBOSE) { |
| Log.v(ImsConferenceController.class, "onDestroyed: %s", conference); |
| } |
| |
| mImsConferences.remove(conference); |
| } |
| }; |
| |
| /** |
| * Ims conference controller connection listener. Used to respond to changes in state of the |
| * Telephony connections the controller is aware of. |
| */ |
| private final Connection.Listener mConnectionListener = new Connection.Listener() { |
| @Override |
| public void onStateChanged(Connection c, int state) { |
| Log.v(this, "onStateChanged: %s", Log.pii(c.getAddress())); |
| recalculate(); |
| } |
| |
| @Override |
| public void onDisconnected(Connection c, DisconnectCause disconnectCause) { |
| Log.v(this, "onDisconnected: %s", Log.pii(c.getAddress())); |
| recalculate(); |
| } |
| |
| @Override |
| public void onDestroyed(Connection connection) { |
| remove(connection); |
| } |
| |
| @Override |
| public void onConferenceStarted() { |
| Log.v(this, "onConferenceStarted"); |
| recalculate(); |
| } |
| |
| @Override |
| public void onConferenceSupportedChanged(Connection c, boolean isConferenceSupported) { |
| Log.v(this, "onConferenceSupportedChanged"); |
| recalculate(); |
| } |
| }; |
| |
| /** |
| * The current {@link ConnectionService}. |
| */ |
| private final TelephonyConnectionServiceProxy mConnectionService; |
| |
| /** |
| * List of known {@link TelephonyConnection}s. |
| */ |
| private final ArrayList<TelephonyConnection> mTelephonyConnections = new ArrayList<>(); |
| |
| /** |
| * List of known {@link ImsConference}s. Realistically there will only ever be a single |
| * concurrent IMS conference. |
| */ |
| private final ArrayList<ImsConference> mImsConferences = new ArrayList<>(1); |
| |
| private TelecomAccountRegistry mTelecomAccountRegistry; |
| |
| /** |
| * Creates a new instance of the Ims conference controller. |
| * |
| * @param connectionService The current connection service. |
| */ |
| public ImsConferenceController(TelecomAccountRegistry telecomAccountRegistry, |
| TelephonyConnectionServiceProxy connectionService) { |
| mConnectionService = connectionService; |
| mTelecomAccountRegistry = telecomAccountRegistry; |
| } |
| |
| /** |
| * Adds a new connection to the IMS conference controller. |
| * |
| * @param connection |
| */ |
| void add(TelephonyConnection connection) { |
| // DO NOT add external calls; we don't want to consider them as a potential conference |
| // member. |
| if ((connection.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) == |
| Connection.PROPERTY_IS_EXTERNAL_CALL) { |
| return; |
| } |
| |
| if (mTelephonyConnections.contains(connection)) { |
| // Adding a duplicate realistically shouldn't happen. |
| Log.w(this, "add - connection already tracked; connection=%s", connection); |
| return; |
| } |
| |
| // Note: Wrap in Log.VERBOSE to avoid calling connection.toString if we are not going to be |
| // outputting the value. |
| if (Log.VERBOSE) { |
| Log.v(this, "add connection %s", connection); |
| } |
| |
| mTelephonyConnections.add(connection); |
| connection.addConnectionListener(mConnectionListener); |
| recalculateConference(); |
| } |
| |
| /** |
| * Removes a connection from the IMS conference controller. |
| * |
| * @param connection |
| */ |
| void remove(Connection connection) { |
| // External calls are not part of the conference controller, so don't remove them. |
| if ((connection.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) == |
| Connection.PROPERTY_IS_EXTERNAL_CALL) { |
| return; |
| } |
| |
| if (!mTelephonyConnections.contains(connection)) { |
| // Debug only since TelephonyConnectionService tries to clean up the connections tracked |
| // when the original connection changes. It does this proactively. |
| Log.d(this, "remove - connection not tracked; connection=%s", connection); |
| return; |
| } |
| |
| if (Log.VERBOSE) { |
| Log.v(this, "remove connection: %s", connection); |
| } |
| |
| connection.removeConnectionListener(mConnectionListener); |
| mTelephonyConnections.remove(connection); |
| recalculateConferenceable(); |
| } |
| |
| /** |
| * Triggers both a re-check of conferenceable connections, as well as checking for new |
| * conferences. |
| */ |
| private void recalculate() { |
| recalculateConferenceable(); |
| recalculateConference(); |
| } |
| |
| /** |
| * Calculates the conference-capable state of all GSM connections in this connection service. |
| */ |
| private void recalculateConferenceable() { |
| Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size()); |
| HashSet<Conferenceable> conferenceableSet = new HashSet<>(mTelephonyConnections.size() + |
| mImsConferences.size()); |
| HashSet<Conferenceable> conferenceParticipantsSet = new HashSet<>(); |
| |
| // Loop through and collect all calls which are active or holding |
| for (TelephonyConnection connection : mTelephonyConnections) { |
| if (Log.DEBUG) { |
| Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection, |
| connection.isConferenceSupported()); |
| } |
| |
| // If this connection is a member of a conference hosted on another device, it is not |
| // conferenceable with any other connections. |
| if (isMemberOfPeerConference(connection)) { |
| if (Log.VERBOSE) { |
| Log.v(this, "Skipping connection in peer conference: %s", connection); |
| } |
| continue; |
| } |
| |
| // If this connection does not support being in a conference call, then it is not |
| // conferenceable with any other connection. |
| if (!connection.isConferenceSupported()) { |
| connection.setConferenceables(Collections.<Conferenceable>emptyList()); |
| continue; |
| } |
| |
| switch (connection.getState()) { |
| case Connection.STATE_ACTIVE: |
| // fall through |
| case Connection.STATE_HOLDING: |
| conferenceableSet.add(connection); |
| continue; |
| default: |
| break; |
| } |
| // This connection is not active or holding, so clear all conferencable connections |
| connection.setConferenceables(Collections.<Conferenceable>emptyList()); |
| } |
| // Also loop through all active conferences and collect the ones that are ACTIVE or HOLDING. |
| for (ImsConference conference : mImsConferences) { |
| if (Log.DEBUG) { |
| Log.d(this, "recalc - %s %s", conference.getState(), conference); |
| } |
| |
| if (!conference.isConferenceHost()) { |
| if (Log.VERBOSE) { |
| Log.v(this, "skipping conference (not hosted on this device): %s", conference); |
| } |
| continue; |
| } |
| |
| switch (conference.getState()) { |
| case Connection.STATE_ACTIVE: |
| //fall through |
| case Connection.STATE_HOLDING: |
| if (!conference.isFullConference()) { |
| conferenceParticipantsSet.addAll(conference.getConnections()); |
| conferenceableSet.add(conference); |
| } |
| continue; |
| default: |
| break; |
| } |
| } |
| |
| Log.v(this, "conferenceableSet size: " + conferenceableSet.size()); |
| |
| for (Conferenceable c : conferenceableSet) { |
| if (c instanceof Connection) { |
| // Remove this connection from the Set and add all others |
| List<Conferenceable> conferenceables = conferenceableSet |
| .stream() |
| .filter(conferenceable -> c != conferenceable) |
| .collect(Collectors.toList()); |
| // TODO: Remove this once RemoteConnection#setConferenceableConnections is fixed. |
| // Add all conference participant connections as conferenceable with a standalone |
| // Connection. We need to do this to ensure that RemoteConnections work properly. |
| // At the current time, a RemoteConnection will not be conferenceable with a |
| // Conference, so we need to add its children to ensure the user can merge the call |
| // into the conference. |
| // We should add support for RemoteConnection#setConferenceables, which accepts a |
| // list of remote conferences and connections in the future. |
| conferenceables.addAll(conferenceParticipantsSet); |
| |
| ((Connection) c).setConferenceables(conferenceables); |
| } else if (c instanceof ImsConference) { |
| ImsConference imsConference = (ImsConference) c; |
| |
| // If the conference is full, don't allow anything to be conferenced with it. |
| if (imsConference.isFullConference()) { |
| imsConference.setConferenceableConnections(Collections.<Connection>emptyList()); |
| } |
| |
| // Remove all conferences from the set, since we can not conference a conference |
| // to another conference. |
| List<Connection> connections = conferenceableSet |
| .stream() |
| .filter(conferenceable -> conferenceable instanceof Connection) |
| .map(conferenceable -> (Connection) conferenceable) |
| .collect(Collectors.toList()); |
| // Conference equivalent to setConferenceables that only accepts Connections |
| imsConference.setConferenceableConnections(connections); |
| } |
| } |
| } |
| |
| /** |
| * Determines if a connection is a member of a conference hosted on another device. |
| * |
| * @param connection The connection. |
| * @return {@code true} if the connection is a member of a conference hosted on another device. |
| */ |
| private boolean isMemberOfPeerConference(Connection connection) { |
| if (!(connection instanceof TelephonyConnection)) { |
| return false; |
| } |
| TelephonyConnection telephonyConnection = (TelephonyConnection) connection; |
| com.android.internal.telephony.Connection originalConnection = |
| telephonyConnection.getOriginalConnection(); |
| |
| return originalConnection != null && originalConnection.isMultiparty() && |
| originalConnection.isMemberOfPeerConference(); |
| } |
| |
| /** |
| * Starts a new ImsConference for a connection which just entered a multiparty state. |
| */ |
| private void recalculateConference() { |
| Log.v(this, "recalculateConference"); |
| |
| Iterator<TelephonyConnection> it = mTelephonyConnections.iterator(); |
| while (it.hasNext()) { |
| TelephonyConnection connection = it.next(); |
| if (connection.isImsConnection() && connection.getOriginalConnection() != null && |
| connection.getOriginalConnection().isMultiparty()) { |
| |
| startConference(connection); |
| it.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Starts a new {@link ImsConference} for the given IMS connection. |
| * <p> |
| * Creates a new IMS Conference to manage the conference represented by the connection. |
| * Internally the ImsConference wraps the radio connection with a new TelephonyConnection |
| * which is NOT reported to the connection service and Telecom. |
| * <p> |
| * Once the new IMS Conference has been created, the connection passed in is held and removed |
| * from the connection service (removing it from Telecom). The connection is put into a held |
| * state to ensure that telecom removes the connection without putting it into a disconnected |
| * state first. |
| * |
| * @param connection The connection to the Ims server. |
| */ |
| private void startConference(TelephonyConnection connection) { |
| if (Log.VERBOSE) { |
| Log.v(this, "Start new ImsConference - connection: %s", connection); |
| } |
| |
| // Make a clone of the connection which will become the Ims conference host connection. |
| // This is necessary since the Connection Service does not support removing a connection |
| // from Telecom. Instead we create a new instance and remove the old one from telecom. |
| TelephonyConnection conferenceHostConnection = connection.cloneConnection(); |
| conferenceHostConnection.setVideoPauseSupported(connection.getVideoPauseSupported()); |
| |
| PhoneAccountHandle phoneAccountHandle = null; |
| |
| // Attempt to determine the phone account associated with the conference host connection. |
| if (connection.getPhone() != null && |
| connection.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { |
| Phone imsPhone = connection.getPhone(); |
| // The phone account handle for an ImsPhone is based on the default phone (ie the |
| // base GSM or CDMA phone, not on the ImsPhone itself). |
| phoneAccountHandle = |
| PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); |
| } |
| |
| ImsConference conference = new ImsConference(mTelecomAccountRegistry, mConnectionService, |
| conferenceHostConnection, phoneAccountHandle); |
| conference.setState(conferenceHostConnection.getState()); |
| conference.addListener(mConferenceListener); |
| conference.updateConferenceParticipantsAfterCreation(); |
| mConnectionService.addConference(conference); |
| conferenceHostConnection.setTelecomCallId(conference.getTelecomCallId()); |
| |
| // Cleanup TelephonyConnection which backed the original connection and remove from telecom. |
| // Use the "Other" disconnect cause to ensure the call is logged to the call log but the |
| // disconnect tone is not played. |
| connection.removeConnectionListener(mConnectionListener); |
| connection.clearOriginalConnection(); |
| connection.setDisconnected(new DisconnectCause(DisconnectCause.OTHER, |
| android.telephony.DisconnectCause.toString( |
| android.telephony.DisconnectCause.IMS_MERGED_SUCCESSFULLY))); |
| connection.destroy(); |
| mImsConferences.add(conference); |
| } |
| } |