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