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