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.Nullable;
20 import android.annotation.SystemApi;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.os.RemoteException;
24 
25 import com.android.internal.telecom.IConnectionService;
26 
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.concurrent.CopyOnWriteArrayList;
32 import java.util.concurrent.CopyOnWriteArraySet;
33 
34 /**
35  * A conference provided to a {@link ConnectionService} by another {@code ConnectionService} through
36  * {@link ConnectionService#conferenceRemoteConnections}. Once created, a {@code RemoteConference}
37  * can be used to control the conference call or monitor changes through
38  * {@link RemoteConnection.Callback}.
39  *
40  * @see ConnectionService#onRemoteConferenceAdded
41  */
42 public final class RemoteConference {
43 
44     /**
45      * Callback base class for {@link RemoteConference}.
46      */
47     public abstract static class Callback {
48         /**
49          * Invoked when the state of this {@code RemoteConferece} has changed. See
50          * {@link #getState()}.
51          *
52          * @param conference The {@code RemoteConference} invoking this method.
53          * @param oldState The previous state of the {@code RemoteConference}.
54          * @param newState The new state of the {@code RemoteConference}.
55          */
onStateChanged(RemoteConference conference, int oldState, int newState)56         public void onStateChanged(RemoteConference conference, int oldState, int newState) {}
57 
58         /**
59          * Invoked when this {@code RemoteConference} is disconnected.
60          *
61          * @param conference The {@code RemoteConference} invoking this method.
62          * @param disconnectCause The ({@see DisconnectCause}) associated with this failed
63          *     conference.
64          */
onDisconnected(RemoteConference conference, DisconnectCause disconnectCause)65         public void onDisconnected(RemoteConference conference, DisconnectCause disconnectCause) {}
66 
67         /**
68          * Invoked when a {@link RemoteConnection} is added to the conference call.
69          *
70          * @param conference The {@code RemoteConference} invoking this method.
71          * @param connection The {@link RemoteConnection} being added.
72          */
onConnectionAdded(RemoteConference conference, RemoteConnection connection)73         public void onConnectionAdded(RemoteConference conference, RemoteConnection connection) {}
74 
75         /**
76          * Invoked when a {@link RemoteConnection} is removed from the conference call.
77          *
78          * @param conference The {@code RemoteConference} invoking this method.
79          * @param connection The {@link RemoteConnection} being removed.
80          */
onConnectionRemoved(RemoteConference conference, RemoteConnection connection)81         public void onConnectionRemoved(RemoteConference conference, RemoteConnection connection) {}
82 
83         /**
84          * Indicates that the call capabilities of this {@code RemoteConference} have changed.
85          * See {@link #getConnectionCapabilities()}.
86          *
87          * @param conference The {@code RemoteConference} invoking this method.
88          * @param connectionCapabilities The new capabilities of the {@code RemoteConference}.
89          */
onConnectionCapabilitiesChanged( RemoteConference conference, int connectionCapabilities)90         public void onConnectionCapabilitiesChanged(
91                 RemoteConference conference,
92                 int connectionCapabilities) {}
93 
94         /**
95          * Indicates that the call properties of this {@code RemoteConference} have changed.
96          * See {@link #getConnectionProperties()}.
97          *
98          * @param conference The {@code RemoteConference} invoking this method.
99          * @param connectionProperties The new properties of the {@code RemoteConference}.
100          */
onConnectionPropertiesChanged( RemoteConference conference, int connectionProperties)101         public void onConnectionPropertiesChanged(
102                 RemoteConference conference,
103                 int connectionProperties) {}
104 
105 
106         /**
107          * Invoked when the set of {@link RemoteConnection}s which can be added to this conference
108          * call have changed.
109          *
110          * @param conference The {@code RemoteConference} invoking this method.
111          * @param conferenceableConnections The list of conferenceable {@link RemoteConnection}s.
112          */
onConferenceableConnectionsChanged( RemoteConference conference, List<RemoteConnection> conferenceableConnections)113         public void onConferenceableConnectionsChanged(
114                 RemoteConference conference,
115                 List<RemoteConnection> conferenceableConnections) {}
116 
117         /**
118          * Indicates that this {@code RemoteConference} has been destroyed. No further requests
119          * should be made to the {@code RemoteConference}, and references to it should be cleared.
120          *
121          * @param conference The {@code RemoteConference} invoking this method.
122          */
onDestroyed(RemoteConference conference)123         public void onDestroyed(RemoteConference conference) {}
124 
125         /**
126          * Handles changes to the {@code RemoteConference} extras.
127          *
128          * @param conference The {@code RemoteConference} invoking this method.
129          * @param extras The extras containing other information associated with the conference.
130          */
onExtrasChanged(RemoteConference conference, @Nullable Bundle extras)131         public void onExtrasChanged(RemoteConference conference, @Nullable Bundle extras) {}
132     }
133 
134     private final String mId;
135     private final IConnectionService mConnectionService;
136 
137     private final Set<CallbackRecord<Callback>> mCallbackRecords = new CopyOnWriteArraySet<>();
138     private final List<RemoteConnection> mChildConnections = new CopyOnWriteArrayList<>();
139     private final List<RemoteConnection> mUnmodifiableChildConnections =
140             Collections.unmodifiableList(mChildConnections);
141     private final List<RemoteConnection> mConferenceableConnections = new ArrayList<>();
142     private final List<RemoteConnection> mUnmodifiableConferenceableConnections =
143             Collections.unmodifiableList(mConferenceableConnections);
144 
145     private int mState = Connection.STATE_NEW;
146     private DisconnectCause mDisconnectCause;
147     private int mConnectionCapabilities;
148     private int mConnectionProperties;
149     private Bundle mExtras;
150 
151     /** @hide */
RemoteConference(String id, IConnectionService connectionService)152     RemoteConference(String id, IConnectionService connectionService) {
153         mId = id;
154         mConnectionService = connectionService;
155     }
156 
157     /** @hide */
RemoteConference(DisconnectCause disconnectCause)158     RemoteConference(DisconnectCause disconnectCause) {
159         mId = "NULL";
160         mConnectionService = null;
161         mState = Connection.STATE_DISCONNECTED;
162         mDisconnectCause = disconnectCause;
163     }
164 
165     /** @hide */
getId()166     String getId() {
167         return mId;
168     }
169 
170     /** @hide */
setDestroyed()171     void setDestroyed() {
172         for (RemoteConnection connection : mChildConnections) {
173             connection.setConference(null);
174         }
175         for (CallbackRecord<Callback> record : mCallbackRecords) {
176             final RemoteConference conference = this;
177             final Callback callback = record.getCallback();
178             record.getHandler().post(new Runnable() {
179                 @Override
180                 public void run() {
181                     callback.onDestroyed(conference);
182                 }
183             });
184         }
185     }
186 
187     /** @hide */
setState(final int newState)188     void setState(final int newState) {
189         if (newState != Connection.STATE_ACTIVE &&
190                 newState != Connection.STATE_HOLDING &&
191                 newState != Connection.STATE_DISCONNECTED) {
192             Log.w(this, "Unsupported state transition for Conference call.",
193                     Connection.stateToString(newState));
194             return;
195         }
196 
197         if (mState != newState) {
198             final int oldState = mState;
199             mState = newState;
200             for (CallbackRecord<Callback> record : mCallbackRecords) {
201                 final RemoteConference conference = this;
202                 final Callback callback = record.getCallback();
203                 record.getHandler().post(new Runnable() {
204                     @Override
205                     public void run() {
206                         callback.onStateChanged(conference, oldState, newState);
207                     }
208                 });
209             }
210         }
211     }
212 
213     /** @hide */
addConnection(final RemoteConnection connection)214     void addConnection(final RemoteConnection connection) {
215         if (!mChildConnections.contains(connection)) {
216             mChildConnections.add(connection);
217             connection.setConference(this);
218             for (CallbackRecord<Callback> record : mCallbackRecords) {
219                 final RemoteConference conference = this;
220                 final Callback callback = record.getCallback();
221                 record.getHandler().post(new Runnable() {
222                     @Override
223                     public void run() {
224                         callback.onConnectionAdded(conference, connection);
225                     }
226                 });
227             }
228         }
229     }
230 
231     /** @hide */
removeConnection(final RemoteConnection connection)232     void removeConnection(final RemoteConnection connection) {
233         if (mChildConnections.contains(connection)) {
234             mChildConnections.remove(connection);
235             connection.setConference(null);
236             for (CallbackRecord<Callback> record : mCallbackRecords) {
237                 final RemoteConference conference = this;
238                 final Callback callback = record.getCallback();
239                 record.getHandler().post(new Runnable() {
240                     @Override
241                     public void run() {
242                         callback.onConnectionRemoved(conference, connection);
243                     }
244                 });
245             }
246         }
247     }
248 
249     /** @hide */
setConnectionCapabilities(final int connectionCapabilities)250     void setConnectionCapabilities(final int connectionCapabilities) {
251         if (mConnectionCapabilities != connectionCapabilities) {
252             mConnectionCapabilities = connectionCapabilities;
253             for (CallbackRecord<Callback> record : mCallbackRecords) {
254                 final RemoteConference conference = this;
255                 final Callback callback = record.getCallback();
256                 record.getHandler().post(new Runnable() {
257                     @Override
258                     public void run() {
259                         callback.onConnectionCapabilitiesChanged(
260                                 conference, mConnectionCapabilities);
261                     }
262                 });
263             }
264         }
265     }
266 
267     /** @hide */
setConnectionProperties(final int connectionProperties)268     void setConnectionProperties(final int connectionProperties) {
269         if (mConnectionProperties != connectionProperties) {
270             mConnectionProperties = connectionProperties;
271             for (CallbackRecord<Callback> record : mCallbackRecords) {
272                 final RemoteConference conference = this;
273                 final Callback callback = record.getCallback();
274                 record.getHandler().post(new Runnable() {
275                     @Override
276                     public void run() {
277                         callback.onConnectionPropertiesChanged(
278                                 conference, mConnectionProperties);
279                     }
280                 });
281             }
282         }
283     }
284 
285     /** @hide */
setConferenceableConnections(List<RemoteConnection> conferenceableConnections)286     void setConferenceableConnections(List<RemoteConnection> conferenceableConnections) {
287         mConferenceableConnections.clear();
288         mConferenceableConnections.addAll(conferenceableConnections);
289         for (CallbackRecord<Callback> record : mCallbackRecords) {
290             final RemoteConference conference = this;
291             final Callback callback = record.getCallback();
292             record.getHandler().post(new Runnable() {
293                 @Override
294                 public void run() {
295                     callback.onConferenceableConnectionsChanged(
296                             conference, mUnmodifiableConferenceableConnections);
297                 }
298             });
299         }
300     }
301 
302     /** @hide */
setDisconnected(final DisconnectCause disconnectCause)303     void setDisconnected(final DisconnectCause disconnectCause) {
304         if (mState != Connection.STATE_DISCONNECTED) {
305             mDisconnectCause = disconnectCause;
306             setState(Connection.STATE_DISCONNECTED);
307             for (CallbackRecord<Callback> record : mCallbackRecords) {
308                 final RemoteConference conference = this;
309                 final Callback callback = record.getCallback();
310                 record.getHandler().post(new Runnable() {
311                     @Override
312                     public void run() {
313                         callback.onDisconnected(conference, disconnectCause);
314                     }
315                 });
316             }
317         }
318     }
319 
320     /** @hide */
putExtras(final Bundle extras)321     void putExtras(final Bundle extras) {
322         if (extras == null) {
323             return;
324         }
325         if (mExtras == null) {
326             mExtras = new Bundle();
327         }
328         mExtras.putAll(extras);
329 
330         notifyExtrasChanged();
331     }
332 
333     /** @hide */
removeExtras(List<String> keys)334     void removeExtras(List<String> keys) {
335         if (mExtras == null || keys == null || keys.isEmpty()) {
336             return;
337         }
338         for (String key : keys) {
339             mExtras.remove(key);
340         }
341 
342         notifyExtrasChanged();
343     }
344 
notifyExtrasChanged()345     private void notifyExtrasChanged() {
346         for (CallbackRecord<Callback> record : mCallbackRecords) {
347             final RemoteConference conference = this;
348             final Callback callback = record.getCallback();
349             record.getHandler().post(new Runnable() {
350                 @Override
351                 public void run() {
352                     callback.onExtrasChanged(conference, mExtras);
353                 }
354             });
355         }
356     }
357 
358     /**
359      * Returns the list of {@link RemoteConnection}s contained in this conference.
360      *
361      * @return A list of child connections.
362      */
getConnections()363     public final List<RemoteConnection> getConnections() {
364         return mUnmodifiableChildConnections;
365     }
366 
367     /**
368      * Gets the state of the conference call. See {@link Connection} for valid values.
369      *
370      * @return A constant representing the state the conference call is currently in.
371      */
getState()372     public final int getState() {
373         return mState;
374     }
375 
376     /**
377      * Returns the capabilities of the conference. See {@code CAPABILITY_*} constants in class
378      * {@link Connection} for valid values.
379      *
380      * @return A bitmask of the capabilities of the conference call.
381      */
getConnectionCapabilities()382     public final int getConnectionCapabilities() {
383         return mConnectionCapabilities;
384     }
385 
386     /**
387      * Returns the properties of the conference. See {@code PROPERTY_*} constants in class
388      * {@link Connection} for valid values.
389      *
390      * @return A bitmask of the properties of the conference call.
391      */
getConnectionProperties()392     public final int getConnectionProperties() {
393         return mConnectionProperties;
394     }
395 
396     /**
397      * Obtain the extras associated with this {@code RemoteConnection}.
398      *
399      * @return The extras for this connection.
400      */
getExtras()401     public final Bundle getExtras() {
402         return mExtras;
403     }
404 
405     /**
406      * Disconnects the conference call as well as the child {@link RemoteConnection}s.
407      */
disconnect()408     public void disconnect() {
409         try {
410             mConnectionService.disconnect(mId, null /*Session.Info*/);
411         } catch (RemoteException e) {
412         }
413     }
414 
415     /**
416      * Removes the specified {@link RemoteConnection} from the conference. This causes the
417      * {@link RemoteConnection} to become a standalone connection. This is a no-op if the
418      * {@link RemoteConnection} does not belong to this conference.
419      *
420      * @param connection The remote-connection to remove.
421      */
separate(RemoteConnection connection)422     public void separate(RemoteConnection connection) {
423         if (mChildConnections.contains(connection)) {
424             try {
425                 mConnectionService.splitFromConference(connection.getId(), null /*Session.Info*/);
426             } catch (RemoteException e) {
427             }
428         }
429     }
430 
431     /**
432      * Merges all {@link RemoteConnection}s of this conference into a single call. This should be
433      * invoked only if the conference contains the capability
434      * {@link Connection#CAPABILITY_MERGE_CONFERENCE}, otherwise it is a no-op. The presence of said
435      * capability indicates that the connections of this conference, despite being part of the
436      * same conference object, are yet to have their audio streams merged; this is a common pattern
437      * for CDMA conference calls, but the capability is not used for GSM and SIP conference calls.
438      * Invoking this method will cause the unmerged child connections to merge their audio
439      * streams.
440      */
merge()441     public void merge() {
442         try {
443             mConnectionService.mergeConference(mId, null /*Session.Info*/);
444         } catch (RemoteException e) {
445         }
446     }
447 
448     /**
449      * Swaps the active audio stream between the conference's child {@link RemoteConnection}s.
450      * This should be invoked only if the conference contains the capability
451      * {@link Connection#CAPABILITY_SWAP_CONFERENCE}, otherwise it is a no-op. This is only used by
452      * {@link ConnectionService}s that create conferences for connections that do not yet have
453      * their audio streams merged; this is a common pattern for CDMA conference calls, but the
454      * capability is not used for GSM and SIP conference calls. Invoking this method will change the
455      * active audio stream to a different child connection.
456      */
swap()457     public void swap() {
458         try {
459             mConnectionService.swapConference(mId, null /*Session.Info*/);
460         } catch (RemoteException e) {
461         }
462     }
463 
464     /**
465      * Puts the conference on hold.
466      */
hold()467     public void hold() {
468         try {
469             mConnectionService.hold(mId, null /*Session.Info*/);
470         } catch (RemoteException e) {
471         }
472     }
473 
474     /**
475      * Unholds the conference call.
476      */
unhold()477     public void unhold() {
478         try {
479             mConnectionService.unhold(mId, null /*Session.Info*/);
480         } catch (RemoteException e) {
481         }
482     }
483 
484     /**
485      * Returns the {@link DisconnectCause} for the conference if it is in the state
486      * {@link Connection#STATE_DISCONNECTED}. If the conference is not disconnected, this will
487      * return null.
488      *
489      * @return The disconnect cause.
490      */
getDisconnectCause()491     public DisconnectCause getDisconnectCause() {
492         return mDisconnectCause;
493     }
494 
495     /**
496      * Requests that the conference start playing the specified DTMF tone.
497      *
498      * @param digit The digit for which to play a DTMF tone.
499      */
playDtmfTone(char digit)500     public void playDtmfTone(char digit) {
501         try {
502             mConnectionService.playDtmfTone(mId, digit, null /*Session.Info*/);
503         } catch (RemoteException e) {
504         }
505     }
506 
507     /**
508      * Stops the most recent request to play a DTMF tone.
509      *
510      * @see #playDtmfTone
511      */
stopDtmfTone()512     public void stopDtmfTone() {
513         try {
514             mConnectionService.stopDtmfTone(mId, null /*Session.Info*/);
515         } catch (RemoteException e) {
516         }
517     }
518 
519     /**
520      * Request to change the conference's audio routing to the specified state. The specified state
521      * can include audio routing (Bluetooth, Speaker, etc) and muting state.
522      *
523      * @see android.telecom.AudioState
524      * @deprecated Use {@link #setCallAudioState(CallAudioState)} instead.
525      * @hide
526      */
527     @SystemApi
528     @Deprecated
setAudioState(AudioState state)529     public void setAudioState(AudioState state) {
530         setCallAudioState(new CallAudioState(state));
531     }
532 
533     /**
534      * Request to change the conference's audio routing to the specified state. The specified state
535      * can include audio routing (Bluetooth, Speaker, etc) and muting state.
536      */
setCallAudioState(CallAudioState state)537     public void setCallAudioState(CallAudioState state) {
538         try {
539             mConnectionService.onCallAudioStateChanged(mId, state, null /*Session.Info*/);
540         } catch (RemoteException e) {
541         }
542     }
543 
544 
545     /**
546      * Returns a list of independent connections that can me merged with this conference.
547      *
548      * @return A list of conferenceable connections.
549      */
getConferenceableConnections()550     public List<RemoteConnection> getConferenceableConnections() {
551         return mUnmodifiableConferenceableConnections;
552     }
553 
554     /**
555      * Register a callback through which to receive state updates for this conference.
556      *
557      * @param callback The callback to notify of state changes.
558      */
registerCallback(Callback callback)559     public final void registerCallback(Callback callback) {
560         registerCallback(callback, new Handler());
561     }
562 
563     /**
564      * Registers a callback through which to receive state updates for this conference.
565      * Callbacks will be notified using the specified handler, if provided.
566      *
567      * @param callback The callback to notify of state changes.
568      * @param handler The handler on which to execute the callbacks.
569      */
registerCallback(Callback callback, Handler handler)570     public final void registerCallback(Callback callback, Handler handler) {
571         unregisterCallback(callback);
572         if (callback != null && handler != null) {
573             mCallbackRecords.add(new CallbackRecord(callback, handler));
574         }
575     }
576 
577     /**
578      * Unregisters a previously registered callback.
579      *
580      * @see #registerCallback
581      *
582      * @param callback The callback to unregister.
583      */
unregisterCallback(Callback callback)584     public final void unregisterCallback(Callback callback) {
585         if (callback != null) {
586             for (CallbackRecord<Callback> record : mCallbackRecords) {
587                 if (record.getCallback() == callback) {
588                     mCallbackRecords.remove(record);
589                     break;
590                 }
591             }
592         }
593     }
594 
595     /**
596      * Create a {@link RemoteConference} represents a failure, and which will
597      * be in {@link Connection#STATE_DISCONNECTED}.
598      *
599      * @param disconnectCause The disconnect cause.
600      * @return a failed {@link RemoteConference}
601      * @hide
602      */
failure(DisconnectCause disconnectCause)603     public static RemoteConference failure(DisconnectCause disconnectCause) {
604         return new RemoteConference(disconnectCause);
605     }
606 }
607