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