blob: 55c8338a6f04ac628ce1a6d1360461eb21d01ad8 [file] [log] [blame]
/*
* 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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.net.Uri;
import android.telecom.Conference;
import android.telecom.ConferenceParticipant;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.gsm.GsmConnection;
import com.android.internal.telephony.imsphone.ImsPhoneConnection;
/**
* Maintains a list of all the known TelephonyConnections connections and controls GSM and
* default IMS conference call behavior. This functionality is characterized by the support of
* two top-level calls, in contrast to a CDMA conference call which automatically starts a
* conference when there are two calls.
*/
final class TelephonyConferenceController {
private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5;
private final Connection.Listener mConnectionListener = new Connection.Listener() {
@Override
public void onStateChanged(Connection c, int state) {
recalculate();
}
/** ${inheritDoc} */
@Override
public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
recalculate();
}
@Override
public void onDestroyed(Connection connection) {
remove(connection);
}
/**
* Handles notifications from an connection that participant(s) in a conference have changed
* state.
*
* @param c The connection.
* @param participants The participant information.
*/
@Override
public void onConferenceParticipantsChanged(Connection c,
List<ConferenceParticipant> participants) {
if (c == null) {
return;
}
Log.v(this, "onConferenceParticipantsChanged: %d participants", participants.size());
TelephonyConnection telephonyConnection = (TelephonyConnection) c;
handleConferenceParticipantsUpdate(telephonyConnection, participants);
}
};
/** The known connections. */
private final List<TelephonyConnection> mTelephonyConnections = new ArrayList<>();
/**
* The known conference participant connections. The HashMap is keyed by endpoint Uri.
*/
private final HashMap<Uri, ConferenceParticipantConnection> mConferenceParticipantConnections =
new HashMap<>();
private final TelephonyConnectionService mConnectionService;
public TelephonyConferenceController(TelephonyConnectionService connectionService) {
mConnectionService = connectionService;
}
/** The TelephonyConference connection object. */
private TelephonyConference mTelephonyConference;
void add(TelephonyConnection connection) {
mTelephonyConnections.add(connection);
connection.addConnectionListener(mConnectionListener);
recalculate();
}
void remove(Connection connection) {
connection.removeConnectionListener(mConnectionListener);
if (connection instanceof ConferenceParticipantConnection) {
mConferenceParticipantConnections.remove(connection);
} else {
mTelephonyConnections.remove(connection);
}
recalculate();
}
private void recalculate() {
recalculateConference();
recalculateConferenceable();
}
private boolean isFullConference(Conference conference) {
return conference.getConnections().size() >= TELEPHONY_CONFERENCE_MAX_SIZE;
}
private boolean participatesInFullConference(Connection connection) {
return connection.getConference() != null &&
isFullConference(connection.getConference());
}
/**
* Calculates the conference-capable state of all GSM connections in this connection service.
*/
private void recalculateConferenceable() {
Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
List<Connection> activeConnections = new ArrayList<>(mTelephonyConnections.size());
List<Connection> backgroundConnections = new ArrayList<>(mTelephonyConnections.size());
// Loop through and collect all calls which are active or holding
for (Connection connection : mTelephonyConnections) {
Log.d(this, "recalc - %s %s", connection.getState(), connection);
if (!participatesInFullConference(connection)) {
switch (connection.getState()) {
case Connection.STATE_ACTIVE:
activeConnections.add(connection);
continue;
case Connection.STATE_HOLDING:
backgroundConnections.add(connection);
continue;
default:
break;
}
}
connection.setConferenceableConnections(Collections.<Connection>emptyList());
}
Log.v(this, "active: %d, holding: %d",
activeConnections.size(), backgroundConnections.size());
// Go through all the active connections and set the background connections as
// conferenceable.
for (Connection connection : activeConnections) {
connection.setConferenceableConnections(backgroundConnections);
}
// Go through all the background connections and set the active connections as
// conferenceable.
for (Connection connection : backgroundConnections) {
connection.setConferenceableConnections(activeConnections);
}
// Set the conference as conferenceable with all the connections
if (mTelephonyConference != null && !isFullConference(mTelephonyConference)) {
List<Connection> nonConferencedConnections =
new ArrayList<>(mTelephonyConnections.size());
for (TelephonyConnection c : mTelephonyConnections) {
if (c.getConference() == null) {
nonConferencedConnections.add(c);
}
}
Log.v(this, "conference conferenceable: %s", nonConferencedConnections);
mTelephonyConference.setConferenceableConnections(nonConferencedConnections);
}
// TODO: Do not allow conferencing of already conferenced connections.
}
private void recalculateConference() {
Set<Connection> conferencedConnections = new HashSet<>();
int numGsmConnections = 0;
int numImsConnections = 0;
for (TelephonyConnection connection : mTelephonyConnections) {
com.android.internal.telephony.Connection radioConnection =
connection.getOriginalConnection();
if (radioConnection != null) {
Call.State state = radioConnection.getState();
Call call = radioConnection.getCall();
if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
(call != null && call.isMultiparty())) {
if (radioConnection instanceof GsmConnection) {
numGsmConnections++;
} else if (radioConnection instanceof ImsPhoneConnection) {
numImsConnections++;
}
conferencedConnections.add(connection);
}
}
}
Log.d(this, "Recalculate conference calls %s %s.",
mTelephonyConference, conferencedConnections);
boolean wasParticipantsAdded = false;
// If the number of telephony connections drops below the limit, the conference can be
// considered terminated.
// We must have less than 2 GSM connections and less than 1 IMS connection.
if (numGsmConnections < 2 && numImsConnections < 1) {
Log.d(this, "not enough connections to be a conference!");
// The underlying telephony connections have been disconnected -- disconnect the
// conference participants now.
disconnectConferenceParticipants();
// No more connections are conferenced, destroy any existing conference.
if (mTelephonyConference != null) {
Log.d(this, "with a conference to destroy!");
mTelephonyConference.destroy();
mTelephonyConference = null;
}
} else {
if (mTelephonyConference != null) {
List<Connection> existingConnections = mTelephonyConference.getConnections();
// Remove any that no longer exist
for (Connection connection : existingConnections) {
if (connection instanceof TelephonyConnection &&
!conferencedConnections.contains(connection)) {
mTelephonyConference.removeConnection(connection);
}
}
// Add any new ones
for (Connection connection : conferencedConnections) {
if (!existingConnections.contains(connection)) {
mTelephonyConference.addConnection(connection);
}
}
// Add new conference participants
for (Connection conferenceParticipant :
mConferenceParticipantConnections.values()) {
if (conferenceParticipant.getState() == Connection.STATE_NEW) {
if (!existingConnections.contains(conferenceParticipant)) {
wasParticipantsAdded = true;
mTelephonyConference.addConnection(conferenceParticipant);
}
}
}
} else {
mTelephonyConference = new TelephonyConference(null);
for (Connection connection : conferencedConnections) {
Log.d(this, "Adding a connection to a conference call: %s %s",
mTelephonyConference, connection);
mTelephonyConference.addConnection(connection);
}
// Add the conference participants
for (Connection conferenceParticipant :
mConferenceParticipantConnections.values()) {
wasParticipantsAdded = true;
mTelephonyConference.addConnection(conferenceParticipant);
}
mConnectionService.addConference(mTelephonyConference);
}
// If we added conference participants (e.g. via an IMS conference event package),
// notify the conference so that the MANAGE_CONFERENCE capability can be added.
if (wasParticipantsAdded) {
mTelephonyConference.setParticipantsReceived();
}
// Set the conference state to the same state as its child connections.
Connection conferencedConnection = mTelephonyConference.getPrimaryConnection();
switch (conferencedConnection.getState()) {
case Connection.STATE_ACTIVE:
mTelephonyConference.setActive();
break;
case Connection.STATE_HOLDING:
mTelephonyConference.setOnHold();
break;
}
}
}
/**
* Disconnects all conference participants from the conference.
*/
private void disconnectConferenceParticipants() {
for (Connection connection : mConferenceParticipantConnections.values()) {
// Disconnect listener so that the connection doesn't fire events on the conference
// controller, causing a recursive call.
connection.removeConnectionListener(mConnectionListener);
mConferenceParticipantConnections.remove(connection);
// Mark disconnect cause as cancelled to ensure that the call is not logged in the
// call log.
connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
connection.destroy();
}
}
/**
* Handles state changes for conference participant(s).
*
* @param parent The connection which was notified of the conference participant.
* @param participants The conference participant information.
*/
private void handleConferenceParticipantsUpdate(
TelephonyConnection parent, List<ConferenceParticipant> participants) {
boolean recalculateConference = false;
ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
for (ConferenceParticipant participant : participants) {
Uri endpoint = participant.getEndpoint();
if (!mConferenceParticipantConnections.containsKey(endpoint)) {
createConferenceParticipantConnection(parent, participant);
newParticipants.add(participant);
recalculateConference = true;
} else {
ConferenceParticipantConnection connection =
mConferenceParticipantConnections.get(endpoint);
connection.updateState(participant.getState());
}
}
if (recalculateConference) {
// Recalculate to add new connections to the conference.
recalculateConference();
// Now that conference is established, set the state for all participants.
for (ConferenceParticipant newParticipant : newParticipants) {
ConferenceParticipantConnection connection =
mConferenceParticipantConnections.get(newParticipant.getEndpoint());
connection.updateState(newParticipant.getState());
}
}
}
/**
* Creates a new {@link ConferenceParticipantConnection} to represent a
* {@link ConferenceParticipant}.
* <p>
* The new connection is added to the conference controller and connection service.
*
* @param parent The connection which was notified of the participant change (e.g. the
* parent connection).
* @param participant The conference participant information.
*/
private void createConferenceParticipantConnection(
TelephonyConnection parent, ConferenceParticipant participant) {
// Create and add the new connection in holding state so that it does not become the
// active call.
ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
parent, participant);
connection.addConnectionListener(mConnectionListener);
mConferenceParticipantConnections.put(participant.getEndpoint(), connection);
PhoneAccountHandle phoneAccountHandle =
TelecomAccountRegistry.makePstnPhoneAccountHandle(parent.getPhone());
mConnectionService.addExistingConnection(phoneAccountHandle, connection);
}
}