diff options
author | Tyler Gunn <tgunn@google.com> | 2014-11-20 03:40:29 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2014-11-20 03:40:30 +0000 |
commit | 9c1749ebd4bef58a54490293780c214dab818a87 (patch) | |
tree | 685c5a6cd4b59ef88ce6c0c64860923eccb1a2e8 /src | |
parent | c30aaabf2a498c4fe11243b25a1d0eb1c833f4d5 (diff) | |
parent | 2c248f38ac4c7fefc50fb9c595b6cbf2798c0632 (diff) | |
download | android_packages_services_Telephony-9c1749ebd4bef58a54490293780c214dab818a87.tar.gz android_packages_services_Telephony-9c1749ebd4bef58a54490293780c214dab818a87.tar.bz2 android_packages_services_Telephony-9c1749ebd4bef58a54490293780c214dab818a87.zip |
Merge "Create IMSConference to represent IMS conference calls." into lmp-mr1-dev
Diffstat (limited to 'src')
7 files changed, 920 insertions, 204 deletions
diff --git a/src/com/android/services/telephony/ConferenceParticipantConnection.java b/src/com/android/services/telephony/ConferenceParticipantConnection.java index e74863481..49307ba1e 100644 --- a/src/com/android/services/telephony/ConferenceParticipantConnection.java +++ b/src/com/android/services/telephony/ConferenceParticipantConnection.java @@ -19,8 +19,8 @@ package com.android.services.telephony; import com.android.internal.telephony.PhoneConstants; import android.net.Uri; -import android.telecom.ConferenceParticipant; import android.telecom.Connection; +import android.telecom.ConferenceParticipant; import android.telecom.DisconnectCause; import android.telecom.PhoneCapabilities; @@ -37,7 +37,7 @@ public class ConferenceParticipantConnection extends Connection { /** * The connection which owns this participant. */ - private final Connection mParentConnection; + private final com.android.internal.telephony.Connection mParentConnection; /** * Creates a new instance. @@ -45,7 +45,8 @@ public class ConferenceParticipantConnection extends Connection { * @param participant The conference participant to create the instance for. */ public ConferenceParticipantConnection( - Connection parentConnection, ConferenceParticipant participant) { + com.android.internal.telephony.Connection parentConnection, + ConferenceParticipant participant) { mParentConnection = parentConnection; setAddress(participant.getHandle(), PhoneConstants.PRESENTATION_ALLOWED); @@ -61,6 +62,8 @@ public class ConferenceParticipantConnection extends Connection { * @param newState The new state. */ public void updateState(int newState) { + Log.v(this, "updateState endPoint: %s state: %s", Log.pii(mEndpoint), + Connection.stateToString(newState)); if (newState == getState()) { return; } @@ -100,12 +103,19 @@ public class ConferenceParticipantConnection extends Connection { */ @Override public void onDisconnect() { - Log.v(this, "onDisconnect"); - mParentConnection.onDisconnectConferenceParticipant(mEndpoint); } /** + * Retrieves the endpoint for this connection. + * + * @return The endpoint. + */ + public Uri getEndpoint() { + return mEndpoint; + } + + /** * Configures the {@link android.telecom.PhoneCapabilities} applicable to this connection. A * conference participant can only be disconnected from a conference since there is not * actual connection to the participant which could be split from the conference. @@ -114,4 +124,25 @@ public class ConferenceParticipantConnection extends Connection { int capabilities = PhoneCapabilities.DISCONNECT_FROM_CONFERENCE; setCallCapabilities(capabilities); } + + /** + * Builds a string representation of this conference participant connection. + * + * @return String representation of connection. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[ConferenceParticipantConnection objId:"); + sb.append(System.identityHashCode(this)); + sb.append(" endPoint:"); + sb.append(Log.pii(mEndpoint)); + sb.append(" parentConnection:"); + sb.append(Log.pii(mParentConnection.getAddress())); + sb.append(" state:"); + sb.append(Connection.stateToString(getState())); + sb.append("]"); + + return sb.toString(); + } } diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java new file mode 100644 index 000000000..0d235c3dc --- /dev/null +++ b/src/com/android/services/telephony/ImsConference.java @@ -0,0 +1,504 @@ +/* + * 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.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Phone; + +import android.net.Uri; +import android.telecom.Connection; +import android.telecom.Conference; +import android.telecom.ConferenceParticipant; +import android.telecom.DisconnectCause; +import android.telecom.PhoneAccountHandle; +import android.telecom.PhoneCapabilities; + +import java.util.ArrayList; +import java.util.HashMap; +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. + * <P> + * 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}. + * <p> + * 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 { + + /** + * TelephonyConnection class used to represent the connection to the conference server. + */ + private class ConferenceHostConnection extends TelephonyConnection { + protected ConferenceHostConnection( + com.android.internal.telephony.Connection originalConnection) { + super(originalConnection); + } + } + + /** + * 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 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<ConferenceParticipant> participants) { + + if (c == null) { + return; + } + Log.v(this, "onConferenceParticipantsChanged: %d participants", participants.size()); + TelephonyConnection telephonyConnection = (TelephonyConnection) c; + handleConferenceParticipantsUpdate(telephonyConnection, participants); + } + }; + + /** + * The telephony connection service; used to add new participant connections to Telecom. + */ + private TelephonyConnectionService mTelephonyConnectionService; + + /** + * The connection to the conference server which is hosting the conference. + */ + private TelephonyConnection mConferenceHost; + + /** + * The known conference participant connections. The HashMap is keyed by endpoint Uri. + * A {@link ConcurrentHashMap} is used as there is a possibility for radio events impacting the + * available participants to occur at the same time as an access via the connection service. + */ + private final ConcurrentHashMap<Uri, ConferenceParticipantConnection> + mConferenceParticipantConnections = + new ConcurrentHashMap<Uri, ConferenceParticipantConnection>(8, 0.9f, 1); + + /** + * Initializes a new {@link ImsConference}. + * + * @param telephonyConnectionService The connection service responsible for adding new + * conferene participants. + * @param conferenceHost The IMS radio connection hosting the conference. + */ + public ImsConference(TelephonyConnectionService telephonyConnectionService, + com.android.internal.telephony.Connection conferenceHost) { + + super(null); + mTelephonyConnectionService = telephonyConnectionService; + setConferenceHost(conferenceHost); + setCapabilities( + PhoneCapabilities.SUPPORT_HOLD | + PhoneCapabilities.HOLD | + PhoneCapabilities.MUTE + ); + } + + /** + * Not used by the IMS conference controller. + * + * @return {@code Null}. + */ + @Override + public android.telecom.Connection getPrimaryConnection() { + return null; + } + + /** + * Invoked when the Conference and all its {@link Connection}s should be disconnected. + * <p> + * 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."); + + 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. + * <p> + * 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() { + mConferenceHost.performHold(); + } + + /** + * Invoked when the conference should be moved from hold to active. + */ + @Override + public void onUnhold() { + mConferenceHost.performUnhold(); + } + + /** + * Invoked to play a DTMF tone. + * + * @param c A DTMF character. + */ + @Override + public void onPlayDtmfTone(char c) { + mConferenceHost.onPlayDtmfTone(c); + } + + /** + * Invoked to stop playing a DTMF tone. + */ + @Override + public void onStopDtmfTone() { + 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 = PhoneCapabilities.can(getCapabilities(), + PhoneCapabilities.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 = getCapabilities(); + + if (canManageConference) { + newCapabilities |= PhoneCapabilities.MANAGE_CONFERENCE; + } else { + newCapabilities = PhoneCapabilities.remove(newCapabilities, + PhoneCapabilities.MANAGE_CONFERENCE); + } + setCapabilities(newCapabilities); + } + } + + /** + * Sets the connection hosting the conference and registers for callbacks. + * + * @param conferenceHost The connection hosting the conference. + */ + private void setConferenceHost(com.android.internal.telephony.Connection conferenceHost) { + if (Log.VERBOSE) { + Log.v(this, "setConferenceHost " + conferenceHost); + } + + mConferenceHost = new ConferenceHostConnection(conferenceHost); + mConferenceHost.addConnectionListener(mConferenceHostListener); + } + + /** + * 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<ConferenceParticipant> participants) { + + boolean newParticipantsAdded = false; + boolean oldParticipantsRemoved = false; + ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size()); + HashSet<Uri> participantEndpoints = new HashSet<>(participants.size()); + + // Add any new participants and update existing. + for (ConferenceParticipant participant : participants) { + Uri endpoint = participant.getEndpoint(); + participantEndpoints.add(endpoint); + if (!mConferenceParticipantConnections.containsKey(endpoint)) { + createConferenceParticipantConnection(parent, participant); + newParticipants.add(participant); + newParticipantsAdded = true; + } else { + ConferenceParticipantConnection connection = + mConferenceParticipantConnections.get(endpoint); + connection.updateState(participant.getState()); + } + } + + // Set state of new participants. + if (newParticipantsAdded) { + // Set the state of the new participants at once and add to the conference + for (ConferenceParticipant newParticipant : newParticipants) { + ConferenceParticipantConnection connection = + mConferenceParticipantConnections.get(newParticipant.getEndpoint()); + connection.updateState(newParticipant.getState()); + } + } + + // Finally, remove any participants from the conference that no longer exist in the + // conference event package data. + Iterator<Map.Entry<Uri, ConferenceParticipantConnection>> entryIterator = + mConferenceParticipantConnections.entrySet().iterator(); + while (entryIterator.hasNext()) { + Map.Entry<Uri, ConferenceParticipantConnection> entry = entryIterator.next(); + + if (!participantEndpoints.contains(entry.getKey())) { + ConferenceParticipantConnection participant = entry.getValue(); + participant.removeConnectionListener(mParticipantListener); + removeConnection(participant); + entryIterator.remove(); + oldParticipantsRemoved = true; + } + } + + // If new participants were added or old ones were removed, we need to ensure the state of + // the manage conference capability is updated. + if (newParticipantsAdded || oldParticipantsRemoved) { + updateManageConference(); + } + } + + /** + * 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.getOriginalConnection(), participant); + connection.addConnectionListener(mParticipantListener); + + if (Log.VERBOSE) { + Log.v(this, "createConferenceParticipantConnection: %s", connection); + } + + mConferenceParticipantConnections.put(participant.getEndpoint(), connection); + PhoneAccountHandle phoneAccountHandle = + TelecomAccountRegistry.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(); + } + + /** + * 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: + setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( + mConferenceHost.getOriginalConnection().getDisconnectCause())); + 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(); + } +} diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java new file mode 100644 index 000000000..6d92fbb9c --- /dev/null +++ b/src/com/android/services/telephony/ImsConferenceController.java @@ -0,0 +1,274 @@ +/* + * 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.Connection; +import android.telecom.ConnectionService; +import android.telecom.DisconnectCause; +import android.telecom.IConferenceable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * 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); + } + }; + + /** + * The current {@link ConnectionService}. + */ + private final TelephonyConnectionService 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); + + /** + * Creates a new instance of the Ims conference controller. + * + * @param connectionService The current connection service. + */ + public ImsConferenceController(TelephonyConnectionService connectionService) { + mConnectionService = connectionService; + } + + /** + * Adds a new connection to the IMS conference controller. + * + * @param connection + */ + void add(TelephonyConnection connection) { + // 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) { + if (Log.VERBOSE) { + Log.v(this, "remove connection: %s", connection); + } + + 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()); + List<IConferenceable> activeConnections = new ArrayList<>(mTelephonyConnections.size()); + List<IConferenceable> backgroundConnections = new ArrayList<>(mTelephonyConnections.size()); + + // Loop through and collect all calls which are active or holding + for (Connection connection : mTelephonyConnections) { + if (Log.DEBUG) { + Log.d(this, "recalc - %s %s", connection.getState(), 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()); + } + + for (Conference conference : mImsConferences) { + if (Log.DEBUG) { + Log.d(this, "recalc - %s %s", conference.getState(), conference); + } + + switch (conference.getState()) { + case Connection.STATE_ACTIVE: + activeConnections.add(conference); + continue; + case Connection.STATE_HOLDING: + backgroundConnections.add(conference); + continue; + default: + break; + } + } + + 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 (IConferenceable conferenceable : activeConnections) { + if (conferenceable instanceof Connection) { + Connection connection = (Connection) conferenceable; + connection.setConferenceables(backgroundConnections); + } + } + + // Go through all the background connections and set the active connections as + // conferenceable. + for (IConferenceable conferenceable : backgroundConnections) { + if (conferenceable instanceof Connection) { + Connection connection = (Connection) conferenceable; + connection.setConferenceables(activeConnections); + } + + } + + // Set the conference as conferenceable with all the connections + for (ImsConference conference : mImsConferences) { + List<Connection> nonConferencedConnections = + new ArrayList<>(mTelephonyConnections.size()); + for (Connection c : mTelephonyConnections) { + if (c.getConference() == null) { + nonConferencedConnections.add(c); + } + } + if (Log.VERBOSE) { + Log.v(this, "conference conferenceable: %s", nonConferencedConnections); + } + conference.setConferenceableConnections(nonConferencedConnections); + } + } + + /** + * 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) { + com.android.internal.telephony.Connection originalConnection = + connection.getOriginalConnection(); + if (Log.VERBOSE) { + Log.v(this, "Start new ImsConference - connection: %s", connection); + } + + // Create conference and add to telecom + ImsConference conference = new ImsConference(mConnectionService, originalConnection); + conference.setState(connection.getState()); + mConnectionService.addConference(conference); + conference.addListener(mConferenceListener); + + // Cleanup TelephonyConnection which backed the original connection and remove from telecom. + // Use the "Canceled" disconnect cause to ensure the call is not logged. + connection.removeConnectionListener(mConnectionListener); + connection.clearOriginalConnection(); + connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); + connection.destroy(); + mImsConferences.add(conference); + } +} diff --git a/src/com/android/services/telephony/TelephonyConference.java b/src/com/android/services/telephony/TelephonyConference.java index fc009bc4f..3a8009705 100644 --- a/src/com/android/services/telephony/TelephonyConference.java +++ b/src/com/android/services/telephony/TelephonyConference.java @@ -33,12 +33,6 @@ import java.util.List; */ public class TelephonyConference extends Conference { - /** - * When {@code true}, indicates that conference participant information from an IMS conference - * event package has been received. - */ - private boolean mParticipantsReceived = false; - public TelephonyConference(PhoneAccountHandle phoneAccount) { super(phoneAccount); setCapabilities( @@ -55,17 +49,30 @@ public class TelephonyConference extends Conference { @Override public void onDisconnect() { for (Connection connection : getConnections()) { - Call call = getMultipartyCallForConnection(connection, "onDisconnect"); - if (call != null) { - Log.d(this, "Found multiparty call to hangup for conference."); - try { - call.hangup(); - break; - } catch (CallStateException e) { - Log.e(this, e, "Exception thrown trying to hangup conference"); - } + if (disconnectCall(connection)) { + break; + } + } + } + + /** + * Disconnect the underlying Telephony Call for a connection. + * + * @param connection The connection. + * @return {@code True} if the call was disconnected. + */ + private boolean disconnectCall(Connection connection) { + Call call = getMultipartyCallForConnection(connection, "onDisconnect"); + if (call != null) { + Log.d(this, "Found multiparty call to hangup for conference."); + try { + call.hangup(); + return true; + } catch (CallStateException e) { + Log.e(this, e, "Exception thrown trying to hangup conference"); } } + return false; } /** @@ -140,8 +147,7 @@ public class TelephonyConference extends Conference { // as the default behavior. If there is a conference event package, this may be overridden. // If a conference event package was received, do not attempt to remove manage conference. if (connection instanceof TelephonyConnection && - ((TelephonyConnection) connection).wasImsConnection() && - !mParticipantsReceived) { + ((TelephonyConnection) connection).wasImsConnection()) { int capabilities = getCapabilities(); if (PhoneCapabilities.can(capabilities, PhoneCapabilities.MANAGE_CONFERENCE)) { int newCapabilities = @@ -153,11 +159,17 @@ public class TelephonyConference extends Conference { @Override public Connection getPrimaryConnection() { + + List<Connection> connections = getConnections(); + if (connections == null || connections.isEmpty()) { + return null; + } + // Default to the first connection. - Connection primaryConnection = getConnections().get(0); + Connection primaryConnection = connections.get(0); // Otherwise look for a connection where the radio connection states it is multiparty. - for (Connection connection : getConnections()) { + for (Connection connection : connections) { com.android.internal.telephony.Connection radioConnection = getOriginalConnection(connection); @@ -182,7 +194,7 @@ public class TelephonyConference extends Conference { return null; } - private com.android.internal.telephony.Connection getOriginalConnection( + protected com.android.internal.telephony.Connection getOriginalConnection( Connection connection) { if (connection instanceof TelephonyConnection) { @@ -199,17 +211,4 @@ public class TelephonyConference extends Conference { } return (TelephonyConnection) connections.get(0); } - - /** - * Flags the conference to indicate that a conference event package has been received and there - * is now participant data present which would permit conference management. - */ - public void setParticipantsReceived() { - if (!mParticipantsReceived) { - int capabilities = getCapabilities(); - capabilities |= PhoneCapabilities.MANAGE_CONFERENCE; - setCapabilities(capabilities); - } - mParticipantsReceived = true; - } } diff --git a/src/com/android/services/telephony/TelephonyConferenceController.java b/src/com/android/services/telephony/TelephonyConferenceController.java index 55c8338a6..2af10a62d 100644 --- a/src/com/android/services/telephony/TelephonyConferenceController.java +++ b/src/com/android/services/telephony/TelephonyConferenceController.java @@ -59,36 +59,11 @@ final class TelephonyConferenceController { 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) { @@ -106,12 +81,7 @@ final class TelephonyConferenceController { void remove(Connection connection) { connection.removeConnectionListener(mConnectionListener); - - if (connection instanceof ConferenceParticipantConnection) { - mConferenceParticipantConnections.remove(connection); - } else { - mTelephonyConnections.remove(connection); - } + mTelephonyConnections.remove(connection); recalculate(); } @@ -192,9 +162,7 @@ final class TelephonyConferenceController { private void recalculateConference() { Set<Connection> conferencedConnections = new HashSet<>(); - int numGsmConnections = 0; - int numImsConnections = 0; for (TelephonyConnection connection : mTelephonyConnections) { com.android.internal.telephony.Connection radioConnection = @@ -206,11 +174,7 @@ final class TelephonyConferenceController { if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) && (call != null && call.isMultiparty())) { - if (radioConnection instanceof GsmConnection) { - numGsmConnections++; - } else if (radioConnection instanceof ImsPhoneConnection) { - numImsConnections++; - } + numGsmConnections++; conferencedConnections.add(connection); } } @@ -219,18 +183,11 @@ final class TelephonyConferenceController { 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) { + // If this is a GSM conference and the number of connections drops below 2, we will + // terminate the conference. + if (numGsmConnections < 2) { 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!"); @@ -254,131 +211,30 @@ final class TelephonyConferenceController { 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()); + if (conferencedConnection != null) { + switch (conferencedConnection.getState()) { + case Connection.STATE_ACTIVE: + mTelephonyConference.setActive(); + break; + case Connection.STATE_HOLDING: + mTelephonyConference.setOnHold(); + break; + } } } } - - /** - * 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); - } } diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java index 056f62f82..77a76d09c 100644 --- a/src/com/android/services/telephony/TelephonyConnection.java +++ b/src/com/android/services/telephony/TelephonyConnection.java @@ -24,6 +24,7 @@ import android.telecom.AudioState; import android.telecom.Conference; import android.telecom.ConferenceParticipant; import android.telecom.Connection; +import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.PhoneCapabilities; @@ -31,6 +32,9 @@ import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.Connection.PostDialListener; import com.android.internal.telephony.Phone; +import com.android.internal.telephony.cdma.CdmaCall; +import com.android.internal.telephony.gsm.*; +import com.android.internal.telephony.gsm.GsmConnection; import com.android.internal.telephony.imsphone.ImsPhoneConnection; import java.lang.Override; @@ -442,12 +446,8 @@ abstract class TelephonyConnection extends Connection { void setOriginalConnection(com.android.internal.telephony.Connection originalConnection) { Log.v(this, "new TelephonyConnection, originalConnection: " + originalConnection); - if (mOriginalConnection != null) { - getPhone().unregisterForPreciseCallStateChanged(mHandler); - getPhone().unregisterForRingbackTone(mHandler); - getPhone().unregisterForHandoverStateChanged(mHandler); - getPhone().unregisterForDisconnect(mHandler); - } + clearOriginalConnection(); + mOriginalConnection = originalConnection; getPhone().registerForPreciseCallStateChanged( mHandler, MSG_PRECISE_CALL_STATE_CHANGED, null); @@ -473,6 +473,19 @@ abstract class TelephonyConnection extends Connection { updateAddress(); } + /** + * Un-sets the underlying radio connection. + */ + void clearOriginalConnection() { + if (mOriginalConnection != null) { + getPhone().unregisterForPreciseCallStateChanged(mHandler); + getPhone().unregisterForRingbackTone(mHandler); + getPhone().unregisterForHandoverStateChanged(mHandler); + getPhone().unregisterForDisconnect(mHandler); + mOriginalConnection = null; + } + } + protected void hangup(int telephonyDisconnectCode) { if (mOriginalConnection != null) { try { @@ -871,4 +884,41 @@ abstract class TelephonyConnection extends Connection { l.onOriginalConnectionConfigured(this); } } + + /** + * Creates a string representation of this {@link TelephonyConnection}. Primarily intended for + * use in log statements. + * + * @return String representation of the connection. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[TelephonyConnection objId:"); + sb.append(System.identityHashCode(this)); + sb.append(" type:"); + if (isImsConnection()) { + sb.append("ims"); + } else if (this instanceof com.android.services.telephony.GsmConnection) { + sb.append("gsm"); + } else if (this instanceof CdmaConnection) { + sb.append("cdma"); + } + sb.append(" state:"); + sb.append(Connection.stateToString(getState())); + sb.append(" capabilities:"); + sb.append(PhoneCapabilities.toString(getCallCapabilities())); + sb.append(" address:"); + sb.append(Log.pii(getAddress())); + sb.append(" originalConnection:"); + sb.append(mOriginalConnection); + sb.append(" partOfConf:"); + if (getConference() == null) { + sb.append("N"); + } else { + sb.append("Y"); + } + sb.append("]"); + return sb.toString(); + } } diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java index 8ef9ae886..7ce5005c0 100644 --- a/src/com/android/services/telephony/TelephonyConnectionService.java +++ b/src/com/android/services/telephony/TelephonyConnectionService.java @@ -52,6 +52,8 @@ public class TelephonyConnectionService extends ConnectionService { new TelephonyConferenceController(this); private final CdmaConferenceController mCdmaConferenceController = new CdmaConferenceController(this); + private final ImsConferenceController mImsConferenceController = + new ImsConferenceController(this); private ComponentName mExpectedComponentName = null; private EmergencyCallHelper mEmergencyCallHelper; private EmergencyTonePlayer mEmergencyTonePlayer; @@ -448,7 +450,7 @@ public class TelephonyConnectionService extends ConnectionService { // conference controllers first before re-adding it. if (connection.isImsConnection()) { Log.d(this, "Adding IMS connection to conference controller: " + connection); - mTelephonyConferenceController.add(connection); + mImsConferenceController.add(connection); } else { int phoneType = connection.getCall().getPhone().getPhoneType(); if (phoneType == TelephonyManager.PHONE_TYPE_GSM) { |