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