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