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