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 com.android.services.telephony;
18
19import com.android.internal.telephony.Phone;
20import com.android.internal.telephony.PhoneConstants;
21import com.android.phone.PhoneUtils;
22
23import android.telecom.Conference;
24import android.telecom.Connection;
25import android.telecom.ConnectionService;
26import android.telecom.DisconnectCause;
27import android.telecom.Conferenceable;
28import android.telecom.PhoneAccountHandle;
29
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.Iterator;
33import java.util.List;
34
35/**
36 * Manages conferences for IMS connections.
37 */
38public class ImsConferenceController {
39
40    /**
41     * Conference listener; used to receive notification when a conference has been disconnected.
42     */
43    private final Conference.Listener mConferenceListener = new Conference.Listener() {
44        @Override
45        public void onDestroyed(Conference conference) {
46            if (Log.VERBOSE) {
47                Log.v(ImsConferenceController.class, "onDestroyed: %s", conference);
48            }
49
50            mImsConferences.remove(conference);
51        }
52    };
53
54    /**
55     * Ims conference controller connection listener.  Used to respond to changes in state of the
56     * Telephony connections the controller is aware of.
57     */
58    private final Connection.Listener mConnectionListener = new Connection.Listener() {
59        @Override
60        public void onStateChanged(Connection c, int state) {
61            Log.v(this, "onStateChanged: %s", Log.pii(c.getAddress()));
62            recalculate();
63        }
64
65        @Override
66        public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
67            Log.v(this, "onDisconnected: %s", Log.pii(c.getAddress()));
68            recalculate();
69        }
70
71        @Override
72        public void onDestroyed(Connection connection) {
73            remove(connection);
74        }
75
76        @Override
77        public void onConferenceStarted() {
78            Log.v(this, "onConferenceStarted");
79            recalculate();
80        }
81    };
82
83    /**
84     * The current {@link ConnectionService}.
85     */
86    private final TelephonyConnectionService mConnectionService;
87
88    /**
89     * List of known {@link TelephonyConnection}s.
90     */
91    private final ArrayList<TelephonyConnection> mTelephonyConnections = new ArrayList<>();
92
93    /**
94     * List of known {@link ImsConference}s.  Realistically there will only ever be a single
95     * concurrent IMS conference.
96     */
97    private final ArrayList<ImsConference> mImsConferences = new ArrayList<>(1);
98
99    /**
100     * Creates a new instance of the Ims conference controller.
101     *
102     * @param connectionService The current connection service.
103     */
104    public ImsConferenceController(TelephonyConnectionService connectionService) {
105        mConnectionService = connectionService;
106    }
107
108    /**
109     * Adds a new connection to the IMS conference controller.
110     *
111     * @param connection
112     */
113    void add(TelephonyConnection connection) {
114        // Note: Wrap in Log.VERBOSE to avoid calling connection.toString if we are not going to be
115        // outputting the value.
116        if (Log.VERBOSE) {
117            Log.v(this, "add connection %s", connection);
118        }
119
120        mTelephonyConnections.add(connection);
121        connection.addConnectionListener(mConnectionListener);
122        recalculateConference();
123    }
124
125    /**
126     * Removes a connection from the IMS conference controller.
127     *
128     * @param connection
129     */
130    void remove(Connection connection) {
131        if (Log.VERBOSE) {
132            Log.v(this, "remove connection: %s", connection);
133        }
134
135        mTelephonyConnections.remove(connection);
136        recalculateConferenceable();
137    }
138
139    /**
140     * Triggers both a re-check of conferenceable connections, as well as checking for new
141     * conferences.
142     */
143    private void recalculate() {
144        recalculateConferenceable();
145        recalculateConference();
146    }
147
148    /**
149     * Calculates the conference-capable state of all GSM connections in this connection service.
150     */
151    private void recalculateConferenceable() {
152        Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
153        List<Conferenceable> activeConnections = new ArrayList<>(mTelephonyConnections.size());
154        List<Conferenceable> backgroundConnections = new ArrayList<>(mTelephonyConnections.size());
155
156        // Loop through and collect all calls which are active or holding
157        for (TelephonyConnection connection : mTelephonyConnections) {
158            if (Log.DEBUG) {
159                Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection,
160                        connection.isConferenceSupported());
161            }
162
163            // If this connection is a member of a conference hosted on another device, it is not
164            // conferenceable with any other connections.
165            if (isMemberOfPeerConference(connection)) {
166                if (Log.VERBOSE) {
167                    Log.v(this, "Skipping connection in peer conference: %s", connection);
168                }
169                continue;
170            }
171
172            // If this connection does not support being in a conference call, then it is not
173            // conferenceable with any other connection.
174            if (!connection.isConferenceSupported()) {
175                continue;
176            }
177
178            switch (connection.getState()) {
179                case Connection.STATE_ACTIVE:
180                    activeConnections.add(connection);
181                    continue;
182                case Connection.STATE_HOLDING:
183                    backgroundConnections.add(connection);
184                    continue;
185                default:
186                    break;
187            }
188            connection.setConferenceableConnections(Collections.<Connection>emptyList());
189        }
190
191        for (ImsConference conference : mImsConferences) {
192            if (Log.DEBUG) {
193                Log.d(this, "recalc - %s %s", conference.getState(), conference);
194            }
195
196            if (!conference.isConferenceHost()) {
197                if (Log.VERBOSE) {
198                    Log.v(this, "skipping conference (not hosted on this device): %s", conference);
199                }
200                continue;
201            }
202
203            switch (conference.getState()) {
204                case Connection.STATE_ACTIVE:
205                    activeConnections.add(conference);
206                    continue;
207                case Connection.STATE_HOLDING:
208                    backgroundConnections.add(conference);
209                    continue;
210                default:
211                    break;
212            }
213        }
214
215        Log.v(this, "active: %d, holding: %d", activeConnections.size(),
216                backgroundConnections.size());
217
218        // Go through all the active connections and set the background connections as
219        // conferenceable.
220        for (Conferenceable conferenceable : activeConnections) {
221            if (conferenceable instanceof Connection) {
222                Connection connection = (Connection) conferenceable;
223                connection.setConferenceables(backgroundConnections);
224            }
225        }
226
227        // Go through all the background connections and set the active connections as
228        // conferenceable.
229        for (Conferenceable conferenceable : backgroundConnections) {
230            if (conferenceable instanceof Connection) {
231                Connection connection = (Connection) conferenceable;
232                connection.setConferenceables(activeConnections);
233            }
234
235        }
236
237        // Set the conference as conferenceable with all the connections
238        for (ImsConference conference : mImsConferences) {
239            // If this conference is not being hosted on the current device, we cannot conference it
240            // with any other connections.
241            if (!conference.isConferenceHost()) {
242                if (Log.VERBOSE) {
243                    Log.v(this, "skipping conference (not hosted on this device): %s",
244                            conference);
245                }
246                continue;
247            }
248
249            List<Connection> nonConferencedConnections =
250                new ArrayList<>(mTelephonyConnections.size());
251            for (TelephonyConnection c : mTelephonyConnections) {
252                if (c.getConference() == null && c.isConferenceSupported()) {
253                    nonConferencedConnections.add(c);
254                }
255            }
256            if (Log.VERBOSE) {
257                Log.v(this, "conference conferenceable: %s", nonConferencedConnections);
258            }
259            conference.setConferenceableConnections(nonConferencedConnections);
260        }
261    }
262
263    /**
264     * Determines if a connection is a member of a conference hosted on another device.
265     *
266     * @param connection The connection.
267     * @return {@code true} if the connection is a member of a conference hosted on another device.
268     */
269    private boolean isMemberOfPeerConference(Connection connection) {
270        if (!(connection instanceof TelephonyConnection)) {
271            return false;
272        }
273        TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
274        com.android.internal.telephony.Connection originalConnection =
275                telephonyConnection.getOriginalConnection();
276
277        return originalConnection != null && originalConnection.isMultiparty() &&
278                originalConnection.isMemberOfPeerConference();
279    }
280
281    /**
282     * Starts a new ImsConference for a connection which just entered a multiparty state.
283     */
284    private void recalculateConference() {
285        Log.v(this, "recalculateConference");
286
287        Iterator<TelephonyConnection> it = mTelephonyConnections.iterator();
288        while (it.hasNext()) {
289            TelephonyConnection connection = it.next();
290
291            if (connection.isImsConnection() && connection.getOriginalConnection() != null &&
292                    connection.getOriginalConnection().isMultiparty()) {
293
294                startConference(connection);
295                it.remove();
296            }
297        }
298    }
299
300    /**
301     * Starts a new {@link ImsConference} for the given IMS connection.
302     * <p>
303     * Creates a new IMS Conference to manage the conference represented by the connection.
304     * Internally the ImsConference wraps the radio connection with a new TelephonyConnection
305     * which is NOT reported to the connection service and Telecom.
306     * <p>
307     * Once the new IMS Conference has been created, the connection passed in is held and removed
308     * from the connection service (removing it from Telecom).  The connection is put into a held
309     * state to ensure that telecom removes the connection without putting it into a disconnected
310     * state first.
311     *
312     * @param connection The connection to the Ims server.
313     */
314    private void startConference(TelephonyConnection connection) {
315        if (Log.VERBOSE) {
316            Log.v(this, "Start new ImsConference - connection: %s", connection);
317        }
318
319        // Make a clone of the connection which will become the Ims conference host connection.
320        // This is necessary since the Connection Service does not support removing a connection
321        // from Telecom.  Instead we create a new instance and remove the old one from telecom.
322        TelephonyConnection conferenceHostConnection = connection.cloneConnection();
323
324        PhoneAccountHandle phoneAccountHandle = null;
325
326        // Attempt to determine the phone account associated with the conference host connection.
327        if (connection.getPhone() != null &&
328                connection.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
329            Phone imsPhone = connection.getPhone();
330            // The phone account handle for an ImsPhone is based on the default phone (ie the
331            // base GSM or CDMA phone, not on the ImsPhone itself).
332            phoneAccountHandle =
333                    PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
334        }
335
336        ImsConference conference = new ImsConference(mConnectionService, conferenceHostConnection,
337                phoneAccountHandle);
338        conference.setState(conferenceHostConnection.getState());
339        conference.addListener(mConferenceListener);
340        conference.updateConferenceParticipantsAfterCreation();
341        mConnectionService.addConference(conference);
342        conferenceHostConnection.setTelecomCallId(conference.getTelecomCallId());
343
344        // Cleanup TelephonyConnection which backed the original connection and remove from telecom.
345        // Use the "Other" disconnect cause to ensure the call is logged to the call log but the
346        // disconnect tone is not played.
347        connection.removeConnectionListener(mConnectionListener);
348        connection.clearOriginalConnection();
349        connection.setDisconnected(new DisconnectCause(DisconnectCause.OTHER));
350        connection.destroy();
351        mImsConferences.add(conference);
352    }
353}
354