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 import android.text.TextUtils;
24 import android.util.Patterns;
25 
26 import com.android.internal.telephony.Call;
27 import com.android.phone.PhoneUtils;
28 
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 
37 /**
38  * Maintains a list of all the known TelephonyConnections connections and controls GSM and
39  * default IMS conference call behavior. This functionality is characterized by the support of
40  * two top-level calls, in contrast to a CDMA conference call which automatically starts a
41  * conference when there are two calls.
42  */
43 final class TelephonyConferenceController {
44     private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5;
45 
46     private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
47             new TelephonyConnection.TelephonyConnectionListener() {
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                 @Override
56                 public void onDisconnected(Connection c, DisconnectCause disconnectCause) {
57                     recalculate();
58                 }
59 
60                 @Override
61                 public void onDestroyed(Connection connection) {
62                     // Only TelephonyConnections are added.
63                     remove((TelephonyConnection) connection);
64                 }
65             };
66 
67     /** The known connections. */
68     private final List<TelephonyConnection> mTelephonyConnections = new ArrayList<>();
69 
70     private final TelephonyConnectionServiceProxy mConnectionService;
71     private boolean mTriggerRecalculate = false;
72 
TelephonyConferenceController(TelephonyConnectionServiceProxy connectionService)73     public TelephonyConferenceController(TelephonyConnectionServiceProxy connectionService) {
74         mConnectionService = connectionService;
75     }
76     /** The TelephonyConference connection object. */
77     private TelephonyConference mTelephonyConference;
78 
shouldRecalculate()79     boolean shouldRecalculate() {
80         Log.d(this, "shouldRecalculate is " + mTriggerRecalculate);
81         return mTriggerRecalculate;
82     }
83 
add(TelephonyConnection connection)84     void add(TelephonyConnection connection) {
85         if (mTelephonyConnections.contains(connection)) {
86             // Adding a duplicate realistically shouldn't happen.
87             Log.w(this, "add - connection already tracked; connection=%s", connection);
88             return;
89         }
90         mTelephonyConnections.add(connection);
91         connection.addTelephonyConnectionListener(mTelephonyConnectionListener);
92         recalculate();
93     }
94 
remove(TelephonyConnection connection)95     void remove(TelephonyConnection connection) {
96         if (!mTelephonyConnections.contains(connection)) {
97             // Debug only since TelephonyConnectionService tries to clean up the connections tracked
98             // when the original connection changes.  It does this proactively.
99             Log.d(this, "remove - connection not tracked; connection=%s", connection);
100             return;
101         }
102         connection.removeTelephonyConnectionListener(mTelephonyConnectionListener);
103         mTelephonyConnections.remove(connection);
104         recalculate();
105     }
106 
recalculate()107     void recalculate() {
108         recalculateConference();
109         recalculateConferenceable();
110     }
111 
isFullConference(Conference conference)112     private boolean isFullConference(Conference conference) {
113         return conference.getConnections().size() >= TELEPHONY_CONFERENCE_MAX_SIZE;
114     }
115 
participatesInFullConference(Connection connection)116     private boolean participatesInFullConference(Connection connection) {
117         return connection.getConference() != null &&
118                 isFullConference(connection.getConference());
119     }
120 
121     /**
122      * Calculates the conference-capable state of all GSM connections in this connection service.
123      */
recalculateConferenceable()124     private void recalculateConferenceable() {
125         Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
126         HashSet<Connection> conferenceableConnections = new HashSet<>(mTelephonyConnections.size());
127 
128         // Loop through and collect all calls which are active or holding
129         for (TelephonyConnection connection : mTelephonyConnections) {
130             Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection,
131                     connection.isConferenceSupported());
132 
133             if (connection.isConferenceSupported() && !participatesInFullConference(connection)) {
134                 switch (connection.getState()) {
135                     case Connection.STATE_ACTIVE:
136                         //fall through
137                     case Connection.STATE_HOLDING:
138                         conferenceableConnections.add(connection);
139                         continue;
140                     default:
141                         break;
142                 }
143             }
144 
145             connection.setConferenceableConnections(Collections.<Connection>emptyList());
146         }
147 
148         Log.v(this, "conferenceable: " + conferenceableConnections.size());
149 
150         // Go through all the conferenceable connections and add all other conferenceable
151         // connections that is not the connection itself
152         for (Connection c : conferenceableConnections) {
153             List<Connection> connections = conferenceableConnections
154                     .stream()
155                     // Filter out this connection from the list of connections
156                     .filter(connection -> c != connection)
157                     .collect(Collectors.toList());
158             c.setConferenceableConnections(connections);
159         }
160 
161         // Set the conference as conferenceable with all of the connections that are not in the
162         // conference.
163         if (mTelephonyConference != null) {
164             if (!isFullConference(mTelephonyConference)) {
165                 List<Connection> nonConferencedConnections = mTelephonyConnections
166                         .stream()
167                         // Only retrieve Connections that are not in a conference (but support
168                         // conferences).
169                         .filter(c -> c.isConferenceSupported() && c.getConference() == null)
170                         .collect(Collectors.toList());
171                 mTelephonyConference.setConferenceableConnections(nonConferencedConnections);
172             } else {
173                 Log.d(this, "cannot merge anymore due it is full");
174                 mTelephonyConference
175                         .setConferenceableConnections(Collections.<Connection>emptyList());
176             }
177         }
178         // TODO: Do not allow conferencing of already conferenced connections.
179     }
180 
recalculateConference()181     private void recalculateConference() {
182         Set<TelephonyConnection> conferencedConnections = new HashSet<>();
183         int numGsmConnections = 0;
184         for (TelephonyConnection connection : mTelephonyConnections) {
185             com.android.internal.telephony.Connection radioConnection =
186                 connection.getOriginalConnection();
187             if (radioConnection != null) {
188                 Call.State state = radioConnection.getState();
189                 Call call = radioConnection.getCall();
190                 if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
191                         (call != null && call.isMultiparty())) {
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.destroyTelephonyConference();
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.removeTelephonyConnection(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.addTelephonyConnection(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                                 conferencedConnections.iterator().next();
259                         phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(
260                                 telephonyConnection.getPhone());
261                     }
262 
263                     mTelephonyConference = new TelephonyConference(phoneAccountHandle);
264                     Log.i(this, "Creating new TelephonyConference to hold conferenced connections."
265                             + " conference=" + mTelephonyConference);
266                     boolean isDowngradedConference = false;
267                     for (TelephonyConnection connection : conferencedConnections) {
268                         Log.d(this, "Adding a connection to a conference call: %s %s",
269                                 mTelephonyConference, connection);
270                         if ((connection.getConnectionProperties()
271                                 & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0) {
272                             // Remove all instances of PROPERTY_IS_DOWNGRADED_CONFERENCE. This
273                             // property should only be set on the parent call (i.e. the newly
274                             // created TelephonyConference.
275                             // This doesn't apply to a connection whose address is not an
276                             // identifiable phone number, which may be updated by some modem
277                             // to create a connection to represent a merged conference connection
278                             // in SRVCC.
279                             if (connection.getAddress() == null
280                                     || isPhoneNumber(
281                                             connection.getAddress().getSchemeSpecificPart())) {
282                                 Log.d(this, "Removing PROPERTY_IS_DOWNGRADED_CONFERENCE"
283                                         + " from connection %s", connection);
284                                 int newProperties = connection.getConnectionProperties()
285                                         & ~Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE;
286                                 connection.setTelephonyConnectionProperties(newProperties);
287                             }
288                             isDowngradedConference = true;
289                         }
290                         mTelephonyConference.addTelephonyConnection(connection);
291                     }
292                     // Reapply the downgraded-conference flag to the parent conference if it was on
293                     // one of the children.
294                     if (isDowngradedConference) {
295                         mTelephonyConference.setConnectionProperties(
296                                 mTelephonyConference.getConnectionProperties()
297                                         | Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE);
298                     }
299                     mTelephonyConference.updateCallRadioTechAfterCreation();
300                     mConnectionService.addConference(mTelephonyConference);
301                 } else {
302                     Log.d(this, "Trigger recalculate later");
303                     mTriggerRecalculate = true;
304                 }
305             }
306             if (mTelephonyConference != null) {
307                 Connection conferencedConnection = mTelephonyConference.getPrimaryConnection();
308                 Log.v(this, "Primary Conferenced connection is " + conferencedConnection);
309                 if (conferencedConnection != null) {
310                     switch (conferencedConnection.getState()) {
311                         case Connection.STATE_ACTIVE:
312                             Log.v(this, "Setting conference to active");
313                             mTelephonyConference.setActive();
314                             break;
315                         case Connection.STATE_HOLDING:
316                             Log.v(this, "Setting conference to hold");
317                             mTelephonyConference.setOnHold();
318                             break;
319                     }
320                 }
321             }
322         }
323     }
324 
isPhoneNumber(String number)325     private boolean isPhoneNumber(String number) {
326         if (TextUtils.isEmpty(number)) {
327             return false;
328         }
329         return Patterns.PHONE.matcher(number).matches();
330     }
331 }
332