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 17package com.android.services.telephony; 18 19import com.android.internal.telephony.Phone; 20import com.android.internal.telephony.PhoneConstants; 21import com.android.phone.PhoneUtils; 22 23import android.telecom.Conference; 24import android.telecom.Connection; 25import android.telecom.ConnectionService; 26import android.telecom.DisconnectCause; 27import android.telecom.Conferenceable; 28import android.telecom.PhoneAccountHandle; 29 30import java.util.ArrayList; 31import java.util.Collections; 32import java.util.Iterator; 33import java.util.List; 34 35/** 36 * Manages conferences for IMS connections. 37 */ 38public class ImsConferenceController { 39 40 /** 41 * Conference listener; used to receive notification when a conference has been disconnected. 42 */ 43 private final Conference.Listener mConferenceListener = new Conference.Listener() { 44 @Override 45 public void onDestroyed(Conference conference) { 46 if (Log.VERBOSE) { 47 Log.v(ImsConferenceController.class, "onDestroyed: %s", conference); 48 } 49 50 mImsConferences.remove(conference); 51 } 52 }; 53 54 /** 55 * Ims conference controller connection listener. Used to respond to changes in state of the 56 * Telephony connections the controller is aware of. 57 */ 58 private final Connection.Listener mConnectionListener = new Connection.Listener() { 59 @Override 60 public void onStateChanged(Connection c, int state) { 61 Log.v(this, "onStateChanged: %s", Log.pii(c.getAddress())); 62 recalculate(); 63 } 64 65 @Override 66 public void onDisconnected(Connection c, DisconnectCause disconnectCause) { 67 Log.v(this, "onDisconnected: %s", Log.pii(c.getAddress())); 68 recalculate(); 69 } 70 71 @Override 72 public void onDestroyed(Connection connection) { 73 remove(connection); 74 } 75 76 @Override 77 public void onConferenceStarted() { 78 Log.v(this, "onConferenceStarted"); 79 recalculate(); 80 } 81 }; 82 83 /** 84 * The current {@link ConnectionService}. 85 */ 86 private final TelephonyConnectionService mConnectionService; 87 88 /** 89 * List of known {@link TelephonyConnection}s. 90 */ 91 private final ArrayList<TelephonyConnection> mTelephonyConnections = new ArrayList<>(); 92 93 /** 94 * List of known {@link ImsConference}s. Realistically there will only ever be a single 95 * concurrent IMS conference. 96 */ 97 private final ArrayList<ImsConference> mImsConferences = new ArrayList<>(1); 98 99 /** 100 * Creates a new instance of the Ims conference controller. 101 * 102 * @param connectionService The current connection service. 103 */ 104 public ImsConferenceController(TelephonyConnectionService connectionService) { 105 mConnectionService = connectionService; 106 } 107 108 /** 109 * Adds a new connection to the IMS conference controller. 110 * 111 * @param connection 112 */ 113 void add(TelephonyConnection connection) { 114 // Note: Wrap in Log.VERBOSE to avoid calling connection.toString if we are not going to be 115 // outputting the value. 116 if (Log.VERBOSE) { 117 Log.v(this, "add connection %s", connection); 118 } 119 120 mTelephonyConnections.add(connection); 121 connection.addConnectionListener(mConnectionListener); 122 recalculateConference(); 123 } 124 125 /** 126 * Removes a connection from the IMS conference controller. 127 * 128 * @param connection 129 */ 130 void remove(Connection connection) { 131 if (Log.VERBOSE) { 132 Log.v(this, "remove connection: %s", connection); 133 } 134 135 mTelephonyConnections.remove(connection); 136 recalculateConferenceable(); 137 } 138 139 /** 140 * Triggers both a re-check of conferenceable connections, as well as checking for new 141 * conferences. 142 */ 143 private void recalculate() { 144 recalculateConferenceable(); 145 recalculateConference(); 146 } 147 148 /** 149 * Calculates the conference-capable state of all GSM connections in this connection service. 150 */ 151 private void recalculateConferenceable() { 152 Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size()); 153 List<Conferenceable> activeConnections = new ArrayList<>(mTelephonyConnections.size()); 154 List<Conferenceable> backgroundConnections = new ArrayList<>(mTelephonyConnections.size()); 155 156 // Loop through and collect all calls which are active or holding 157 for (TelephonyConnection connection : mTelephonyConnections) { 158 if (Log.DEBUG) { 159 Log.d(this, "recalc - %s %s supportsConf? %s", connection.getState(), connection, 160 connection.isConferenceSupported()); 161 } 162 163 // If this connection is a member of a conference hosted on another device, it is not 164 // conferenceable with any other connections. 165 if (isMemberOfPeerConference(connection)) { 166 if (Log.VERBOSE) { 167 Log.v(this, "Skipping connection in peer conference: %s", connection); 168 } 169 continue; 170 } 171 172 // If this connection does not support being in a conference call, then it is not 173 // conferenceable with any other connection. 174 if (!connection.isConferenceSupported()) { 175 continue; 176 } 177 178 switch (connection.getState()) { 179 case Connection.STATE_ACTIVE: 180 activeConnections.add(connection); 181 continue; 182 case Connection.STATE_HOLDING: 183 backgroundConnections.add(connection); 184 continue; 185 default: 186 break; 187 } 188 connection.setConferenceableConnections(Collections.<Connection>emptyList()); 189 } 190 191 for (ImsConference conference : mImsConferences) { 192 if (Log.DEBUG) { 193 Log.d(this, "recalc - %s %s", conference.getState(), conference); 194 } 195 196 if (!conference.isConferenceHost()) { 197 if (Log.VERBOSE) { 198 Log.v(this, "skipping conference (not hosted on this device): %s", conference); 199 } 200 continue; 201 } 202 203 switch (conference.getState()) { 204 case Connection.STATE_ACTIVE: 205 activeConnections.add(conference); 206 continue; 207 case Connection.STATE_HOLDING: 208 backgroundConnections.add(conference); 209 continue; 210 default: 211 break; 212 } 213 } 214 215 Log.v(this, "active: %d, holding: %d", activeConnections.size(), 216 backgroundConnections.size()); 217 218 // Go through all the active connections and set the background connections as 219 // conferenceable. 220 for (Conferenceable conferenceable : activeConnections) { 221 if (conferenceable instanceof Connection) { 222 Connection connection = (Connection) conferenceable; 223 connection.setConferenceables(backgroundConnections); 224 } 225 } 226 227 // Go through all the background connections and set the active connections as 228 // conferenceable. 229 for (Conferenceable conferenceable : backgroundConnections) { 230 if (conferenceable instanceof Connection) { 231 Connection connection = (Connection) conferenceable; 232 connection.setConferenceables(activeConnections); 233 } 234 235 } 236 237 // Set the conference as conferenceable with all the connections 238 for (ImsConference conference : mImsConferences) { 239 // If this conference is not being hosted on the current device, we cannot conference it 240 // with any other connections. 241 if (!conference.isConferenceHost()) { 242 if (Log.VERBOSE) { 243 Log.v(this, "skipping conference (not hosted on this device): %s", 244 conference); 245 } 246 continue; 247 } 248 249 List<Connection> nonConferencedConnections = 250 new ArrayList<>(mTelephonyConnections.size()); 251 for (TelephonyConnection c : mTelephonyConnections) { 252 if (c.getConference() == null && c.isConferenceSupported()) { 253 nonConferencedConnections.add(c); 254 } 255 } 256 if (Log.VERBOSE) { 257 Log.v(this, "conference conferenceable: %s", nonConferencedConnections); 258 } 259 conference.setConferenceableConnections(nonConferencedConnections); 260 } 261 } 262 263 /** 264 * Determines if a connection is a member of a conference hosted on another device. 265 * 266 * @param connection The connection. 267 * @return {@code true} if the connection is a member of a conference hosted on another device. 268 */ 269 private boolean isMemberOfPeerConference(Connection connection) { 270 if (!(connection instanceof TelephonyConnection)) { 271 return false; 272 } 273 TelephonyConnection telephonyConnection = (TelephonyConnection) connection; 274 com.android.internal.telephony.Connection originalConnection = 275 telephonyConnection.getOriginalConnection(); 276 277 return originalConnection != null && originalConnection.isMultiparty() && 278 originalConnection.isMemberOfPeerConference(); 279 } 280 281 /** 282 * Starts a new ImsConference for a connection which just entered a multiparty state. 283 */ 284 private void recalculateConference() { 285 Log.v(this, "recalculateConference"); 286 287 Iterator<TelephonyConnection> it = mTelephonyConnections.iterator(); 288 while (it.hasNext()) { 289 TelephonyConnection connection = it.next(); 290 291 if (connection.isImsConnection() && connection.getOriginalConnection() != null && 292 connection.getOriginalConnection().isMultiparty()) { 293 294 startConference(connection); 295 it.remove(); 296 } 297 } 298 } 299 300 /** 301 * Starts a new {@link ImsConference} for the given IMS connection. 302 * <p> 303 * Creates a new IMS Conference to manage the conference represented by the connection. 304 * Internally the ImsConference wraps the radio connection with a new TelephonyConnection 305 * which is NOT reported to the connection service and Telecom. 306 * <p> 307 * Once the new IMS Conference has been created, the connection passed in is held and removed 308 * from the connection service (removing it from Telecom). The connection is put into a held 309 * state to ensure that telecom removes the connection without putting it into a disconnected 310 * state first. 311 * 312 * @param connection The connection to the Ims server. 313 */ 314 private void startConference(TelephonyConnection connection) { 315 if (Log.VERBOSE) { 316 Log.v(this, "Start new ImsConference - connection: %s", connection); 317 } 318 319 // Make a clone of the connection which will become the Ims conference host connection. 320 // This is necessary since the Connection Service does not support removing a connection 321 // from Telecom. Instead we create a new instance and remove the old one from telecom. 322 TelephonyConnection conferenceHostConnection = connection.cloneConnection(); 323 324 PhoneAccountHandle phoneAccountHandle = null; 325 326 // Attempt to determine the phone account associated with the conference host connection. 327 if (connection.getPhone() != null && 328 connection.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 329 Phone imsPhone = connection.getPhone(); 330 // The phone account handle for an ImsPhone is based on the default phone (ie the 331 // base GSM or CDMA phone, not on the ImsPhone itself). 332 phoneAccountHandle = 333 PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); 334 } 335 336 ImsConference conference = new ImsConference(mConnectionService, conferenceHostConnection, 337 phoneAccountHandle); 338 conference.setState(conferenceHostConnection.getState()); 339 conference.addListener(mConferenceListener); 340 conference.updateConferenceParticipantsAfterCreation(); 341 mConnectionService.addConference(conference); 342 conferenceHostConnection.setTelecomCallId(conference.getTelecomCallId()); 343 344 // Cleanup TelephonyConnection which backed the original connection and remove from telecom. 345 // Use the "Other" disconnect cause to ensure the call is logged to the call log but the 346 // disconnect tone is not played. 347 connection.removeConnectionListener(mConnectionListener); 348 connection.clearOriginalConnection(); 349 connection.setDisconnected(new DisconnectCause(DisconnectCause.OTHER)); 350 connection.destroy(); 351 mImsConferences.add(conference); 352 } 353} 354