/* * 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 mTelephonyConnections = new ArrayList<>(); /** * List of known {@link ImsConference}s. Realistically there will only ever be a single * concurrent IMS conference. */ private final ArrayList 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 conferenceableSet = new HashSet<>(mTelephonyConnections.size() + mImsConferences.size()); HashSet 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.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.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 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.emptyList()); } // Remove all conferences from the set, since we can not conference a conference // to another conference. List 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 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. *

* 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. *

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