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.SystemApi;
20
21import java.util.ArrayList;
22import java.util.Collections;
23import java.util.List;
24import java.util.Set;
25import java.util.concurrent.CopyOnWriteArrayList;
26import java.util.concurrent.CopyOnWriteArraySet;
27
28/**
29 * Represents a conference call which can contain any number of {@link Connection} objects.
30 * @hide
31 */
32@SystemApi
33public abstract class Conference implements IConferenceable {
34
35    /**
36     * Used to indicate that the conference connection time is not specified.  If not specified,
37     * Telecom will set the connect time.
38     */
39    public static long CONNECT_TIME_NOT_SPECIFIED = 0;
40
41    /** @hide */
42    public abstract static class Listener {
43        public void onStateChanged(Conference conference, int oldState, int newState) {}
44        public void onDisconnected(Conference conference, DisconnectCause disconnectCause) {}
45        public void onConnectionAdded(Conference conference, Connection connection) {}
46        public void onConnectionRemoved(Conference conference, Connection connection) {}
47        public void onConferenceableConnectionsChanged(
48                Conference conference, List<Connection> conferenceableConnections) {}
49        public void onDestroyed(Conference conference) {}
50        public void onConnectionCapabilitiesChanged(
51                Conference conference, int connectionCapabilities) {}
52    }
53
54    private final Set<Listener> mListeners = new CopyOnWriteArraySet<>();
55    private final List<Connection> mChildConnections = new CopyOnWriteArrayList<>();
56    private final List<Connection> mUnmodifiableChildConnections =
57            Collections.unmodifiableList(mChildConnections);
58    private final List<Connection> mConferenceableConnections = new ArrayList<>();
59    private final List<Connection> mUnmodifiableConferenceableConnections =
60            Collections.unmodifiableList(mConferenceableConnections);
61
62    protected PhoneAccountHandle mPhoneAccount;
63    private AudioState mAudioState;
64    private int mState = Connection.STATE_NEW;
65    private DisconnectCause mDisconnectCause;
66    private int mConnectionCapabilities;
67    private String mDisconnectMessage;
68    private long mConnectTimeMillis = CONNECT_TIME_NOT_SPECIFIED;
69
70    private final Connection.Listener mConnectionDeathListener = new Connection.Listener() {
71        @Override
72        public void onDestroyed(Connection c) {
73            if (mConferenceableConnections.remove(c)) {
74                fireOnConferenceableConnectionsChanged();
75            }
76        }
77    };
78
79    /**
80     * Constructs a new Conference with a mandatory {@link PhoneAccountHandle}
81     *
82     * @param phoneAccount The {@code PhoneAccountHandle} associated with the conference.
83     */
84    public Conference(PhoneAccountHandle phoneAccount) {
85        mPhoneAccount = phoneAccount;
86    }
87
88    /**
89     * Returns the {@link PhoneAccountHandle} the conference call is being placed through.
90     *
91     * @return A {@code PhoneAccountHandle} object representing the PhoneAccount of the conference.
92     */
93    public final PhoneAccountHandle getPhoneAccountHandle() {
94        return mPhoneAccount;
95    }
96
97    /**
98     * Returns the list of connections currently associated with the conference call.
99     *
100     * @return A list of {@code Connection} objects which represent the children of the conference.
101     */
102    public final List<Connection> getConnections() {
103        return mUnmodifiableChildConnections;
104    }
105
106    /**
107     * Gets the state of the conference call. See {@link Connection} for valid values.
108     *
109     * @return A constant representing the state the conference call is currently in.
110     */
111    public final int getState() {
112        return mState;
113    }
114
115    /** @hide */
116    @Deprecated public final int getCapabilities() {
117        return getConnectionCapabilities();
118    }
119
120    /**
121     * Returns the capabilities of a conference. See {@code CAPABILITY_*} constants in class
122     * {@link Connection} for valid values.
123     *
124     * @return A bitmask of the capabilities of the conference call.
125     */
126    public final int getConnectionCapabilities() {
127        return mConnectionCapabilities;
128    }
129
130    /**
131     * Whether the given capabilities support the specified capability.
132     *
133     * @param capabilities A capability bit field.
134     * @param capability The capability to check capabilities for.
135     * @return Whether the specified capability is supported.
136     * @hide
137     */
138    public static boolean can(int capabilities, int capability) {
139        return (capabilities & capability) != 0;
140    }
141
142    /**
143     * Whether the capabilities of this {@code Connection} supports the specified capability.
144     *
145     * @param capability The capability to check capabilities for.
146     * @return Whether the specified capability is supported.
147     * @hide
148     */
149    public boolean can(int capability) {
150        return can(mConnectionCapabilities, capability);
151    }
152
153    /**
154     * Removes the specified capability from the set of capabilities of this {@code Conference}.
155     *
156     * @param capability The capability to remove from the set.
157     * @hide
158     */
159    public void removeCapability(int capability) {
160        mConnectionCapabilities &= ~capability;
161    }
162
163    /**
164     * Adds the specified capability to the set of capabilities of this {@code Conference}.
165     *
166     * @param capability The capability to add to the set.
167     * @hide
168     */
169    public void addCapability(int capability) {
170        mConnectionCapabilities |= capability;
171    }
172
173    /**
174     * @return The audio state of the conference, describing how its audio is currently
175     *         being routed by the system. This is {@code null} if this Conference
176     *         does not directly know about its audio state.
177     */
178    public final AudioState getAudioState() {
179        return mAudioState;
180    }
181
182    /**
183     * Invoked when the Conference and all it's {@link Connection}s should be disconnected.
184     */
185    public void onDisconnect() {}
186
187    /**
188     * Invoked when the specified {@link Connection} should be separated from the conference call.
189     *
190     * @param connection The connection to separate.
191     */
192    public void onSeparate(Connection connection) {}
193
194    /**
195     * Invoked when the specified {@link Connection} should merged with the conference call.
196     *
197     * @param connection The {@code Connection} to merge.
198     */
199    public void onMerge(Connection connection) {}
200
201    /**
202     * Invoked when the conference should be put on hold.
203     */
204    public void onHold() {}
205
206    /**
207     * Invoked when the conference should be moved from hold to active.
208     */
209    public void onUnhold() {}
210
211    /**
212     * Invoked when the child calls should be merged. Only invoked if the conference contains the
213     * capability {@link Connection#CAPABILITY_MERGE_CONFERENCE}.
214     */
215    public void onMerge() {}
216
217    /**
218     * Invoked when the child calls should be swapped. Only invoked if the conference contains the
219     * capability {@link Connection#CAPABILITY_SWAP_CONFERENCE}.
220     */
221    public void onSwap() {}
222
223    /**
224     * Notifies this conference of a request to play a DTMF tone.
225     *
226     * @param c A DTMF character.
227     */
228    public void onPlayDtmfTone(char c) {}
229
230    /**
231     * Notifies this conference of a request to stop any currently playing DTMF tones.
232     */
233    public void onStopDtmfTone() {}
234
235    /**
236     * Notifies this conference that the {@link #getAudioState()} property has a new value.
237     *
238     * @param state The new call audio state.
239     */
240    public void onAudioStateChanged(AudioState state) {}
241
242    /**
243     * Notifies this conference that a connection has been added to it.
244     *
245     * @param connection The newly added connection.
246     */
247    public void onConnectionAdded(Connection connection) {}
248
249    /**
250     * Sets state to be on hold.
251     */
252    public final void setOnHold() {
253        setState(Connection.STATE_HOLDING);
254    }
255
256    /**
257     * Sets state to be active.
258     */
259    public final void setActive() {
260        setState(Connection.STATE_ACTIVE);
261    }
262
263    /**
264     * Sets state to disconnected.
265     *
266     * @param disconnectCause The reason for the disconnection, as described by
267     *     {@link android.telecom.DisconnectCause}.
268     */
269    public final void setDisconnected(DisconnectCause disconnectCause) {
270        mDisconnectCause = disconnectCause;;
271        setState(Connection.STATE_DISCONNECTED);
272        for (Listener l : mListeners) {
273            l.onDisconnected(this, mDisconnectCause);
274        }
275    }
276
277    /**
278     * @return The {@link DisconnectCause} for this connection.
279     */
280    public final DisconnectCause getDisconnectCause() {
281        return mDisconnectCause;
282    }
283
284    /** @hide */
285    @Deprecated public final void setCapabilities(int connectionCapabilities) {
286        setConnectionCapabilities(connectionCapabilities);
287    }
288
289    /**
290     * Sets the capabilities of a conference. See {@code CAPABILITY_*} constants of class
291     * {@link Connection} for valid values.
292     *
293     * @param connectionCapabilities A bitmask of the {@code PhoneCapabilities} of the conference call.
294     */
295    public final void setConnectionCapabilities(int connectionCapabilities) {
296        if (connectionCapabilities != mConnectionCapabilities) {
297            mConnectionCapabilities = connectionCapabilities;
298
299            for (Listener l : mListeners) {
300                l.onConnectionCapabilitiesChanged(this, mConnectionCapabilities);
301            }
302        }
303    }
304
305    /**
306     * Adds the specified connection as a child of this conference.
307     *
308     * @param connection The connection to add.
309     * @return True if the connection was successfully added.
310     */
311    public final boolean addConnection(Connection connection) {
312        if (connection != null && !mChildConnections.contains(connection)) {
313            if (connection.setConference(this)) {
314                mChildConnections.add(connection);
315                onConnectionAdded(connection);
316                for (Listener l : mListeners) {
317                    l.onConnectionAdded(this, connection);
318                }
319                return true;
320            }
321        }
322        return false;
323    }
324
325    /**
326     * Removes the specified connection as a child of this conference.
327     *
328     * @param connection The connection to remove.
329     */
330    public final void removeConnection(Connection connection) {
331        Log.d(this, "removing %s from %s", connection, mChildConnections);
332        if (connection != null && mChildConnections.remove(connection)) {
333            connection.resetConference();
334            for (Listener l : mListeners) {
335                l.onConnectionRemoved(this, connection);
336            }
337        }
338    }
339
340    /**
341     * Sets the connections with which this connection can be conferenced.
342     *
343     * @param conferenceableConnections The set of connections this connection can conference with.
344     */
345    public final void setConferenceableConnections(List<Connection> conferenceableConnections) {
346        clearConferenceableList();
347        for (Connection c : conferenceableConnections) {
348            // If statement checks for duplicates in input. It makes it N^2 but we're dealing with a
349            // small amount of items here.
350            if (!mConferenceableConnections.contains(c)) {
351                c.addConnectionListener(mConnectionDeathListener);
352                mConferenceableConnections.add(c);
353            }
354        }
355        fireOnConferenceableConnectionsChanged();
356    }
357
358    private final void fireOnConferenceableConnectionsChanged() {
359        for (Listener l : mListeners) {
360            l.onConferenceableConnectionsChanged(this, getConferenceableConnections());
361        }
362    }
363
364    /**
365     * Returns the connections with which this connection can be conferenced.
366     */
367    public final List<Connection> getConferenceableConnections() {
368        return mUnmodifiableConferenceableConnections;
369    }
370
371    /**
372     * Tears down the conference object and any of its current connections.
373     */
374    public final void destroy() {
375        Log.d(this, "destroying conference : %s", this);
376        // Tear down the children.
377        for (Connection connection : mChildConnections) {
378            Log.d(this, "removing connection %s", connection);
379            removeConnection(connection);
380        }
381
382        // If not yet disconnected, set the conference call as disconnected first.
383        if (mState != Connection.STATE_DISCONNECTED) {
384            Log.d(this, "setting to disconnected");
385            setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
386        }
387
388        // ...and notify.
389        for (Listener l : mListeners) {
390            l.onDestroyed(this);
391        }
392    }
393
394    /**
395     * Add a listener to be notified of a state change.
396     *
397     * @param listener The new listener.
398     * @return This conference.
399     * @hide
400     */
401    public final Conference addListener(Listener listener) {
402        mListeners.add(listener);
403        return this;
404    }
405
406    /**
407     * Removes the specified listener.
408     *
409     * @param listener The listener to remove.
410     * @return This conference.
411     * @hide
412     */
413    public final Conference removeListener(Listener listener) {
414        mListeners.remove(listener);
415        return this;
416    }
417
418    /**
419     * Retrieves the primary connection associated with the conference.  The primary connection is
420     * the connection from which the conference will retrieve its current state.
421     *
422     * @return The primary connection.
423     */
424    public Connection getPrimaryConnection() {
425        if (mUnmodifiableChildConnections == null || mUnmodifiableChildConnections.isEmpty()) {
426            return null;
427        }
428        return mUnmodifiableChildConnections.get(0);
429    }
430
431    /**
432     * Sets the connect time of the {@code Conference}.
433     *
434     * @param connectTimeMillis The connection time, in milliseconds.
435     */
436    public void setConnectTimeMillis(long connectTimeMillis) {
437        mConnectTimeMillis = connectTimeMillis;
438    }
439
440    /**
441     * Retrieves the connect time of the {@code Conference}, if specified.  A value of
442     * {@link #CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the start time
443     * of the conference.
444     *
445     * @return The time the {@code Conference} has been connected.
446     */
447    public long getConnectTimeMillis() {
448        return mConnectTimeMillis;
449    }
450
451    /**
452     * Inform this Conference that the state of its audio output has been changed externally.
453     *
454     * @param state The new audio state.
455     * @hide
456     */
457    final void setAudioState(AudioState state) {
458        Log.d(this, "setAudioState %s", state);
459        mAudioState = state;
460        onAudioStateChanged(state);
461    }
462
463    private void setState(int newState) {
464        if (newState != Connection.STATE_ACTIVE &&
465                newState != Connection.STATE_HOLDING &&
466                newState != Connection.STATE_DISCONNECTED) {
467            Log.w(this, "Unsupported state transition for Conference call.",
468                    Connection.stateToString(newState));
469            return;
470        }
471
472        if (mState != newState) {
473            int oldState = mState;
474            mState = newState;
475            for (Listener l : mListeners) {
476                l.onStateChanged(this, oldState, newState);
477            }
478        }
479    }
480
481    private final void clearConferenceableList() {
482        for (Connection c : mConferenceableConnections) {
483            c.removeConnectionListener(mConnectionDeathListener);
484        }
485        mConferenceableConnections.clear();
486    }
487}
488