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 java.util.ArrayList; 20 import java.util.Collection; 21 import java.util.Collections; 22 import java.util.HashMap; 23 import java.util.HashSet; 24 import java.util.List; 25 import java.util.Set; 26 import java.util.stream.Collectors; 27 28 import android.net.Uri; 29 import android.telecom.Conference; 30 import android.telecom.ConferenceParticipant; 31 import android.telecom.Conferenceable; 32 import android.telecom.Connection; 33 import android.telecom.DisconnectCause; 34 import android.telecom.PhoneAccountHandle; 35 import com.android.phone.PhoneUtils; 36 37 import com.android.internal.telephony.Call; 38 39 /** 40 * Maintains a list of all the known TelephonyConnections connections and controls GSM and 41 * default IMS conference call behavior. This functionality is characterized by the support of 42 * two top-level calls, in contrast to a CDMA conference call which automatically starts a 43 * conference when there are two calls. 44 */ 45 final class TelephonyConferenceController { 46 private static final int TELEPHONY_CONFERENCE_MAX_SIZE = 5; 47 48 private final Connection.Listener mConnectionListener = new Connection.Listener() { 49 @Override 50 public void onStateChanged(Connection c, int state) { 51 Log.v(this, "onStateChange triggered in Conf Controller : connection = "+ c 52 + " state = " + state); 53 recalculate(); 54 } 55 56 /** ${inheritDoc} */ 57 @Override 58 public void onDisconnected(Connection c, DisconnectCause disconnectCause) { 59 recalculate(); 60 } 61 62 @Override 63 public void onDestroyed(Connection connection) { 64 remove(connection); 65 } 66 }; 67 68 /** The known connections. */ 69 private final List<TelephonyConnection> mTelephonyConnections = new ArrayList<>(); 70 71 private final TelephonyConnectionService mConnectionService; 72 private boolean mTriggerRecalculate = false; 73 TelephonyConferenceController(TelephonyConnectionService connectionService)74 public TelephonyConferenceController(TelephonyConnectionService connectionService) { 75 mConnectionService = connectionService; 76 } 77 78 /** The TelephonyConference connection object. */ 79 private TelephonyConference mTelephonyConference; 80 shouldRecalculate()81 boolean shouldRecalculate() { 82 Log.d(this, "shouldRecalculate is " + mTriggerRecalculate); 83 return mTriggerRecalculate; 84 } 85 add(TelephonyConnection connection)86 void add(TelephonyConnection connection) { 87 if (mTelephonyConnections.contains(connection)) { 88 // Adding a duplicate realistically shouldn't happen. 89 Log.w(this, "add - connection already tracked; connection=%s", connection); 90 return; 91 } 92 93 mTelephonyConnections.add(connection); 94 connection.addConnectionListener(mConnectionListener); 95 recalculate(); 96 } 97 remove(Connection connection)98 void remove(Connection connection) { 99 if (!mTelephonyConnections.contains(connection)) { 100 // Debug only since TelephonyConnectionService tries to clean up the connections tracked 101 // when the original connection changes. It does this proactively. 102 Log.d(this, "remove - connection not tracked; connection=%s", connection); 103 return; 104 } 105 connection.removeConnectionListener(mConnectionListener); 106 mTelephonyConnections.remove(connection); 107 recalculate(); 108 } 109 recalculate()110 void recalculate() { 111 recalculateConference(); 112 recalculateConferenceable(); 113 } 114 isFullConference(Conference conference)115 private boolean isFullConference(Conference conference) { 116 return conference.getConnections().size() >= TELEPHONY_CONFERENCE_MAX_SIZE; 117 } 118 participatesInFullConference(Connection connection)119 private boolean participatesInFullConference(Connection connection) { 120 return connection.getConference() != null && 121 isFullConference(connection.getConference()); 122 } 123 124 /** 125 * Calculates the conference-capable state of all GSM connections in this connection service. 126 */ recalculateConferenceable()127 private void recalculateConferenceable() { 128 Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size()); 129 HashSet<Connection> conferenceableConnections = new HashSet<>(mTelephonyConnections.size()); 130 131 // Loop through and collect all calls which are active or holding 132 for (TelephonyConnection connection : mTelephonyConnections) { 133 Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection, 134 connection.isConferenceSupported()); 135 136 if (connection.isConferenceSupported() && !participatesInFullConference(connection)) { 137 switch (connection.getState()) { 138 case Connection.STATE_ACTIVE: 139 //fall through 140 case Connection.STATE_HOLDING: 141 conferenceableConnections.add(connection); 142 continue; 143 default: 144 break; 145 } 146 } 147 148 connection.setConferenceableConnections(Collections.<Connection>emptyList()); 149 } 150 151 Log.v(this, "conferenceable: " + conferenceableConnections.size()); 152 153 // Go through all the conferenceable connections and add all other conferenceable 154 // connections that is not the connection itself 155 for (Connection c : conferenceableConnections) { 156 List<Connection> connections = conferenceableConnections 157 .stream() 158 // Filter out this connection from the list of connections 159 .filter(connection -> c != connection) 160 .collect(Collectors.toList()); 161 c.setConferenceableConnections(connections); 162 } 163 164 // Set the conference as conferenceable with all of the connections that are not in the 165 // conference. 166 if (mTelephonyConference != null && !isFullConference(mTelephonyConference)) { 167 List<Connection> nonConferencedConnections = mTelephonyConnections 168 .stream() 169 // Only retrieve Connections that are not in a conference (but support 170 // conferences). 171 .filter(c -> c.isConferenceSupported() && c.getConference() == null) 172 .collect(Collectors.toList()); 173 mTelephonyConference.setConferenceableConnections(nonConferencedConnections); 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 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 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.destroy(); 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.removeConnection(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.addConnection(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 (TelephonyConnection) conferencedConnections.iterator().next(); 259 phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle( 260 telephonyConnection.getPhone()); 261 } 262 263 mTelephonyConference = new TelephonyConference(phoneAccountHandle); 264 for (Connection connection : conferencedConnections) { 265 Log.d(this, "Adding a connection to a conference call: %s %s", 266 mTelephonyConference, connection); 267 mTelephonyConference.addConnection(connection); 268 } 269 mConnectionService.addConference(mTelephonyConference); 270 } else { 271 Log.d(this, "Trigger recalculate later"); 272 mTriggerRecalculate = true; 273 } 274 } 275 if (mTelephonyConference != null) { 276 Connection conferencedConnection = mTelephonyConference.getPrimaryConnection(); 277 Log.v(this, "Primary Conferenced connection is " + conferencedConnection); 278 if (conferencedConnection != null) { 279 switch (conferencedConnection.getState()) { 280 case Connection.STATE_ACTIVE: 281 Log.v(this, "Setting conference to active"); 282 mTelephonyConference.setActive(); 283 break; 284 case Connection.STATE_HOLDING: 285 Log.v(this, "Setting conference to hold"); 286 mTelephonyConference.setOnHold(); 287 break; 288 } 289 } 290 } 291 } 292 } 293 } 294