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