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 com.android.services.telephony;
18 
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Set;
26 
27 import android.net.Uri;
28 import android.telecom.Conference;
29 import android.telecom.ConferenceParticipant;
30 import android.telecom.Conferenceable;
31 import android.telecom.Connection;
32 import android.telecom.DisconnectCause;
33 import android.telecom.PhoneAccountHandle;
34 import com.android.phone.PhoneUtils;
35 
36 import com.android.internal.telephony.Call;
37 
38 /**
39  * Maintains a list of all the known TelephonyConnections connections and controls GSM and
40  * default IMS conference call behavior. This functionality is characterized by the support of
41  * two top-level calls, in contrast to a CDMA conference call which automatically starts a
42  * conference when there are two calls.
43  */
44 final class TelephonyConferenceController {
45     private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5;
46 
47     private final Connection.Listener mConnectionListener = new Connection.Listener() {
48         @Override
49         public void onStateChanged(Connection c, int state) {
50             Log.v(this, "onStateChange triggered in Conf Controller : connection = "+ c
51                  + " state = " + state);
52             recalculate();
53         }
54 
55         /** ${inheritDoc} */
56         @Override
57         public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
58             recalculate();
59         }
60 
61         @Override
62         public void onDestroyed(Connection connection) {
63             remove(connection);
64         }
65     };
66 
67     /** The known connections. */
68     private final List<TelephonyConnection> mTelephonyConnections = new ArrayList<>();
69 
70     private final TelephonyConnectionService mConnectionService;
71     private boolean mTriggerRecalculate = false;
72 
TelephonyConferenceController(TelephonyConnectionService connectionService)73     public TelephonyConferenceController(TelephonyConnectionService connectionService) {
74         mConnectionService = connectionService;
75     }
76 
77     /** The TelephonyConference connection object. */
78     private TelephonyConference mTelephonyConference;
79 
shouldRecalculate()80     boolean shouldRecalculate() {
81         Log.d(this, "shouldRecalculate is " + mTriggerRecalculate);
82         return mTriggerRecalculate;
83     }
84 
add(TelephonyConnection connection)85     void add(TelephonyConnection connection) {
86         mTelephonyConnections.add(connection);
87         connection.addConnectionListener(mConnectionListener);
88         recalculate();
89     }
90 
remove(Connection connection)91     void remove(Connection connection) {
92         connection.removeConnectionListener(mConnectionListener);
93         mTelephonyConnections.remove(connection);
94         recalculate();
95     }
96 
recalculate()97     void recalculate() {
98         recalculateConference();
99         recalculateConferenceable();
100     }
101 
isFullConference(Conference conference)102     private boolean isFullConference(Conference conference) {
103         return conference.getConnections().size() >= TELEPHONY_CONFERENCE_MAX_SIZE;
104     }
105 
participatesInFullConference(Connection connection)106     private boolean participatesInFullConference(Connection connection) {
107         return connection.getConference() != null &&
108                 isFullConference(connection.getConference());
109     }
110 
111     /**
112      * Calculates the conference-capable state of all GSM connections in this connection service.
113      */
recalculateConferenceable()114     private void recalculateConferenceable() {
115         Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
116 
117         List<Connection> activeConnections = new ArrayList<>(mTelephonyConnections.size());
118         List<Connection> backgroundConnections = new ArrayList<>(
119                 mTelephonyConnections.size());
120 
121         // Loop through and collect all calls which are active or holding
122         for (TelephonyConnection connection : mTelephonyConnections) {
123             Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection,
124                     connection.isConferenceSupported());
125 
126             if (connection.isConferenceSupported() && !participatesInFullConference(connection)) {
127                 switch (connection.getState()) {
128                     case Connection.STATE_ACTIVE:
129                         activeConnections.add(connection);
130                         continue;
131                     case Connection.STATE_HOLDING:
132                         backgroundConnections.add(connection);
133                         continue;
134                     default:
135                         break;
136                 }
137             }
138 
139             connection.setConferenceableConnections(Collections.<Connection>emptyList());
140         }
141 
142         Log.v(this, "active: %d, holding: %d",
143                 activeConnections.size(), backgroundConnections.size());
144 
145         // Go through all the active connections and set the background connections as
146         // conferenceable.
147         for (Connection connection : activeConnections) {
148             connection.setConferenceableConnections(backgroundConnections);
149         }
150 
151         // Go through all the background connections and set the active connections as
152         // conferenceable.
153         for (Connection connection : backgroundConnections) {
154             connection.setConferenceableConnections(activeConnections);
155         }
156 
157         // Set the conference as conferenceable with all the connections
158         if (mTelephonyConference != null && !isFullConference(mTelephonyConference)) {
159             List<Connection> nonConferencedConnections =
160                     new ArrayList<>(mTelephonyConnections.size());
161             for (TelephonyConnection c : mTelephonyConnections) {
162                 if (c.isConferenceSupported() && c.getConference() == null) {
163                     nonConferencedConnections.add(c);
164                 }
165             }
166             Log.v(this, "conference conferenceable: %s", nonConferencedConnections);
167             mTelephonyConference.setConferenceableConnections(nonConferencedConnections);
168         }
169 
170         // TODO: Do not allow conferencing of already conferenced connections.
171     }
172 
recalculateConference()173     private void recalculateConference() {
174         Set<Connection> conferencedConnections = new HashSet<>();
175         int numGsmConnections = 0;
176 
177         for (TelephonyConnection connection : mTelephonyConnections) {
178             com.android.internal.telephony.Connection radioConnection =
179                 connection.getOriginalConnection();
180 
181             if (radioConnection != null) {
182                 Call.State state = radioConnection.getState();
183                 Call call = radioConnection.getCall();
184                 if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
185                         (call != null && call.isMultiparty())) {
186 
187                     numGsmConnections++;
188                     conferencedConnections.add(connection);
189                 }
190             }
191         }
192 
193         Log.d(this, "Recalculate conference calls %s %s.",
194                 mTelephonyConference, conferencedConnections);
195 
196         // Check if all conferenced connections are in Connection Service
197         boolean allConnInService = true;
198         Collection<Connection> allConnections = mConnectionService.getAllConnections();
199         for (Connection connection : conferencedConnections) {
200             Log.v (this, "Finding connection in Connection Service for " + connection);
201             if (!allConnections.contains(connection)) {
202                 allConnInService = false;
203                 Log.v(this, "Finding connection in Connection Service Failed");
204                 break;
205             }
206         }
207 
208         Log.d(this, "Is there a match for all connections in connection service " +
209             allConnInService);
210 
211         // If this is a GSM conference and the number of connections drops below 2, we will
212         // terminate the conference.
213         if (numGsmConnections < 2) {
214             Log.d(this, "not enough connections to be a conference!");
215 
216             // No more connections are conferenced, destroy any existing conference.
217             if (mTelephonyConference != null) {
218                 Log.d(this, "with a conference to destroy!");
219                 mTelephonyConference.destroy();
220                 mTelephonyConference = null;
221             }
222         } else {
223             if (mTelephonyConference != null) {
224                 List<Connection> existingConnections = mTelephonyConference.getConnections();
225                 // Remove any that no longer exist
226                 for (Connection connection : existingConnections) {
227                     if (connection instanceof TelephonyConnection &&
228                             !conferencedConnections.contains(connection)) {
229                         mTelephonyConference.removeConnection(connection);
230                     }
231                 }
232                 if (allConnInService) {
233                     mTriggerRecalculate = false;
234                     // Add any new ones
235                     for (Connection connection : conferencedConnections) {
236                         if (!existingConnections.contains(connection)) {
237                             mTelephonyConference.addConnection(connection);
238                         }
239                     }
240                 } else {
241                     Log.d(this, "Trigger recalculate later");
242                     mTriggerRecalculate = true;
243                 }
244             } else {
245                 if (allConnInService) {
246                     mTriggerRecalculate = false;
247 
248                     // Get PhoneAccount from one of the conferenced connections and use it to set
249                     // the phone account on the conference.
250                     PhoneAccountHandle phoneAccountHandle = null;
251                     if (!conferencedConnections.isEmpty()) {
252                         TelephonyConnection telephonyConnection =
253                                 (TelephonyConnection) conferencedConnections.iterator().next();
254                         phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(
255                                 telephonyConnection.getPhone());
256                     }
257 
258                     mTelephonyConference = new TelephonyConference(phoneAccountHandle);
259                     for (Connection connection : conferencedConnections) {
260                         Log.d(this, "Adding a connection to a conference call: %s %s",
261                                 mTelephonyConference, connection);
262                         mTelephonyConference.addConnection(connection);
263                     }
264                     mConnectionService.addConference(mTelephonyConference);
265                 } else {
266                     Log.d(this, "Trigger recalculate later");
267                     mTriggerRecalculate = true;
268                 }
269             }
270             if (mTelephonyConference != null) {
271                 Connection conferencedConnection = mTelephonyConference.getPrimaryConnection();
272                 Log.v(this, "Primary Conferenced connection is " + conferencedConnection);
273                 if (conferencedConnection != null) {
274                     switch (conferencedConnection.getState()) {
275                         case Connection.STATE_ACTIVE:
276                             Log.v(this, "Setting conference to active");
277                             mTelephonyConference.setActive();
278                             break;
279                         case Connection.STATE_HOLDING:
280                             Log.v(this, "Setting conference to hold");
281                             mTelephonyConference.setOnHold();
282                             break;
283                     }
284                 }
285             }
286         }
287     }
288 }
289