/* * 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 android.telecom; import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import com.android.internal.telecom.IConnectionService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; /** * A conference provided to a {@link ConnectionService} by another {@code ConnectionService} through * {@link ConnectionService#conferenceRemoteConnections}. Once created, a {@code RemoteConference} * can be used to control the conference call or monitor changes through * {@link RemoteConnection.Callback}. * * @see ConnectionService#onRemoteConferenceAdded */ public final class RemoteConference { /** * Callback base class for {@link RemoteConference}. */ public abstract static class Callback { /** * Invoked when the state of this {@code RemoteConferece} has changed. See * {@link #getState()}. * * @param conference The {@code RemoteConference} invoking this method. * @param oldState The previous state of the {@code RemoteConference}. * @param newState The new state of the {@code RemoteConference}. */ public void onStateChanged(RemoteConference conference, int oldState, int newState) {} /** * Invoked when this {@code RemoteConference} is disconnected. * * @param conference The {@code RemoteConference} invoking this method. * @param disconnectCause The ({@see DisconnectCause}) associated with this failed * conference. */ public void onDisconnected(RemoteConference conference, DisconnectCause disconnectCause) {} /** * Invoked when a {@link RemoteConnection} is added to the conference call. * * @param conference The {@code RemoteConference} invoking this method. * @param connection The {@link RemoteConnection} being added. */ public void onConnectionAdded(RemoteConference conference, RemoteConnection connection) {} /** * Invoked when a {@link RemoteConnection} is removed from the conference call. * * @param conference The {@code RemoteConference} invoking this method. * @param connection The {@link RemoteConnection} being removed. */ public void onConnectionRemoved(RemoteConference conference, RemoteConnection connection) {} /** * Indicates that the call capabilities of this {@code RemoteConference} have changed. * See {@link #getConnectionCapabilities()}. * * @param conference The {@code RemoteConference} invoking this method. * @param connectionCapabilities The new capabilities of the {@code RemoteConference}. */ public void onConnectionCapabilitiesChanged( RemoteConference conference, int connectionCapabilities) {} /** * Indicates that the call properties of this {@code RemoteConference} have changed. * See {@link #getConnectionProperties()}. * * @param conference The {@code RemoteConference} invoking this method. * @param connectionProperties The new properties of the {@code RemoteConference}. */ public void onConnectionPropertiesChanged( RemoteConference conference, int connectionProperties) {} /** * Invoked when the set of {@link RemoteConnection}s which can be added to this conference * call have changed. * * @param conference The {@code RemoteConference} invoking this method. * @param conferenceableConnections The list of conferenceable {@link RemoteConnection}s. */ public void onConferenceableConnectionsChanged( RemoteConference conference, List conferenceableConnections) {} /** * Indicates that this {@code RemoteConference} has been destroyed. No further requests * should be made to the {@code RemoteConference}, and references to it should be cleared. * * @param conference The {@code RemoteConference} invoking this method. */ public void onDestroyed(RemoteConference conference) {} /** * Handles changes to the {@code RemoteConference} extras. * * @param conference The {@code RemoteConference} invoking this method. * @param extras The extras containing other information associated with the conference. */ public void onExtrasChanged(RemoteConference conference, @Nullable Bundle extras) {} } private final String mId; private final IConnectionService mConnectionService; private final Set> mCallbackRecords = new CopyOnWriteArraySet<>(); private final List mChildConnections = new CopyOnWriteArrayList<>(); private final List mUnmodifiableChildConnections = Collections.unmodifiableList(mChildConnections); private final List mConferenceableConnections = new ArrayList<>(); private final List mUnmodifiableConferenceableConnections = Collections.unmodifiableList(mConferenceableConnections); private int mState = Connection.STATE_NEW; private DisconnectCause mDisconnectCause; private int mConnectionCapabilities; private int mConnectionProperties; private Bundle mExtras; /** @hide */ RemoteConference(String id, IConnectionService connectionService) { mId = id; mConnectionService = connectionService; } /** @hide */ RemoteConference(DisconnectCause disconnectCause) { mId = "NULL"; mConnectionService = null; mState = Connection.STATE_DISCONNECTED; mDisconnectCause = disconnectCause; } /** @hide */ String getId() { return mId; } /** @hide */ void setDestroyed() { for (RemoteConnection connection : mChildConnections) { connection.setConference(null); } for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onDestroyed(conference); } }); } } /** @hide */ void setState(final int newState) { if (newState != Connection.STATE_ACTIVE && newState != Connection.STATE_HOLDING && newState != Connection.STATE_DISCONNECTED) { Log.w(this, "Unsupported state transition for Conference call.", Connection.stateToString(newState)); return; } if (mState != newState) { final int oldState = mState; mState = newState; for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onStateChanged(conference, oldState, newState); } }); } } } /** @hide */ void addConnection(final RemoteConnection connection) { if (!mChildConnections.contains(connection)) { mChildConnections.add(connection); connection.setConference(this); for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onConnectionAdded(conference, connection); } }); } } } /** @hide */ void removeConnection(final RemoteConnection connection) { if (mChildConnections.contains(connection)) { mChildConnections.remove(connection); connection.setConference(null); for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onConnectionRemoved(conference, connection); } }); } } } /** @hide */ void setConnectionCapabilities(final int connectionCapabilities) { if (mConnectionCapabilities != connectionCapabilities) { mConnectionCapabilities = connectionCapabilities; for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onConnectionCapabilitiesChanged( conference, mConnectionCapabilities); } }); } } } /** @hide */ void setConnectionProperties(final int connectionProperties) { if (mConnectionProperties != connectionProperties) { mConnectionProperties = connectionProperties; for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onConnectionPropertiesChanged( conference, mConnectionProperties); } }); } } } /** @hide */ void setConferenceableConnections(List conferenceableConnections) { mConferenceableConnections.clear(); mConferenceableConnections.addAll(conferenceableConnections); for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onConferenceableConnectionsChanged( conference, mUnmodifiableConferenceableConnections); } }); } } /** @hide */ void setDisconnected(final DisconnectCause disconnectCause) { if (mState != Connection.STATE_DISCONNECTED) { mDisconnectCause = disconnectCause; setState(Connection.STATE_DISCONNECTED); for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onDisconnected(conference, disconnectCause); } }); } } } /** @hide */ void putExtras(final Bundle extras) { if (extras == null) { return; } if (mExtras == null) { mExtras = new Bundle(); } mExtras.putAll(extras); notifyExtrasChanged(); } /** @hide */ void removeExtras(List keys) { if (mExtras == null || keys == null || keys.isEmpty()) { return; } for (String key : keys) { mExtras.remove(key); } notifyExtrasChanged(); } private void notifyExtrasChanged() { for (CallbackRecord record : mCallbackRecords) { final RemoteConference conference = this; final Callback callback = record.getCallback(); record.getHandler().post(new Runnable() { @Override public void run() { callback.onExtrasChanged(conference, mExtras); } }); } } /** * Returns the list of {@link RemoteConnection}s contained in this conference. * * @return A list of child connections. */ public final List getConnections() { return mUnmodifiableChildConnections; } /** * Gets the state of the conference call. See {@link Connection} for valid values. * * @return A constant representing the state the conference call is currently in. */ public final int getState() { return mState; } /** * Returns the capabilities of the conference. See {@code CAPABILITY_*} constants in class * {@link Connection} for valid values. * * @return A bitmask of the capabilities of the conference call. */ public final int getConnectionCapabilities() { return mConnectionCapabilities; } /** * Returns the properties of the conference. See {@code PROPERTY_*} constants in class * {@link Connection} for valid values. * * @return A bitmask of the properties of the conference call. */ public final int getConnectionProperties() { return mConnectionProperties; } /** * Obtain the extras associated with this {@code RemoteConnection}. * * @return The extras for this connection. */ public final Bundle getExtras() { return mExtras; } /** * Disconnects the conference call as well as the child {@link RemoteConnection}s. */ public void disconnect() { try { mConnectionService.disconnect(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Removes the specified {@link RemoteConnection} from the conference. This causes the * {@link RemoteConnection} to become a standalone connection. This is a no-op if the * {@link RemoteConnection} does not belong to this conference. * * @param connection The remote-connection to remove. */ public void separate(RemoteConnection connection) { if (mChildConnections.contains(connection)) { try { mConnectionService.splitFromConference(connection.getId(), null /*Session.Info*/); } catch (RemoteException e) { } } } /** * Merges all {@link RemoteConnection}s of this conference into a single call. This should be * invoked only if the conference contains the capability * {@link Connection#CAPABILITY_MERGE_CONFERENCE}, otherwise it is a no-op. The presence of said * capability indicates that the connections of this conference, despite being part of the * same conference object, are yet to have their audio streams merged; this is a common pattern * for CDMA conference calls, but the capability is not used for GSM and SIP conference calls. * Invoking this method will cause the unmerged child connections to merge their audio * streams. */ public void merge() { try { mConnectionService.mergeConference(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Swaps the active audio stream between the conference's child {@link RemoteConnection}s. * This should be invoked only if the conference contains the capability * {@link Connection#CAPABILITY_SWAP_CONFERENCE}, otherwise it is a no-op. This is only used by * {@link ConnectionService}s that create conferences for connections that do not yet have * their audio streams merged; this is a common pattern for CDMA conference calls, but the * capability is not used for GSM and SIP conference calls. Invoking this method will change the * active audio stream to a different child connection. */ public void swap() { try { mConnectionService.swapConference(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Puts the conference on hold. */ public void hold() { try { mConnectionService.hold(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Unholds the conference call. */ public void unhold() { try { mConnectionService.unhold(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Returns the {@link DisconnectCause} for the conference if it is in the state * {@link Connection#STATE_DISCONNECTED}. If the conference is not disconnected, this will * return null. * * @return The disconnect cause. */ public DisconnectCause getDisconnectCause() { return mDisconnectCause; } /** * Requests that the conference start playing the specified DTMF tone. * * @param digit The digit for which to play a DTMF tone. */ public void playDtmfTone(char digit) { try { mConnectionService.playDtmfTone(mId, digit, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Stops the most recent request to play a DTMF tone. * * @see #playDtmfTone */ public void stopDtmfTone() { try { mConnectionService.stopDtmfTone(mId, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Request to change the conference's audio routing to the specified state. The specified state * can include audio routing (Bluetooth, Speaker, etc) and muting state. * * @see android.telecom.AudioState * @deprecated Use {@link #setCallAudioState(CallAudioState)} instead. * @hide */ @SystemApi @Deprecated public void setAudioState(AudioState state) { setCallAudioState(new CallAudioState(state)); } /** * Request to change the conference's audio routing to the specified state. The specified state * can include audio routing (Bluetooth, Speaker, etc) and muting state. */ public void setCallAudioState(CallAudioState state) { try { mConnectionService.onCallAudioStateChanged(mId, state, null /*Session.Info*/); } catch (RemoteException e) { } } /** * Returns a list of independent connections that can me merged with this conference. * * @return A list of conferenceable connections. */ public List getConferenceableConnections() { return mUnmodifiableConferenceableConnections; } /** * Register a callback through which to receive state updates for this conference. * * @param callback The callback to notify of state changes. */ public final void registerCallback(Callback callback) { registerCallback(callback, new Handler()); } /** * Registers a callback through which to receive state updates for this conference. * Callbacks will be notified using the specified handler, if provided. * * @param callback The callback to notify of state changes. * @param handler The handler on which to execute the callbacks. */ public final void registerCallback(Callback callback, Handler handler) { unregisterCallback(callback); if (callback != null && handler != null) { mCallbackRecords.add(new CallbackRecord(callback, handler)); } } /** * Unregisters a previously registered callback. * * @see #registerCallback * * @param callback The callback to unregister. */ public final void unregisterCallback(Callback callback) { if (callback != null) { for (CallbackRecord record : mCallbackRecords) { if (record.getCallback() == callback) { mCallbackRecords.remove(record); break; } } } } /** * Create a {@link RemoteConference} represents a failure, and which will * be in {@link Connection#STATE_DISCONNECTED}. * * @param disconnectCause The disconnect cause. * @return a failed {@link RemoteConference} * @hide */ public static RemoteConference failure(DisconnectCause disconnectCause) { return new RemoteConference(disconnectCause); } }