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 java.util.ArrayList;
20import java.util.Collections;
21import java.util.HashSet;
22import java.util.List;
23import java.util.Set;
24
25import android.telecom.Conference;
26import android.telecom.Connection;
27import android.telecom.DisconnectCause;
28
29import com.android.internal.telephony.Call;
30
31/**
32 * Maintains a list of all the known GSM connections and implements GSM-specific conference
33 * call functionality.
34 */
35final class GsmConferenceController {
36    private static final int GSM_CONFERENCE_MAX_SIZE = 5;
37
38    private final Connection.Listener mConnectionListener = new Connection.Listener() {
39        @Override
40        public void onStateChanged(Connection c, int state) {
41            recalculate();
42        }
43
44        /** ${inheritDoc} */
45        @Override
46        public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
47            recalculate();
48        }
49
50        @Override
51        public void onDestroyed(Connection connection) {
52            remove((GsmConnection) connection);
53        }
54    };
55
56    /** The known GSM connections. */
57    private final List<GsmConnection> mGsmConnections = new ArrayList<>();
58
59    private final TelephonyConnectionService mConnectionService;
60
61    public GsmConferenceController(TelephonyConnectionService connectionService) {
62        mConnectionService = connectionService;
63    }
64
65    /** The GSM conference connection object. */
66    private Conference mGsmConference;
67
68    void add(GsmConnection connection) {
69        mGsmConnections.add(connection);
70        connection.addConnectionListener(mConnectionListener);
71        recalculate();
72    }
73
74    void remove(GsmConnection connection) {
75        connection.removeConnectionListener(mConnectionListener);
76        mGsmConnections.remove(connection);
77        recalculate();
78    }
79
80    private void recalculate() {
81        recalculateConferenceable();
82        recalculateConference();
83    }
84
85    private boolean isFullConference(Conference conference) {
86        return conference.getConnections().size() >= GSM_CONFERENCE_MAX_SIZE;
87    }
88
89    private boolean participatesInFullConference(Connection connection) {
90        return connection.getConference() != null &&
91                isFullConference(connection.getConference());
92    }
93
94    /**
95     * Calculates the conference-capable state of all GSM connections in this connection service.
96     */
97    private void recalculateConferenceable() {
98        Log.v(this, "recalculateConferenceable : %d", mGsmConnections.size());
99
100        List<Connection> activeConnections = new ArrayList<>(mGsmConnections.size());
101        List<Connection> backgroundConnections = new ArrayList<>(mGsmConnections.size());
102
103        // Loop through and collect all calls which are active or holding
104        for (GsmConnection connection : mGsmConnections) {
105            Log.d(this, "recalc - %s %s", connection.getState(), connection);
106
107            if (!participatesInFullConference(connection)) {
108                switch (connection.getState()) {
109                    case Connection.STATE_ACTIVE:
110                        activeConnections.add(connection);
111                        continue;
112                    case Connection.STATE_HOLDING:
113                        backgroundConnections.add(connection);
114                        continue;
115                    default:
116                        break;
117                }
118            }
119
120            connection.setConferenceableConnections(Collections.<Connection>emptyList());
121        }
122
123        Log.v(this, "active: %d, holding: %d",
124                activeConnections.size(), backgroundConnections.size());
125
126        // Go through all the active connections and set the background connections as
127        // conferenceable.
128        for (Connection connection : activeConnections) {
129            connection.setConferenceableConnections(backgroundConnections);
130        }
131
132        // Go through all the background connections and set the active connections as
133        // conferenceable.
134        for (Connection connection : backgroundConnections) {
135            connection.setConferenceableConnections(activeConnections);
136        }
137
138        // Set the conference as conferenceable with all the connections
139        if (mGsmConference != null && !isFullConference(mGsmConference)) {
140            List<Connection> nonConferencedConnections = new ArrayList<>(mGsmConnections.size());
141            for (GsmConnection c : mGsmConnections) {
142                if (c.getConference() == null) {
143                    nonConferencedConnections.add(c);
144                }
145            }
146            mGsmConference.setConferenceableConnections(nonConferencedConnections);
147        }
148
149        // TODO: Do not allow conferencing of already conferenced connections.
150    }
151
152    private void recalculateConference() {
153        Set<GsmConnection> conferencedConnections = new HashSet<>();
154
155        for (GsmConnection connection : mGsmConnections) {
156            com.android.internal.telephony.Connection radioConnection =
157                connection.getOriginalConnection();
158
159            if (radioConnection != null) {
160                Call.State state = radioConnection.getState();
161                Call call = radioConnection.getCall();
162                if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
163                        (call != null && call.isMultiparty())) {
164                    conferencedConnections.add(connection);
165                }
166            }
167        }
168
169        Log.d(this, "Recalculate conference calls %s %s.",
170                mGsmConference, conferencedConnections);
171
172        if (conferencedConnections.size() < 2) {
173            Log.d(this, "less than two conference calls!");
174            // No more connections are conferenced, destroy any existing conference.
175            if (mGsmConference != null) {
176                Log.d(this, "with a conference to destroy!");
177                mGsmConference.destroy();
178                mGsmConference = null;
179            }
180        } else {
181            if (mGsmConference != null) {
182                List<Connection> existingConnections = mGsmConference.getConnections();
183                // Remove any that no longer exist
184                for (Connection connection : existingConnections) {
185                    if (!conferencedConnections.contains(connection)) {
186                        mGsmConference.removeConnection(connection);
187                    }
188                }
189
190                // Add any new ones
191                for (Connection connection : conferencedConnections) {
192                    if (!existingConnections.contains(connection)) {
193                        mGsmConference.addConnection(connection);
194                    }
195                }
196            } else {
197                mGsmConference = new GsmConference(null);
198                for (Connection connection : conferencedConnections) {
199                    Log.d(this, "Adding a connection to a conference call: %s %s",
200                            mGsmConference, connection);
201                    mGsmConference.addConnection(connection);
202                }
203                mConnectionService.addConference(mGsmConference);
204            }
205
206            // Set the conference state to the same state as its child connections.
207            Connection conferencedConnection = mGsmConference.getConnections().get(0);
208            switch (conferencedConnection.getState()) {
209                case Connection.STATE_ACTIVE:
210                    mGsmConference.setActive();
211                    break;
212                case Connection.STATE_HOLDING:
213                    mGsmConference.setOnHold();
214                    break;
215            }
216        }
217    }
218}
219