Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.telecom;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.annotation.SystemApi;
     22 import android.os.Bundle;
     23 import android.telecom.Connection.VideoProvider;
     24 import android.util.ArraySet;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Arrays;
     28 import java.util.Collections;
     29 import java.util.List;
     30 import java.util.Locale;
     31 import java.util.Objects;
     32 import java.util.Set;
     33 import java.util.concurrent.CopyOnWriteArrayList;
     34 import java.util.concurrent.CopyOnWriteArraySet;
     35 
     36 /**
     37  * Represents a conference call which can contain any number of {@link Connection} objects.
     38  */
     39 public abstract class Conference extends Conferenceable {
     40 
     41     /**
     42      * Used to indicate that the conference connection time is not specified.  If not specified,
     43      * Telecom will set the connect time.
     44      */
     45     public static final long CONNECT_TIME_NOT_SPECIFIED = 0;
     46 
     47     /** @hide */
     48     public abstract static class Listener {
     49         public void onStateChanged(Conference conference, int oldState, int newState) {}
     50         public void onDisconnected(Conference conference, DisconnectCause disconnectCause) {}
     51         public void onConnectionAdded(Conference conference, Connection connection) {}
     52         public void onConnectionRemoved(Conference conference, Connection connection) {}
     53         public void onConferenceableConnectionsChanged(
     54                 Conference conference, List<Connection> conferenceableConnections) {}
     55         public void onDestroyed(Conference conference) {}
     56         public void onConnectionCapabilitiesChanged(
     57                 Conference conference, int connectionCapabilities) {}
     58         public void onConnectionPropertiesChanged(
     59                 Conference conference, int connectionProperties) {}
     60         public void onVideoStateChanged(Conference c, int videoState) { }
     61         public void onVideoProviderChanged(Conference c, Connection.VideoProvider videoProvider) {}
     62         public void onStatusHintsChanged(Conference conference, StatusHints statusHints) {}
     63         public void onExtrasChanged(Conference c, Bundle extras) {}
     64         public void onExtrasRemoved(Conference c, List<String> keys) {}
     65     }
     66 
     67     private final Set<Listener> mListeners = new CopyOnWriteArraySet<>();
     68     private final List<Connection> mChildConnections = new CopyOnWriteArrayList<>();
     69     private final List<Connection> mUnmodifiableChildConnections =
     70             Collections.unmodifiableList(mChildConnections);
     71     private final List<Connection> mConferenceableConnections = new ArrayList<>();
     72     private final List<Connection> mUnmodifiableConferenceableConnections =
     73             Collections.unmodifiableList(mConferenceableConnections);
     74 
     75     private String mTelecomCallId;
     76     private PhoneAccountHandle mPhoneAccount;
     77     private CallAudioState mCallAudioState;
     78     private int mState = Connection.STATE_NEW;
     79     private DisconnectCause mDisconnectCause;
     80     private int mConnectionCapabilities;
     81     private int mConnectionProperties;
     82     private String mDisconnectMessage;
     83     private long mConnectTimeMillis = CONNECT_TIME_NOT_SPECIFIED;
     84     private StatusHints mStatusHints;
     85     private Bundle mExtras;
     86     private Set<String> mPreviousExtraKeys;
     87     private final Object mExtrasLock = new Object();
     88 
     89     private final Connection.Listener mConnectionDeathListener = new Connection.Listener() {
     90         @Override
     91         public void onDestroyed(Connection c) {
     92             if (mConferenceableConnections.remove(c)) {
     93                 fireOnConferenceableConnectionsChanged();
     94             }
     95         }
     96     };
     97 
     98     /**
     99      * Constructs a new Conference with a mandatory {@link PhoneAccountHandle}
    100      *
    101      * @param phoneAccount The {@code PhoneAccountHandle} associated with the conference.
    102      */
    103     public Conference(PhoneAccountHandle phoneAccount) {
    104         mPhoneAccount = phoneAccount;
    105     }
    106 
    107     /**
    108      * Returns the telecom internal call ID associated with this conference.
    109      *
    110      * @return The telecom call ID.
    111      * @hide
    112      */
    113     public final String getTelecomCallId() {
    114         return mTelecomCallId;
    115     }
    116 
    117     /**
    118      * Sets the telecom internal call ID associated with this conference.
    119      *
    120      * @param telecomCallId The telecom call ID.
    121      * @hide
    122      */
    123     public final void setTelecomCallId(String telecomCallId) {
    124         mTelecomCallId = telecomCallId;
    125     }
    126 
    127     /**
    128      * Returns the {@link PhoneAccountHandle} the conference call is being placed through.
    129      *
    130      * @return A {@code PhoneAccountHandle} object representing the PhoneAccount of the conference.
    131      */
    132     public final PhoneAccountHandle getPhoneAccountHandle() {
    133         return mPhoneAccount;
    134     }
    135 
    136     /**
    137      * Returns the list of connections currently associated with the conference call.
    138      *
    139      * @return A list of {@code Connection} objects which represent the children of the conference.
    140      */
    141     public final List<Connection> getConnections() {
    142         return mUnmodifiableChildConnections;
    143     }
    144 
    145     /**
    146      * Gets the state of the conference call. See {@link Connection} for valid values.
    147      *
    148      * @return A constant representing the state the conference call is currently in.
    149      */
    150     public final int getState() {
    151         return mState;
    152     }
    153 
    154     /**
    155      * Returns the capabilities of the conference. See {@code CAPABILITY_*} constants in class
    156      * {@link Connection} for valid values.
    157      *
    158      * @return A bitmask of the capabilities of the conference call.
    159      */
    160     public final int getConnectionCapabilities() {
    161         return mConnectionCapabilities;
    162     }
    163 
    164     /**
    165      * Returns the properties of the conference. See {@code PROPERTY_*} constants in class
    166      * {@link Connection} for valid values.
    167      *
    168      * @return A bitmask of the properties of the conference call.
    169      */
    170     public final int getConnectionProperties() {
    171         return mConnectionProperties;
    172     }
    173 
    174     /**
    175      * Whether the given capabilities support the specified capability.
    176      *
    177      * @param capabilities A capability bit field.
    178      * @param capability The capability to check capabilities for.
    179      * @return Whether the specified capability is supported.
    180      * @hide
    181      */
    182     public static boolean can(int capabilities, int capability) {
    183         return (capabilities & capability) != 0;
    184     }
    185 
    186     /**
    187      * Whether the capabilities of this {@code Connection} supports the specified capability.
    188      *
    189      * @param capability The capability to check capabilities for.
    190      * @return Whether the specified capability is supported.
    191      * @hide
    192      */
    193     public boolean can(int capability) {
    194         return can(mConnectionCapabilities, capability);
    195     }
    196 
    197     /**
    198      * Removes the specified capability from the set of capabilities of this {@code Conference}.
    199      *
    200      * @param capability The capability to remove from the set.
    201      * @hide
    202      */
    203     public void removeCapability(int capability) {
    204         int newCapabilities = mConnectionCapabilities;
    205         newCapabilities &= ~capability;
    206 
    207         setConnectionCapabilities(newCapabilities);
    208     }
    209 
    210     /**
    211      * Adds the specified capability to the set of capabilities of this {@code Conference}.
    212      *
    213      * @param capability The capability to add to the set.
    214      * @hide
    215      */
    216     public void addCapability(int capability) {
    217         int newCapabilities = mConnectionCapabilities;
    218         newCapabilities |= capability;
    219 
    220         setConnectionCapabilities(newCapabilities);
    221     }
    222 
    223     /**
    224      * @return The audio state of the conference, describing how its audio is currently
    225      *         being routed by the system. This is {@code null} if this Conference
    226      *         does not directly know about its audio state.
    227      * @deprecated Use {@link #getCallAudioState()} instead.
    228      * @hide
    229      */
    230     @Deprecated
    231     @SystemApi
    232     public final AudioState getAudioState() {
    233         return new AudioState(mCallAudioState);
    234     }
    235 
    236     /**
    237      * @return The audio state of the conference, describing how its audio is currently
    238      *         being routed by the system. This is {@code null} if this Conference
    239      *         does not directly know about its audio state.
    240      */
    241     public final CallAudioState getCallAudioState() {
    242         return mCallAudioState;
    243     }
    244 
    245     /**
    246      * Returns VideoProvider of the primary call. This can be null.
    247      */
    248     public VideoProvider getVideoProvider() {
    249         return null;
    250     }
    251 
    252     /**
    253      * Returns video state of the primary call.
    254      */
    255     public int getVideoState() {
    256         return VideoProfile.STATE_AUDIO_ONLY;
    257     }
    258 
    259     /**
    260      * Notifies the {@link Conference} when the Conference and all it's {@link Connection}s should
    261      * be disconnected.
    262      */
    263     public void onDisconnect() {}
    264 
    265     /**
    266      * Notifies the {@link Conference} when the specified {@link Connection} should be separated
    267      * from the conference call.
    268      *
    269      * @param connection The connection to separate.
    270      */
    271     public void onSeparate(Connection connection) {}
    272 
    273     /**
    274      * Notifies the {@link Conference} when the specified {@link Connection} should merged with the
    275      * conference call.
    276      *
    277      * @param connection The {@code Connection} to merge.
    278      */
    279     public void onMerge(Connection connection) {}
    280 
    281     /**
    282      * Notifies the {@link Conference} when it should be put on hold.
    283      */
    284     public void onHold() {}
    285 
    286     /**
    287      * Notifies the {@link Conference} when it should be moved from a held to active state.
    288      */
    289     public void onUnhold() {}
    290 
    291     /**
    292      * Notifies the {@link Conference} when the child calls should be merged.  Only invoked if the
    293      * conference contains the capability {@link Connection#CAPABILITY_MERGE_CONFERENCE}.
    294      */
    295     public void onMerge() {}
    296 
    297     /**
    298      * Notifies the {@link Conference} when the child calls should be swapped. Only invoked if the
    299      * conference contains the capability {@link Connection#CAPABILITY_SWAP_CONFERENCE}.
    300      */
    301     public void onSwap() {}
    302 
    303     /**
    304      * Notifies the {@link Conference} of a request to play a DTMF tone.
    305      *
    306      * @param c A DTMF character.
    307      */
    308     public void onPlayDtmfTone(char c) {}
    309 
    310     /**
    311      * Notifies the {@link Conference} of a request to stop any currently playing DTMF tones.
    312      */
    313     public void onStopDtmfTone() {}
    314 
    315     /**
    316      * Notifies the {@link Conference} that the {@link #getAudioState()} property has a new value.
    317      *
    318      * @param state The new call audio state.
    319      * @deprecated Use {@link #onCallAudioStateChanged(CallAudioState)} instead.
    320      * @hide
    321      */
    322     @SystemApi
    323     @Deprecated
    324     public void onAudioStateChanged(AudioState state) {}
    325 
    326     /**
    327      * Notifies the {@link Conference} that the {@link #getCallAudioState()} property has a new
    328      * value.
    329      *
    330      * @param state The new call audio state.
    331      */
    332     public void onCallAudioStateChanged(CallAudioState state) {}
    333 
    334     /**
    335      * Notifies the {@link Conference} that a {@link Connection} has been added to it.
    336      *
    337      * @param connection The newly added connection.
    338      */
    339     public void onConnectionAdded(Connection connection) {}
    340 
    341     /**
    342      * Sets state to be on hold.
    343      */
    344     public final void setOnHold() {
    345         setState(Connection.STATE_HOLDING);
    346     }
    347 
    348     /**
    349      * Sets state to be dialing.
    350      */
    351     public final void setDialing() {
    352         setState(Connection.STATE_DIALING);
    353     }
    354 
    355     /**
    356      * Sets state to be active.
    357      */
    358     public final void setActive() {
    359         setState(Connection.STATE_ACTIVE);
    360     }
    361 
    362     /**
    363      * Sets state to disconnected.
    364      *
    365      * @param disconnectCause The reason for the disconnection, as described by
    366      *     {@link android.telecom.DisconnectCause}.
    367      */
    368     public final void setDisconnected(DisconnectCause disconnectCause) {
    369         mDisconnectCause = disconnectCause;;
    370         setState(Connection.STATE_DISCONNECTED);
    371         for (Listener l : mListeners) {
    372             l.onDisconnected(this, mDisconnectCause);
    373         }
    374     }
    375 
    376     /**
    377      * @return The {@link DisconnectCause} for this connection.
    378      */
    379     public final DisconnectCause getDisconnectCause() {
    380         return mDisconnectCause;
    381     }
    382 
    383     /**
    384      * Sets the capabilities of a conference. See {@code CAPABILITY_*} constants of class
    385      * {@link Connection} for valid values.
    386      *
    387      * @param connectionCapabilities A bitmask of the {@code Capabilities} of the conference call.
    388      */
    389     public final void setConnectionCapabilities(int connectionCapabilities) {
    390         if (connectionCapabilities != mConnectionCapabilities) {
    391             mConnectionCapabilities = connectionCapabilities;
    392 
    393             for (Listener l : mListeners) {
    394                 l.onConnectionCapabilitiesChanged(this, mConnectionCapabilities);
    395             }
    396         }
    397     }
    398 
    399     /**
    400      * Sets the properties of a conference. See {@code PROPERTY_*} constants of class
    401      * {@link Connection} for valid values.
    402      *
    403      * @param connectionProperties A bitmask of the {@code Properties} of the conference call.
    404      */
    405     public final void setConnectionProperties(int connectionProperties) {
    406         if (connectionProperties != mConnectionProperties) {
    407             mConnectionProperties = connectionProperties;
    408 
    409             for (Listener l : mListeners) {
    410                 l.onConnectionPropertiesChanged(this, mConnectionProperties);
    411             }
    412         }
    413     }
    414 
    415     /**
    416      * Adds the specified connection as a child of this conference.
    417      *
    418      * @param connection The connection to add.
    419      * @return True if the connection was successfully added.
    420      */
    421     public final boolean addConnection(Connection connection) {
    422         Log.d(this, "Connection=%s, connection=", connection);
    423         if (connection != null && !mChildConnections.contains(connection)) {
    424             if (connection.setConference(this)) {
    425                 mChildConnections.add(connection);
    426                 onConnectionAdded(connection);
    427                 for (Listener l : mListeners) {
    428                     l.onConnectionAdded(this, connection);
    429                 }
    430                 return true;
    431             }
    432         }
    433         return false;
    434     }
    435 
    436     /**
    437      * Removes the specified connection as a child of this conference.
    438      *
    439      * @param connection The connection to remove.
    440      */
    441     public final void removeConnection(Connection connection) {
    442         Log.d(this, "removing %s from %s", connection, mChildConnections);
    443         if (connection != null && mChildConnections.remove(connection)) {
    444             connection.resetConference();
    445             for (Listener l : mListeners) {
    446                 l.onConnectionRemoved(this, connection);
    447             }
    448         }
    449     }
    450 
    451     /**
    452      * Sets the connections with which this connection can be conferenced.
    453      *
    454      * @param conferenceableConnections The set of connections this connection can conference with.
    455      */
    456     public final void setConferenceableConnections(List<Connection> conferenceableConnections) {
    457         if (Objects.equals(mConferenceableConnections, conferenceableConnections)) {
    458             return;
    459         }
    460 
    461         clearConferenceableList();
    462         for (Connection c : conferenceableConnections) {
    463             // If statement checks for duplicates in input. It makes it N^2 but we're dealing with a
    464             // small amount of items here.
    465             if (!mConferenceableConnections.contains(c)) {
    466                 c.addConnectionListener(mConnectionDeathListener);
    467                 mConferenceableConnections.add(c);
    468             }
    469         }
    470         fireOnConferenceableConnectionsChanged();
    471     }
    472 
    473     /**
    474      * Set the video state for the conference.
    475      * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY},
    476      * {@link VideoProfile#STATE_BIDIRECTIONAL},
    477      * {@link VideoProfile#STATE_TX_ENABLED},
    478      * {@link VideoProfile#STATE_RX_ENABLED}.
    479      *
    480      * @param videoState The new video state.
    481      */
    482     public final void setVideoState(Connection c, int videoState) {
    483         Log.d(this, "setVideoState Conference: %s Connection: %s VideoState: %s",
    484                 this, c, videoState);
    485         for (Listener l : mListeners) {
    486             l.onVideoStateChanged(this, videoState);
    487         }
    488     }
    489 
    490     /**
    491      * Sets the video connection provider.
    492      *
    493      * @param videoProvider The video provider.
    494      */
    495     public final void setVideoProvider(Connection c, Connection.VideoProvider videoProvider) {
    496         Log.d(this, "setVideoProvider Conference: %s Connection: %s VideoState: %s",
    497                 this, c, videoProvider);
    498         for (Listener l : mListeners) {
    499             l.onVideoProviderChanged(this, videoProvider);
    500         }
    501     }
    502 
    503     private final void fireOnConferenceableConnectionsChanged() {
    504         for (Listener l : mListeners) {
    505             l.onConferenceableConnectionsChanged(this, getConferenceableConnections());
    506         }
    507     }
    508 
    509     /**
    510      * Returns the connections with which this connection can be conferenced.
    511      */
    512     public final List<Connection> getConferenceableConnections() {
    513         return mUnmodifiableConferenceableConnections;
    514     }
    515 
    516     /**
    517      * Tears down the conference object and any of its current connections.
    518      */
    519     public final void destroy() {
    520         Log.d(this, "destroying conference : %s", this);
    521         // Tear down the children.
    522         for (Connection connection : mChildConnections) {
    523             Log.d(this, "removing connection %s", connection);
    524             removeConnection(connection);
    525         }
    526 
    527         // If not yet disconnected, set the conference call as disconnected first.
    528         if (mState != Connection.STATE_DISCONNECTED) {
    529             Log.d(this, "setting to disconnected");
    530             setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
    531         }
    532 
    533         // ...and notify.
    534         for (Listener l : mListeners) {
    535             l.onDestroyed(this);
    536         }
    537     }
    538 
    539     /**
    540      * Add a listener to be notified of a state change.
    541      *
    542      * @param listener The new listener.
    543      * @return This conference.
    544      * @hide
    545      */
    546     public final Conference addListener(Listener listener) {
    547         mListeners.add(listener);
    548         return this;
    549     }
    550 
    551     /**
    552      * Removes the specified listener.
    553      *
    554      * @param listener The listener to remove.
    555      * @return This conference.
    556      * @hide
    557      */
    558     public final Conference removeListener(Listener listener) {
    559         mListeners.remove(listener);
    560         return this;
    561     }
    562 
    563     /**
    564      * Retrieves the primary connection associated with the conference.  The primary connection is
    565      * the connection from which the conference will retrieve its current state.
    566      *
    567      * @return The primary connection.
    568      * @hide
    569      */
    570     @SystemApi
    571     public Connection getPrimaryConnection() {
    572         if (mUnmodifiableChildConnections == null || mUnmodifiableChildConnections.isEmpty()) {
    573             return null;
    574         }
    575         return mUnmodifiableChildConnections.get(0);
    576     }
    577 
    578     /**
    579      * @hide
    580      * @deprecated Use {@link #setConnectionTime}.
    581      */
    582     @Deprecated
    583     @SystemApi
    584     public final void setConnectTimeMillis(long connectTimeMillis) {
    585         setConnectionTime(connectTimeMillis);
    586     }
    587 
    588     /**
    589      * Sets the connection start time of the {@code Conference}.
    590      *
    591      * @param connectionTimeMillis The connection time, in milliseconds.
    592      */
    593     public final void setConnectionTime(long connectionTimeMillis) {
    594         mConnectTimeMillis = connectionTimeMillis;
    595     }
    596 
    597     /**
    598      * @hide
    599      * @deprecated Use {@link #getConnectionTime}.
    600      */
    601     @Deprecated
    602     @SystemApi
    603     public final long getConnectTimeMillis() {
    604         return getConnectionTime();
    605     }
    606 
    607     /**
    608      * Retrieves the connection start time of the {@code Conference}, if specified.  A value of
    609      * {@link #CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the start time
    610      * of the conference.
    611      *
    612      * @return The time at which the {@code Conference} was connected.
    613      */
    614     public final long getConnectionTime() {
    615         return mConnectTimeMillis;
    616     }
    617 
    618     /**
    619      * Inform this Conference that the state of its audio output has been changed externally.
    620      *
    621      * @param state The new audio state.
    622      * @hide
    623      */
    624     final void setCallAudioState(CallAudioState state) {
    625         Log.d(this, "setCallAudioState %s", state);
    626         mCallAudioState = state;
    627         onAudioStateChanged(getAudioState());
    628         onCallAudioStateChanged(state);
    629     }
    630 
    631     private void setState(int newState) {
    632         if (newState != Connection.STATE_ACTIVE &&
    633                 newState != Connection.STATE_HOLDING &&
    634                 newState != Connection.STATE_DISCONNECTED) {
    635             Log.w(this, "Unsupported state transition for Conference call.",
    636                     Connection.stateToString(newState));
    637             return;
    638         }
    639 
    640         if (mState != newState) {
    641             int oldState = mState;
    642             mState = newState;
    643             for (Listener l : mListeners) {
    644                 l.onStateChanged(this, oldState, newState);
    645             }
    646         }
    647     }
    648 
    649     private final void clearConferenceableList() {
    650         for (Connection c : mConferenceableConnections) {
    651             c.removeConnectionListener(mConnectionDeathListener);
    652         }
    653         mConferenceableConnections.clear();
    654     }
    655 
    656     @Override
    657     public String toString() {
    658         return String.format(Locale.US,
    659                 "[State: %s,Capabilites: %s, VideoState: %s, VideoProvider: %s, ThisObject %s]",
    660                 Connection.stateToString(mState),
    661                 Call.Details.capabilitiesToString(mConnectionCapabilities),
    662                 getVideoState(),
    663                 getVideoProvider(),
    664                 super.toString());
    665     }
    666 
    667     /**
    668      * Sets the label and icon status to display in the InCall UI.
    669      *
    670      * @param statusHints The status label and icon to set.
    671      */
    672     public final void setStatusHints(StatusHints statusHints) {
    673         mStatusHints = statusHints;
    674         for (Listener l : mListeners) {
    675             l.onStatusHintsChanged(this, statusHints);
    676         }
    677     }
    678 
    679     /**
    680      * @return The status hints for this conference.
    681      */
    682     public final StatusHints getStatusHints() {
    683         return mStatusHints;
    684     }
    685 
    686     /**
    687      * Replaces all the extras associated with this {@code Conference}.
    688      * <p>
    689      * New or existing keys are replaced in the {@code Conference} extras.  Keys which are no longer
    690      * in the new extras, but were present the last time {@code setExtras} was called are removed.
    691      * <p>
    692      * Alternatively you may use the {@link #putExtras(Bundle)}, and
    693      * {@link #removeExtras(String...)} methods to modify the extras.
    694      * <p>
    695      * No assumptions should be made as to how an In-Call UI or service will handle these extras.
    696      * Keys should be fully qualified (e.g., com.example.extras.MY_EXTRA) to avoid conflicts.
    697      *
    698      * @param extras The extras associated with this {@code Conference}.
    699      */
    700     public final void setExtras(@Nullable Bundle extras) {
    701         // Keeping putExtras and removeExtras in the same lock so that this operation happens as a
    702         // block instead of letting other threads put/remove while this method is running.
    703         synchronized (mExtrasLock) {
    704             // Add/replace any new or changed extras values.
    705             putExtras(extras);
    706             // If we have used "setExtras" in the past, compare the key set from the last invocation
    707             // to the current one and remove any keys that went away.
    708             if (mPreviousExtraKeys != null) {
    709                 List<String> toRemove = new ArrayList<String>();
    710                 for (String oldKey : mPreviousExtraKeys) {
    711                     if (extras == null || !extras.containsKey(oldKey)) {
    712                         toRemove.add(oldKey);
    713                     }
    714                 }
    715 
    716                 if (!toRemove.isEmpty()) {
    717                     removeExtras(toRemove);
    718                 }
    719             }
    720 
    721             // Track the keys the last time set called setExtras.  This way, the next time setExtras
    722             // is called we can see if the caller has removed any extras values.
    723             if (mPreviousExtraKeys == null) {
    724                 mPreviousExtraKeys = new ArraySet<String>();
    725             }
    726             mPreviousExtraKeys.clear();
    727             if (extras != null) {
    728                 mPreviousExtraKeys.addAll(extras.keySet());
    729             }
    730         }
    731     }
    732 
    733     /**
    734      * Adds some extras to this {@link Conference}.  Existing keys are replaced and new ones are
    735      * added.
    736      * <p>
    737      * No assumptions should be made as to how an In-Call UI or service will handle these extras.
    738      * Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts.
    739      *
    740      * @param extras The extras to add.
    741      */
    742     public final void putExtras(@NonNull Bundle extras) {
    743         if (extras == null) {
    744             return;
    745         }
    746 
    747         // Creating a Bundle clone so we don't have to synchronize on mExtrasLock while calling
    748         // onExtrasChanged.
    749         Bundle listenersBundle;
    750         synchronized (mExtrasLock) {
    751             if (mExtras == null) {
    752                 mExtras = new Bundle();
    753             }
    754             mExtras.putAll(extras);
    755             listenersBundle = new Bundle(mExtras);
    756         }
    757 
    758         for (Listener l : mListeners) {
    759             l.onExtrasChanged(this, new Bundle(listenersBundle));
    760         }
    761     }
    762 
    763     /**
    764      * Adds a boolean extra to this {@link Conference}.
    765      *
    766      * @param key The extra key.
    767      * @param value The value.
    768      * @hide
    769      */
    770     public final void putExtra(String key, boolean value) {
    771         Bundle newExtras = new Bundle();
    772         newExtras.putBoolean(key, value);
    773         putExtras(newExtras);
    774     }
    775 
    776     /**
    777      * Adds an integer extra to this {@link Conference}.
    778      *
    779      * @param key The extra key.
    780      * @param value The value.
    781      * @hide
    782      */
    783     public final void putExtra(String key, int value) {
    784         Bundle newExtras = new Bundle();
    785         newExtras.putInt(key, value);
    786         putExtras(newExtras);
    787     }
    788 
    789     /**
    790      * Adds a string extra to this {@link Conference}.
    791      *
    792      * @param key The extra key.
    793      * @param value The value.
    794      * @hide
    795      */
    796     public final void putExtra(String key, String value) {
    797         Bundle newExtras = new Bundle();
    798         newExtras.putString(key, value);
    799         putExtras(newExtras);
    800     }
    801 
    802     /**
    803      * Removes extras from this {@link Conference}.
    804      *
    805      * @param keys The keys of the extras to remove.
    806      */
    807     public final void removeExtras(List<String> keys) {
    808         if (keys == null || keys.isEmpty()) {
    809             return;
    810         }
    811 
    812         synchronized (mExtrasLock) {
    813             if (mExtras != null) {
    814                 for (String key : keys) {
    815                     mExtras.remove(key);
    816                 }
    817             }
    818         }
    819 
    820         List<String> unmodifiableKeys = Collections.unmodifiableList(keys);
    821         for (Listener l : mListeners) {
    822             l.onExtrasRemoved(this, unmodifiableKeys);
    823         }
    824     }
    825 
    826     /**
    827      * Removes extras from this {@link Conference}.
    828      *
    829      * @param keys The keys of the extras to remove.
    830      */
    831     public final void removeExtras(String ... keys) {
    832         removeExtras(Arrays.asList(keys));
    833     }
    834 
    835     /**
    836      * Returns the extras associated with this conference.
    837      * <p>
    838      * Extras should be updated using {@link #putExtras(Bundle)} and {@link #removeExtras(List)}.
    839      * <p>
    840      * Telecom or an {@link InCallService} can also update the extras via
    841      * {@link android.telecom.Call#putExtras(Bundle)}, and
    842      * {@link Call#removeExtras(List)}.
    843      * <p>
    844      * The conference is notified of changes to the extras made by Telecom or an
    845      * {@link InCallService} by {@link #onExtrasChanged(Bundle)}.
    846      *
    847      * @return The extras associated with this connection.
    848      */
    849     public final Bundle getExtras() {
    850         return mExtras;
    851     }
    852 
    853     /**
    854      * Notifies this {@link Conference} of a change to the extras made outside the
    855      * {@link ConnectionService}.
    856      * <p>
    857      * These extras changes can originate from Telecom itself, or from an {@link InCallService} via
    858      * {@link android.telecom.Call#putExtras(Bundle)}, and
    859      * {@link Call#removeExtras(List)}.
    860      *
    861      * @param extras The new extras bundle.
    862      */
    863     public void onExtrasChanged(Bundle extras) {}
    864 
    865     /**
    866      * Handles a change to extras received from Telecom.
    867      *
    868      * @param extras The new extras.
    869      * @hide
    870      */
    871     final void handleExtrasChanged(Bundle extras) {
    872         Bundle b = null;
    873         synchronized (mExtrasLock) {
    874             mExtras = extras;
    875             if (mExtras != null) {
    876                 b = new Bundle(mExtras);
    877             }
    878         }
    879         onExtrasChanged(b);
    880     }
    881 }
    882