/*
* 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 com.android.internal.telecom.IConnectionService;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
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<RemoteConnection> 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<CallbackRecord<Callback>> mCallbackRecords = new CopyOnWriteArraySet<>();
private final List<RemoteConnection> mChildConnections = new CopyOnWriteArrayList<>();
private final List<RemoteConnection> mUnmodifiableChildConnections =
Collections.unmodifiableList(mChildConnections);
private final List<RemoteConnection> mConferenceableConnections = new ArrayList<>();
private final List<RemoteConnection> 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 */
String getId() {
return mId;
}
/** @hide */
void setDestroyed() {
for (RemoteConnection connection : mChildConnections) {
connection.setConference(null);
}
for (CallbackRecord<Callback> 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<Callback> 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<Callback> 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<Callback> 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<Callback> 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<Callback> 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<RemoteConnection> conferenceableConnections) {
mConferenceableConnections.clear();
mConferenceableConnections.addAll(conferenceableConnections);
for (CallbackRecord<Callback> 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<Callback> 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<String> keys) {
if (mExtras == null || keys == null || keys.isEmpty()) {
return;
}
for (String key : keys) {
mExtras.remove(key);
}
notifyExtrasChanged();
}
private void notifyExtrasChanged() {
for (CallbackRecord<Callback> 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<RemoteConnection> 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<RemoteConnection> 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<Callback> record : mCallbackRecords) {
if (record.getCallback() == callback) {
mCallbackRecords.remove(record);
break;
}
}
}
}
}