/* * 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 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.CallStateException; import com.android.internal.telephony.Phone; import com.android.internal.telephony.imsphone.ImsPhoneConnection; import com.android.phone.PhoneUtils; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Represents an IMS conference call. *
* An IMS conference call consists of a conference host connection and potentially a list of * conference participants. The conference host connection represents the radio connection to the * IMS conference server. Since it is not a connection to any one individual, it is not represented * in Telecom/InCall as a call. The conference participant information is received via the host * connection via a conference event package. Conference participant connections do not represent * actual radio connections to the participants; they act as a virtual representation of the * participant, keyed by a unique endpoint {@link android.net.Uri}. *
* The {@link ImsConference} listens for conference event package data received via the host
* connection and is responsible for managing the conference participant connections which represent
* the participants.
*/
public class ImsConference extends Conference {
/**
* Listener used to respond to changes to conference participants. At the conference level we
* are most concerned with handling destruction of a conference participant.
*/
private final Connection.Listener mParticipantListener = new Connection.Listener() {
/**
* Participant has been destroyed. Remove it from the conference.
*
* @param connection The participant which was destroyed.
*/
@Override
public void onDestroyed(Connection connection) {
ConferenceParticipantConnection participant =
(ConferenceParticipantConnection) connection;
removeConferenceParticipant(participant);
updateManageConference();
}
};
/**
* Listener used to respond to changes to the underlying radio connection for the conference
* host connection. Used to respond to SRVCC changes.
*/
private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
new TelephonyConnection.TelephonyConnectionListener() {
@Override
public void onOriginalConnectionConfigured(TelephonyConnection c) {
if (c == mConferenceHost) {
handleOriginalConnectionChange();
}
}
};
/**
* Listener used to respond to changes to the connection to the IMS conference server.
*/
private final android.telecom.Connection.Listener mConferenceHostListener =
new android.telecom.Connection.Listener() {
/**
* Updates the state of the conference based on the new state of the host.
*
* @param c The host connection.
* @param state The new state
*/
@Override
public void onStateChanged(android.telecom.Connection c, int state) {
setState(state);
}
/**
* Disconnects the conference when its host connection disconnects.
*
* @param c The host connection.
* @param disconnectCause The host connection disconnect cause.
*/
@Override
public void onDisconnected(android.telecom.Connection c, DisconnectCause disconnectCause) {
setDisconnected(disconnectCause);
}
/**
* Handles destruction of the host connection; once the host connection has been
* destroyed, cleans up the conference participant connection.
*
* @param connection The host connection.
*/
@Override
public void onDestroyed(android.telecom.Connection connection) {
disconnectConferenceParticipants();
}
/**
* Handles changes to conference participant data as reported by the conference host
* connection.
*
* @param c The connection.
* @param participants The participant information.
*/
@Override
public void onConferenceParticipantsChanged(android.telecom.Connection c,
List
* Hangs up the call via the conference host connection. When the host connection has been
* successfully disconnected, the {@link #mConferenceHostListener} listener receives an
* {@code onDestroyed} event, which triggers the conference participant connections to be
* disconnected.
*/
@Override
public void onDisconnect() {
Log.v(this, "onDisconnect: hanging up conference host.");
if (mConferenceHost == null) {
return;
}
Call call = mConferenceHost.getCall();
if (call != null) {
try {
call.hangup();
} catch (CallStateException e) {
Log.e(this, e, "Exception thrown trying to hangup conference");
}
}
}
/**
* Invoked when the specified {@link android.telecom.Connection} should be separated from the
* conference call.
*
* IMS does not support separating connections from the conference.
*
* @param connection The connection to separate.
*/
@Override
public void onSeparate(android.telecom.Connection connection) {
Log.wtf(this, "Cannot separate connections from an IMS conference.");
}
/**
* Invoked when the specified {@link android.telecom.Connection} should be merged into the
* conference call.
*
* @param connection The {@code Connection} to merge.
*/
@Override
public void onMerge(android.telecom.Connection connection) {
try {
Phone phone = ((TelephonyConnection) connection).getPhone();
if (phone != null) {
phone.conference();
}
} catch (CallStateException e) {
Log.e(this, e, "Exception thrown trying to merge call into a conference");
}
}
/**
* Invoked when the conference should be put on hold.
*/
@Override
public void onHold() {
if (mConferenceHost == null) {
return;
}
mConferenceHost.performHold();
}
/**
* Invoked when the conference should be moved from hold to active.
*/
@Override
public void onUnhold() {
if (mConferenceHost == null) {
return;
}
mConferenceHost.performUnhold();
}
/**
* Invoked to play a DTMF tone.
*
* @param c A DTMF character.
*/
@Override
public void onPlayDtmfTone(char c) {
if (mConferenceHost == null) {
return;
}
mConferenceHost.onPlayDtmfTone(c);
}
/**
* Invoked to stop playing a DTMF tone.
*/
@Override
public void onStopDtmfTone() {
if (mConferenceHost == null) {
return;
}
mConferenceHost.onStopDtmfTone();
}
/**
* Handles the addition of connections to the {@link ImsConference}. The
* {@link ImsConferenceController} does not add connections to the conference.
*
* @param connection The newly added connection.
*/
@Override
public void onConnectionAdded(android.telecom.Connection connection) {
// No-op
}
/**
* Updates the manage conference capability of the conference. Where there are one or more
* conference event package participants, the conference management is permitted. Where there
* are no conference event package participants, conference management is not permitted.
*/
private void updateManageConference() {
boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
Log.v(this, "updateManageConference was:%s is:%s", couldManageConference ? "Y" : "N",
canManageConference ? "Y" : "N");
if (couldManageConference != canManageConference) {
int newCapabilities = getConnectionCapabilities();
if (canManageConference) {
addCapability(Connection.CAPABILITY_MANAGE_CONFERENCE);
} else {
removeCapability(Connection.CAPABILITY_MANAGE_CONFERENCE);
}
}
}
/**
* Sets the connection hosting the conference and registers for callbacks.
*
* @param conferenceHost The connection hosting the conference.
*/
private void setConferenceHost(TelephonyConnection conferenceHost) {
if (Log.VERBOSE) {
Log.v(this, "setConferenceHost " + conferenceHost);
}
mConferenceHost = conferenceHost;
mConferenceHost.addConnectionListener(mConferenceHostListener);
mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener);
}
/**
* Handles state changes for conference participant(s). The participants data passed in
*
* @param parent The connection which was notified of the conference participant.
* @param participants The conference participant information.
*/
private void handleConferenceParticipantsUpdate(
TelephonyConnection parent, List
* 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.getOriginalConnection(), participant);
connection.addConnectionListener(mParticipantListener);
if (Log.VERBOSE) {
Log.v(this, "createConferenceParticipantConnection: %s", connection);
}
mConferenceParticipantConnections.put(participant.getEndpoint(), connection);
PhoneAccountHandle phoneAccountHandle =
PhoneUtils.makePstnPhoneAccountHandle(parent.getPhone());
mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, connection);
addConnection(connection);
}
/**
* Removes a conference participant from the conference.
*
* @param participant The participant to remove.
*/
private void removeConferenceParticipant(ConferenceParticipantConnection participant) {
if (Log.VERBOSE) {
Log.v(this, "removeConferenceParticipant: %s", participant);
}
participant.removeConnectionListener(mParticipantListener);
participant.getEndpoint();
mConferenceParticipantConnections.remove(participant.getEndpoint());
}
/**
* Disconnects all conference participants from the conference.
*/
private void disconnectConferenceParticipants() {
Log.v(this, "disconnectConferenceParticipants");
for (ConferenceParticipantConnection connection :
mConferenceParticipantConnections.values()) {
removeConferenceParticipant(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();
}
mConferenceParticipantConnections.clear();
}
/**
* Handles a change in the original connection backing the conference host connection. This can
* happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to
* GSM or CDMA.
*
* If this happens, we will add the conference host connection to telecom and tear down the
* conference.
*/
private void handleOriginalConnectionChange() {
if (mConferenceHost == null) {
Log.w(this, "handleOriginalConnectionChange; conference host missing.");
return;
}
com.android.internal.telephony.Connection originalConnection =
mConferenceHost.getOriginalConnection();
if (!(originalConnection instanceof ImsPhoneConnection)) {
if (Log.VERBOSE) {
Log.v(this,
"Original connection for conference host is no longer an IMS connection; " +
"new connection: %s", originalConnection);
}
PhoneAccountHandle phoneAccountHandle =
PhoneUtils.makePstnPhoneAccountHandle(mConferenceHost.getPhone());
mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, mConferenceHost);
mConferenceHost.removeConnectionListener(mConferenceHostListener);
mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener);
mConferenceHost = null;
setDisconnected(new DisconnectCause(DisconnectCause.OTHER));
destroy();
}
}
/**
* Changes the state of the Ims conference.
*
* @param state the new state.
*/
public void setState(int state) {
Log.v(this, "setState %s", Connection.stateToString(state));
switch (state) {
case Connection.STATE_INITIALIZING:
case Connection.STATE_NEW:
case Connection.STATE_RINGING:
case Connection.STATE_DIALING:
// No-op -- not applicable.
break;
case Connection.STATE_DISCONNECTED:
DisconnectCause disconnectCause;
if (mConferenceHost == null) {
disconnectCause = new DisconnectCause(DisconnectCause.CANCELED);
} else {
disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause(
mConferenceHost.getOriginalConnection().getDisconnectCause());
}
setDisconnected(disconnectCause);
destroy();
break;
case Connection.STATE_ACTIVE:
setActive();
break;
case Connection.STATE_HOLDING:
setOnHold();
break;
}
}
/**
* Builds a string representation of the {@link ImsConference}.
*
* @return String representing the conference.
*/
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[ImsConference objId:");
sb.append(System.identityHashCode(this));
sb.append(" state:");
sb.append(Connection.stateToString(getState()));
sb.append(" hostConnection:");
sb.append(mConferenceHost);
sb.append(" participants:");
sb.append(mConferenceParticipantConnections.size());
sb.append("]");
return sb.toString();
}
}