content: /*
   * 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;
                  }
              }
          }
      }
  }
  