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 android.content.Context; 20import android.graphics.drawable.Icon; 21import android.net.Uri; 22import android.os.Bundle; 23import android.os.PersistableBundle; 24import android.telecom.Conference; 25import android.telecom.ConferenceParticipant; 26import android.telecom.Connection.VideoProvider; 27import android.telecom.Connection; 28import android.telecom.DisconnectCause; 29import android.telecom.Log; 30import android.telecom.PhoneAccountHandle; 31import android.telecom.StatusHints; 32import android.telecom.VideoProfile; 33import android.telephony.CarrierConfigManager; 34import android.telephony.PhoneNumberUtils; 35import android.util.Pair; 36 37import com.android.internal.telephony.Call; 38import com.android.internal.telephony.CallStateException; 39import com.android.internal.telephony.Phone; 40import com.android.internal.telephony.PhoneConstants; 41import com.android.phone.PhoneGlobals; 42import com.android.phone.PhoneUtils; 43import com.android.phone.R; 44 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.HashMap; 48import java.util.HashSet; 49import java.util.Iterator; 50import java.util.List; 51import java.util.Map; 52 53/** 54 * Represents an IMS conference call. 55 * <p> 56 * An IMS conference call consists of a conference host connection and potentially a list of 57 * conference participants. The conference host connection represents the radio connection to the 58 * IMS conference server. Since it is not a connection to any one individual, it is not represented 59 * in Telecom/InCall as a call. The conference participant information is received via the host 60 * connection via a conference event package. Conference participant connections do not represent 61 * actual radio connections to the participants; they act as a virtual representation of the 62 * participant, keyed by a unique endpoint {@link android.net.Uri}. 63 * <p> 64 * The {@link ImsConference} listens for conference event package data received via the host 65 * connection and is responsible for managing the conference participant connections which represent 66 * the participants. 67 */ 68public class ImsConference extends Conference { 69 70 /** 71 * Listener used to respond to changes to conference participants. At the conference level we 72 * are most concerned with handling destruction of a conference participant. 73 */ 74 private final Connection.Listener mParticipantListener = new Connection.Listener() { 75 /** 76 * Participant has been destroyed. Remove it from the conference. 77 * 78 * @param connection The participant which was destroyed. 79 */ 80 @Override 81 public void onDestroyed(Connection connection) { 82 ConferenceParticipantConnection participant = 83 (ConferenceParticipantConnection) connection; 84 removeConferenceParticipant(participant); 85 updateManageConference(); 86 } 87 88 }; 89 90 /** 91 * Listener used to respond to changes to the underlying radio connection for the conference 92 * host connection. Used to respond to SRVCC changes. 93 */ 94 private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener = 95 new TelephonyConnection.TelephonyConnectionListener() { 96 97 @Override 98 public void onOriginalConnectionConfigured(TelephonyConnection c) { 99 if (c == mConferenceHost) { 100 handleOriginalConnectionChange(); 101 } 102 } 103 }; 104 105 /** 106 * Listener used to respond to changes to the connection to the IMS conference server. 107 */ 108 private final android.telecom.Connection.Listener mConferenceHostListener = 109 new android.telecom.Connection.Listener() { 110 111 /** 112 * Updates the state of the conference based on the new state of the host. 113 * 114 * @param c The host connection. 115 * @param state The new state 116 */ 117 @Override 118 public void onStateChanged(android.telecom.Connection c, int state) { 119 setState(state); 120 } 121 122 /** 123 * Disconnects the conference when its host connection disconnects. 124 * 125 * @param c The host connection. 126 * @param disconnectCause The host connection disconnect cause. 127 */ 128 @Override 129 public void onDisconnected(android.telecom.Connection c, DisconnectCause disconnectCause) { 130 setDisconnected(disconnectCause); 131 } 132 133 /** 134 * Handles changes to conference participant data as reported by the conference host 135 * connection. 136 * 137 * @param c The connection. 138 * @param participants The participant information. 139 */ 140 @Override 141 public void onConferenceParticipantsChanged(android.telecom.Connection c, 142 List<ConferenceParticipant> participants) { 143 144 if (c == null || participants == null) { 145 return; 146 } 147 Log.v(this, "onConferenceParticipantsChanged: %d participants", participants.size()); 148 TelephonyConnection telephonyConnection = (TelephonyConnection) c; 149 handleConferenceParticipantsUpdate(telephonyConnection, participants); 150 } 151 152 @Override 153 public void onVideoStateChanged(android.telecom.Connection c, int videoState) { 154 Log.d(this, "onVideoStateChanged video state %d", videoState); 155 setVideoState(c, videoState); 156 } 157 158 @Override 159 public void onVideoProviderChanged(android.telecom.Connection c, 160 Connection.VideoProvider videoProvider) { 161 Log.d(this, "onVideoProviderChanged: Connection: %s, VideoProvider: %s", c, 162 videoProvider); 163 setVideoProvider(c, videoProvider); 164 } 165 166 @Override 167 public void onConnectionCapabilitiesChanged(Connection c, int connectionCapabilities) { 168 Log.d(this, "onConnectionCapabilitiesChanged: Connection: %s," + 169 " connectionCapabilities: %s", c, connectionCapabilities); 170 int capabilites = ImsConference.this.getConnectionCapabilities(); 171 boolean isVideoConferencingSupported = mConferenceHost == null ? false : 172 mConferenceHost.isCarrierVideoConferencingSupported(); 173 setConnectionCapabilities(applyHostCapabilities(capabilites, connectionCapabilities, 174 isVideoConferencingSupported)); 175 } 176 177 @Override 178 public void onConnectionPropertiesChanged(Connection c, int connectionProperties) { 179 Log.d(this, "onConnectionPropertiesChanged: Connection: %s," + 180 " connectionProperties: %s", c, connectionProperties); 181 int properties = ImsConference.this.getConnectionProperties(); 182 setConnectionProperties(applyHostProperties(properties, connectionProperties)); 183 } 184 185 @Override 186 public void onStatusHintsChanged(Connection c, StatusHints statusHints) { 187 Log.v(this, "onStatusHintsChanged"); 188 updateStatusHints(); 189 } 190 191 @Override 192 public void onExtrasChanged(Connection c, Bundle extras) { 193 Log.v(this, "onExtrasChanged: c=" + c + " Extras=" + extras); 194 putExtras(extras); 195 } 196 197 @Override 198 public void onExtrasRemoved(Connection c, List<String> keys) { 199 Log.v(this, "onExtrasRemoved: c=" + c + " key=" + keys); 200 removeExtras(keys); 201 } 202 }; 203 204 /** 205 * The telephony connection service; used to add new participant connections to Telecom. 206 */ 207 private TelephonyConnectionServiceProxy mTelephonyConnectionService; 208 209 /** 210 * The connection to the conference server which is hosting the conference. 211 */ 212 private TelephonyConnection mConferenceHost; 213 214 /** 215 * The PhoneAccountHandle of the conference host. 216 */ 217 private PhoneAccountHandle mConferenceHostPhoneAccountHandle; 218 219 /** 220 * The address of the conference host. 221 */ 222 private Uri[] mConferenceHostAddress; 223 224 private TelecomAccountRegistry mTelecomAccountRegistry; 225 226 /** 227 * The known conference participant connections. The HashMap is keyed by a Pair containing 228 * the handle and endpoint Uris. 229 * Access to the hashmap is protected by the {@link #mUpdateSyncRoot}. 230 */ 231 private final HashMap<Pair<Uri, Uri>, ConferenceParticipantConnection> 232 mConferenceParticipantConnections = new HashMap<>(); 233 234 /** 235 * Sychronization root used to ensure that updates to the 236 * {@link #mConferenceParticipantConnections} happen atomically are are not interleaved across 237 * threads. There are some instances where the network will send conference event package 238 * data closely spaced. If that happens, it is possible that the interleaving of the update 239 * will cause duplicate participant info to be added. 240 */ 241 private final Object mUpdateSyncRoot = new Object(); 242 243 public void updateConferenceParticipantsAfterCreation() { 244 if (mConferenceHost != null) { 245 Log.v(this, "updateConferenceStateAfterCreation :: process participant update"); 246 handleConferenceParticipantsUpdate(mConferenceHost, 247 mConferenceHost.getConferenceParticipants()); 248 } else { 249 Log.v(this, "updateConferenceStateAfterCreation :: null mConferenceHost"); 250 } 251 } 252 253 /** 254 * Initializes a new {@link ImsConference}. 255 * 256 * @param telephonyConnectionService The connection service responsible for adding new 257 * conferene participants. 258 * @param conferenceHost The telephony connection hosting the conference. 259 * @param phoneAccountHandle The phone account handle associated with the conference. 260 */ 261 public ImsConference(TelecomAccountRegistry telecomAccountRegistry, 262 TelephonyConnectionServiceProxy telephonyConnectionService, 263 TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle) { 264 265 super(phoneAccountHandle); 266 267 mTelecomAccountRegistry = telecomAccountRegistry; 268 269 // Specify the connection time of the conference to be the connection time of the original 270 // connection. 271 long connectTime = conferenceHost.getOriginalConnection().getConnectTime(); 272 setConnectTimeMillis(connectTime); 273 // Set the connectTime in the connection as well. 274 conferenceHost.setConnectTimeMillis(connectTime); 275 276 mTelephonyConnectionService = telephonyConnectionService; 277 setConferenceHost(conferenceHost); 278 279 int capabilities = Connection.CAPABILITY_MUTE | 280 Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 281 if (canHoldImsCalls()) { 282 capabilities |= Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD; 283 } 284 capabilities = applyHostCapabilities(capabilities, 285 mConferenceHost.getConnectionCapabilities(), 286 mConferenceHost.isCarrierVideoConferencingSupported()); 287 setConnectionCapabilities(capabilities); 288 289 } 290 291 /** 292 * Transfers capabilities from the conference host to the conference itself. 293 * 294 * @param conferenceCapabilities The current conference capabilities. 295 * @param capabilities The new conference host capabilities. 296 * @param isVideoConferencingSupported Whether video conferencing is supported. 297 * @return The merged capabilities to be applied to the conference. 298 */ 299 private int applyHostCapabilities(int conferenceCapabilities, int capabilities, 300 boolean isVideoConferencingSupported) { 301 302 conferenceCapabilities = changeBitmask(conferenceCapabilities, 303 Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL, 304 can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)); 305 306 if (isVideoConferencingSupported) { 307 conferenceCapabilities = changeBitmask(conferenceCapabilities, 308 Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL, 309 can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)); 310 conferenceCapabilities = changeBitmask(conferenceCapabilities, 311 Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO, 312 can(capabilities, Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO)); 313 } else { 314 // If video conferencing is not supported, explicitly turn off the remote video 315 // capability and the ability to upgrade to video. 316 Log.v(this, "applyHostCapabilities : video conferencing not supported"); 317 conferenceCapabilities = changeBitmask(conferenceCapabilities, 318 Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL, false); 319 conferenceCapabilities = changeBitmask(conferenceCapabilities, 320 Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO, false); 321 } 322 323 conferenceCapabilities = changeBitmask(conferenceCapabilities, 324 Connection.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO, 325 can(capabilities, Connection.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO)); 326 327 conferenceCapabilities = changeBitmask(conferenceCapabilities, 328 Connection.CAPABILITY_CAN_PAUSE_VIDEO, 329 mConferenceHost.getVideoPauseSupported() && isVideoCapable()); 330 331 return conferenceCapabilities; 332 } 333 334 /** 335 * Transfers properties from the conference host to the conference itself. 336 * 337 * @param conferenceProperties The current conference properties. 338 * @param properties The new conference host properties. 339 * @return The merged properties to be applied to the conference. 340 */ 341 private int applyHostProperties(int conferenceProperties, int properties) { 342 conferenceProperties = changeBitmask(conferenceProperties, 343 Connection.PROPERTY_HIGH_DEF_AUDIO, 344 can(properties, Connection.PROPERTY_HIGH_DEF_AUDIO)); 345 346 conferenceProperties = changeBitmask(conferenceProperties, 347 Connection.PROPERTY_WIFI, 348 can(properties, Connection.PROPERTY_WIFI)); 349 350 conferenceProperties = changeBitmask(conferenceProperties, 351 Connection.PROPERTY_IS_EXTERNAL_CALL, 352 can(properties, Connection.PROPERTY_IS_EXTERNAL_CALL)); 353 354 return conferenceProperties; 355 } 356 357 /** 358 * Not used by the IMS conference controller. 359 * 360 * @return {@code Null}. 361 */ 362 @Override 363 public android.telecom.Connection getPrimaryConnection() { 364 return null; 365 } 366 367 /** 368 * Returns VideoProvider of the conference. This can be null. 369 * 370 * @hide 371 */ 372 @Override 373 public VideoProvider getVideoProvider() { 374 if (mConferenceHost != null) { 375 return mConferenceHost.getVideoProvider(); 376 } 377 return null; 378 } 379 380 /** 381 * Returns video state of conference 382 * 383 * @hide 384 */ 385 @Override 386 public int getVideoState() { 387 if (mConferenceHost != null) { 388 return mConferenceHost.getVideoState(); 389 } 390 return VideoProfile.STATE_AUDIO_ONLY; 391 } 392 393 /** 394 * Invoked when the Conference and all its {@link Connection}s should be disconnected. 395 * <p> 396 * Hangs up the call via the conference host connection. When the host connection has been 397 * successfully disconnected, the {@link #mConferenceHostListener} listener receives an 398 * {@code onDestroyed} event, which triggers the conference participant connections to be 399 * disconnected. 400 */ 401 @Override 402 public void onDisconnect() { 403 Log.v(this, "onDisconnect: hanging up conference host."); 404 if (mConferenceHost == null) { 405 return; 406 } 407 408 disconnectConferenceParticipants(); 409 410 Call call = mConferenceHost.getCall(); 411 if (call != null) { 412 try { 413 call.hangup(); 414 } catch (CallStateException e) { 415 Log.e(this, e, "Exception thrown trying to hangup conference"); 416 } 417 } 418 } 419 420 /** 421 * Invoked when the specified {@link android.telecom.Connection} should be separated from the 422 * conference call. 423 * <p> 424 * IMS does not support separating connections from the conference. 425 * 426 * @param connection The connection to separate. 427 */ 428 @Override 429 public void onSeparate(android.telecom.Connection connection) { 430 Log.wtf(this, "Cannot separate connections from an IMS conference."); 431 } 432 433 /** 434 * Invoked when the specified {@link android.telecom.Connection} should be merged into the 435 * conference call. 436 * 437 * @param connection The {@code Connection} to merge. 438 */ 439 @Override 440 public void onMerge(android.telecom.Connection connection) { 441 try { 442 Phone phone = mConferenceHost.getPhone(); 443 if (phone != null) { 444 phone.conference(); 445 } 446 } catch (CallStateException e) { 447 Log.e(this, e, "Exception thrown trying to merge call into a conference"); 448 } 449 } 450 451 /** 452 * Invoked when the conference should be put on hold. 453 */ 454 @Override 455 public void onHold() { 456 if (mConferenceHost == null) { 457 return; 458 } 459 mConferenceHost.performHold(); 460 } 461 462 /** 463 * Invoked when the conference should be moved from hold to active. 464 */ 465 @Override 466 public void onUnhold() { 467 if (mConferenceHost == null) { 468 return; 469 } 470 mConferenceHost.performUnhold(); 471 } 472 473 /** 474 * Invoked to play a DTMF tone. 475 * 476 * @param c A DTMF character. 477 */ 478 @Override 479 public void onPlayDtmfTone(char c) { 480 if (mConferenceHost == null) { 481 return; 482 } 483 mConferenceHost.onPlayDtmfTone(c); 484 } 485 486 /** 487 * Invoked to stop playing a DTMF tone. 488 */ 489 @Override 490 public void onStopDtmfTone() { 491 if (mConferenceHost == null) { 492 return; 493 } 494 mConferenceHost.onStopDtmfTone(); 495 } 496 497 /** 498 * Handles the addition of connections to the {@link ImsConference}. The 499 * {@link ImsConferenceController} does not add connections to the conference. 500 * 501 * @param connection The newly added connection. 502 */ 503 @Override 504 public void onConnectionAdded(android.telecom.Connection connection) { 505 // No-op 506 } 507 508 /** 509 * Changes a bit-mask to add or remove a bit-field. 510 * 511 * @param bitmask The bit-mask. 512 * @param bitfield The bit-field to change. 513 * @param enabled Whether the bit-field should be set or removed. 514 * @return The bit-mask with the bit-field changed. 515 */ 516 private int changeBitmask(int bitmask, int bitfield, boolean enabled) { 517 if (enabled) { 518 return bitmask | bitfield; 519 } else { 520 return bitmask & ~bitfield; 521 } 522 } 523 524 /** 525 * Determines if this conference is hosted on the current device or the peer device. 526 * 527 * @return {@code true} if this conference is hosted on the current device, {@code false} if it 528 * is hosted on the peer device. 529 */ 530 public boolean isConferenceHost() { 531 if (mConferenceHost == null) { 532 return false; 533 } 534 com.android.internal.telephony.Connection originalConnection = 535 mConferenceHost.getOriginalConnection(); 536 537 return originalConnection != null && originalConnection.isMultiparty() && 538 originalConnection.isConferenceHost(); 539 } 540 541 /** 542 * Updates the manage conference capability of the conference. Where there are one or more 543 * conference event package participants, the conference management is permitted. Where there 544 * are no conference event package participants, conference management is not permitted. 545 * <p> 546 * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure 547 * that the conference is represented appropriately on Bluetooth devices. 548 */ 549 private void updateManageConference() { 550 boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE); 551 boolean canManageConference = !mConferenceParticipantConnections.isEmpty(); 552 Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N", 553 canManageConference ? "Y" : "N"); 554 555 if (couldManageConference != canManageConference) { 556 int capabilities = getConnectionCapabilities(); 557 558 if (canManageConference) { 559 capabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE; 560 capabilities &= ~Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 561 } else { 562 capabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE; 563 capabilities |= Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 564 } 565 566 setConnectionCapabilities(capabilities); 567 } 568 } 569 570 /** 571 * Sets the connection hosting the conference and registers for callbacks. 572 * 573 * @param conferenceHost The connection hosting the conference. 574 */ 575 private void setConferenceHost(TelephonyConnection conferenceHost) { 576 if (Log.VERBOSE) { 577 Log.v(this, "setConferenceHost " + conferenceHost); 578 } 579 580 mConferenceHost = conferenceHost; 581 582 // Attempt to get the conference host's address (e.g. the host's own phone number). 583 // We need to look at the default phone for the ImsPhone when creating the phone account 584 // for the 585 if (mConferenceHost.getPhone() != null && 586 mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 587 // Look up the conference host's address; we need this later for filtering out the 588 // conference host in conference event package data. 589 Phone imsPhone = mConferenceHost.getPhone(); 590 mConferenceHostPhoneAccountHandle = 591 PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); 592 Uri hostAddress = mTelecomAccountRegistry.getAddress(mConferenceHostPhoneAccountHandle); 593 594 ArrayList<Uri> hostAddresses = new ArrayList<>(); 595 596 // add address from TelecomAccountRegistry 597 if (hostAddress != null) { 598 hostAddresses.add(hostAddress); 599 } 600 601 // add addresses from phone 602 if (imsPhone.getCurrentSubscriberUris() != null) { 603 hostAddresses.addAll( 604 new ArrayList<>(Arrays.asList(imsPhone.getCurrentSubscriberUris()))); 605 } 606 607 mConferenceHostAddress = new Uri[hostAddresses.size()]; 608 mConferenceHostAddress = hostAddresses.toArray(mConferenceHostAddress); 609 } 610 611 mConferenceHost.addConnectionListener(mConferenceHostListener); 612 mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener); 613 setConnectionCapabilities(applyHostCapabilities(getConnectionCapabilities(), 614 mConferenceHost.getConnectionCapabilities(), 615 mConferenceHost.isCarrierVideoConferencingSupported())); 616 setConnectionProperties(applyHostProperties(getConnectionProperties(), 617 mConferenceHost.getConnectionProperties())); 618 619 setState(mConferenceHost.getState()); 620 updateStatusHints(); 621 } 622 623 /** 624 * Handles state changes for conference participant(s). The participants data passed in 625 * 626 * @param parent The connection which was notified of the conference participant. 627 * @param participants The conference participant information. 628 */ 629 private void handleConferenceParticipantsUpdate( 630 TelephonyConnection parent, List<ConferenceParticipant> participants) { 631 632 if (participants == null) { 633 return; 634 } 635 636 Log.i(this, "handleConferenceParticipantsUpdate: size=%d", participants.size()); 637 638 // Perform the update in a synchronized manner. It is possible for the IMS framework to 639 // trigger two onConferenceParticipantsChanged callbacks in quick succession. If the first 640 // update adds new participants, and the second does something like update the status of one 641 // of the participants, we can get into a situation where the participant is added twice. 642 synchronized (mUpdateSyncRoot) { 643 boolean newParticipantsAdded = false; 644 boolean oldParticipantsRemoved = false; 645 ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size()); 646 HashSet<Pair<Uri,Uri>> participantUserEntities = new HashSet<>(participants.size()); 647 648 // Add any new participants and update existing. 649 for (ConferenceParticipant participant : participants) { 650 Pair<Uri,Uri> userEntity = new Pair<>(participant.getHandle(), 651 participant.getEndpoint()); 652 653 participantUserEntities.add(userEntity); 654 if (!mConferenceParticipantConnections.containsKey(userEntity)) { 655 // Some carriers will also include the conference host in the CEP. We will 656 // filter that out here. 657 if (!isParticipantHost(mConferenceHostAddress, participant.getHandle())) { 658 createConferenceParticipantConnection(parent, participant); 659 newParticipants.add(participant); 660 newParticipantsAdded = true; 661 } 662 } else { 663 ConferenceParticipantConnection connection = 664 mConferenceParticipantConnections.get(userEntity); 665 Log.i(this, "handleConferenceParticipantsUpdate: updateState, participant = %s", 666 participant); 667 connection.updateState(participant.getState()); 668 } 669 } 670 671 // Set state of new participants. 672 if (newParticipantsAdded) { 673 // Set the state of the new participants at once and add to the conference 674 for (ConferenceParticipant newParticipant : newParticipants) { 675 ConferenceParticipantConnection connection = 676 mConferenceParticipantConnections.get(new Pair<>( 677 newParticipant.getHandle(), 678 newParticipant.getEndpoint())); 679 connection.updateState(newParticipant.getState()); 680 } 681 } 682 683 // Finally, remove any participants from the conference that no longer exist in the 684 // conference event package data. 685 Iterator<Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection>> entryIterator = 686 mConferenceParticipantConnections.entrySet().iterator(); 687 while (entryIterator.hasNext()) { 688 Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection> entry = 689 entryIterator.next(); 690 691 if (!participantUserEntities.contains(entry.getKey())) { 692 ConferenceParticipantConnection participant = entry.getValue(); 693 participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); 694 participant.removeConnectionListener(mParticipantListener); 695 mTelephonyConnectionService.removeConnection(participant); 696 removeConnection(participant); 697 entryIterator.remove(); 698 oldParticipantsRemoved = true; 699 } 700 } 701 702 // If new participants were added or old ones were removed, we need to ensure the state 703 // of the manage conference capability is updated. 704 if (newParticipantsAdded || oldParticipantsRemoved) { 705 updateManageConference(); 706 } 707 } 708 } 709 710 /** 711 * Creates a new {@link ConferenceParticipantConnection} to represent a 712 * {@link ConferenceParticipant}. 713 * <p> 714 * The new connection is added to the conference controller and connection service. 715 * 716 * @param parent The connection which was notified of the participant change (e.g. the 717 * parent connection). 718 * @param participant The conference participant information. 719 */ 720 private void createConferenceParticipantConnection( 721 TelephonyConnection parent, ConferenceParticipant participant) { 722 723 // Create and add the new connection in holding state so that it does not become the 724 // active call. 725 ConferenceParticipantConnection connection = new ConferenceParticipantConnection( 726 parent.getOriginalConnection(), participant); 727 connection.addConnectionListener(mParticipantListener); 728 connection.setConnectTimeMillis(parent.getConnectTimeMillis()); 729 730 Log.i(this, "createConferenceParticipantConnection: participant=%s, connection=%s", 731 participant, connection); 732 733 synchronized(mUpdateSyncRoot) { 734 mConferenceParticipantConnections.put(new Pair<>(participant.getHandle(), 735 participant.getEndpoint()), connection); 736 } 737 738 mTelephonyConnectionService.addExistingConnection(mConferenceHostPhoneAccountHandle, 739 connection, this); 740 addConnection(connection); 741 } 742 743 /** 744 * Removes a conference participant from the conference. 745 * 746 * @param participant The participant to remove. 747 */ 748 private void removeConferenceParticipant(ConferenceParticipantConnection participant) { 749 Log.i(this, "removeConferenceParticipant: %s", participant); 750 751 participant.removeConnectionListener(mParticipantListener); 752 synchronized(mUpdateSyncRoot) { 753 mConferenceParticipantConnections.remove(participant.getUserEntity()); 754 } 755 mTelephonyConnectionService.removeConnection(participant); 756 } 757 758 /** 759 * Disconnects all conference participants from the conference. 760 */ 761 private void disconnectConferenceParticipants() { 762 Log.v(this, "disconnectConferenceParticipants"); 763 764 synchronized(mUpdateSyncRoot) { 765 for (ConferenceParticipantConnection connection : 766 mConferenceParticipantConnections.values()) { 767 768 connection.removeConnectionListener(mParticipantListener); 769 // Mark disconnect cause as cancelled to ensure that the call is not logged in the 770 // call log. 771 connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); 772 mTelephonyConnectionService.removeConnection(connection); 773 connection.destroy(); 774 } 775 mConferenceParticipantConnections.clear(); 776 } 777 } 778 779 /** 780 * Determines if the passed in participant handle is the same as the conference host's handle. 781 * Starts with a simple equality check. However, the handles from a conference event package 782 * will be a SIP uri, so we need to pull that apart to look for the participant's phone number. 783 * 784 * @param hostHandles The handle(s) of the connection hosting the conference. 785 * @param handle The handle of the conference participant. 786 * @return {@code true} if the host's handle matches the participant's handle, {@code false} 787 * otherwise. 788 */ 789 private boolean isParticipantHost(Uri[] hostHandles, Uri handle) { 790 // If there is no host handle or no participant handle, bail early. 791 if (hostHandles == null || hostHandles.length == 0 || handle == null) { 792 Log.v(this, "isParticipantHost(N) : host or participant uri null"); 793 return false; 794 } 795 796 // Conference event package participants are identified using SIP URIs (see RFC3261). 797 // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers 798 // Per RFC3261, the "user" can be a telephone number. 799 // For example: sip:1650555121;phone-context=blah.com@host.com 800 // In this case, the phone number is in the user field of the URI, and the parameters can be 801 // ignored. 802 // 803 // A SIP URI can also specify a phone number in a format similar to: 804 // sip:+1-212-555-1212@something.com;user=phone 805 // In this case, the phone number is again in user field and the parameters can be ignored. 806 // We can get the user field in these instances by splitting the string on the @, ;, or : 807 // and looking at the first found item. 808 809 String number = handle.getSchemeSpecificPart(); 810 String numberParts[] = number.split("[@;:]"); 811 812 if (numberParts.length == 0) { 813 Log.v(this, "isParticipantHost(N) : no number in participant handle"); 814 return false; 815 } 816 number = numberParts[0]; 817 818 for (Uri hostHandle : hostHandles) { 819 if (hostHandle == null) { 820 continue; 821 } 822 // The host number will be a tel: uri. Per RFC3966, the part after tel: is the phone 823 // number. 824 String hostNumber = hostHandle.getSchemeSpecificPart(); 825 826 // Use a loose comparison of the phone numbers. This ensures that numbers that differ 827 // by special characters are counted as equal. 828 // E.g. +16505551212 would be the same as 16505551212 829 boolean isHost = PhoneNumberUtils.compare(hostNumber, number); 830 831 Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"), 832 Log.pii(hostNumber), Log.pii(number)); 833 834 if (isHost) { 835 return true; 836 } 837 } 838 return false; 839 } 840 841 /** 842 * Handles a change in the original connection backing the conference host connection. This can 843 * happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to 844 * GSM or CDMA. 845 * <p> 846 * If this happens, we will add the conference host connection to telecom and tear down the 847 * conference. 848 */ 849 private void handleOriginalConnectionChange() { 850 if (mConferenceHost == null) { 851 Log.w(this, "handleOriginalConnectionChange; conference host missing."); 852 return; 853 } 854 855 com.android.internal.telephony.Connection originalConnection = 856 mConferenceHost.getOriginalConnection(); 857 858 if (originalConnection != null && 859 originalConnection.getPhoneType() != PhoneConstants.PHONE_TYPE_IMS) { 860 Log.i(this, 861 "handleOriginalConnectionChange : handover from IMS connection to " + 862 "new connection: %s", originalConnection); 863 864 PhoneAccountHandle phoneAccountHandle = null; 865 if (mConferenceHost.getPhone() != null) { 866 if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 867 Phone imsPhone = mConferenceHost.getPhone(); 868 // The phone account handle for an ImsPhone is based on the default phone (ie 869 // the base GSM or CDMA phone, not on the ImsPhone itself). 870 phoneAccountHandle = 871 PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); 872 } else { 873 // In the case of SRVCC, we still need a phone account, so use the top level 874 // phone to create a phone account. 875 phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle( 876 mConferenceHost.getPhone()); 877 } 878 } 879 880 if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) { 881 Log.i(this,"handleOriginalConnectionChange : SRVCC to GSM"); 882 GsmConnection c = new GsmConnection(originalConnection, getTelecomCallId(), 883 mConferenceHost.isOutgoingCall()); 884 // This is a newly created conference connection as a result of SRVCC 885 c.setConferenceSupported(true); 886 c.addCapability(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN); 887 c.setConnectionProperties( 888 c.getConnectionProperties() | Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE); 889 c.updateState(); 890 // Copy the connect time from the conferenceHost 891 c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis()); 892 mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c); 893 mTelephonyConnectionService.addConnectionToConferenceController(c); 894 } // CDMA case not applicable for SRVCC 895 mConferenceHost.removeConnectionListener(mConferenceHostListener); 896 mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener); 897 mConferenceHost = null; 898 setDisconnected(new DisconnectCause(DisconnectCause.OTHER)); 899 disconnectConferenceParticipants(); 900 destroy(); 901 } 902 903 updateStatusHints(); 904 } 905 906 /** 907 * Changes the state of the Ims conference. 908 * 909 * @param state the new state. 910 */ 911 public void setState(int state) { 912 Log.v(this, "setState %s", Connection.stateToString(state)); 913 914 switch (state) { 915 case Connection.STATE_INITIALIZING: 916 case Connection.STATE_NEW: 917 case Connection.STATE_RINGING: 918 // No-op -- not applicable. 919 break; 920 case Connection.STATE_DIALING: 921 setDialing(); 922 break; 923 case Connection.STATE_DISCONNECTED: 924 DisconnectCause disconnectCause; 925 if (mConferenceHost == null) { 926 disconnectCause = new DisconnectCause(DisconnectCause.CANCELED); 927 } else { 928 disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause( 929 mConferenceHost.getOriginalConnection().getDisconnectCause()); 930 } 931 setDisconnected(disconnectCause); 932 disconnectConferenceParticipants(); 933 destroy(); 934 break; 935 case Connection.STATE_ACTIVE: 936 setActive(); 937 break; 938 case Connection.STATE_HOLDING: 939 setOnHold(); 940 break; 941 } 942 } 943 944 /** 945 * Determines if the host of this conference is capable of video calling. 946 * @return {@code true} if video capable, {@code false} otherwise. 947 */ 948 private boolean isVideoCapable() { 949 int capabilities = mConferenceHost.getConnectionCapabilities(); 950 return can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL) 951 && can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL); 952 } 953 954 private void updateStatusHints() { 955 if (mConferenceHost == null) { 956 setStatusHints(null); 957 return; 958 } 959 960 if (mConferenceHost.isWifi()) { 961 Phone phone = mConferenceHost.getPhone(); 962 if (phone != null) { 963 Context context = phone.getContext(); 964 setStatusHints(new StatusHints( 965 context.getString(R.string.status_hint_label_wifi_call), 966 Icon.createWithResource( 967 context.getResources(), 968 R.drawable.ic_signal_wifi_4_bar_24dp), 969 null /* extras */)); 970 } 971 } else { 972 setStatusHints(null); 973 } 974 } 975 976 /** 977 * Builds a string representation of the {@link ImsConference}. 978 * 979 * @return String representing the conference. 980 */ 981 public String toString() { 982 StringBuilder sb = new StringBuilder(); 983 sb.append("[ImsConference objId:"); 984 sb.append(System.identityHashCode(this)); 985 sb.append(" telecomCallID:"); 986 sb.append(getTelecomCallId()); 987 sb.append(" state:"); 988 sb.append(Connection.stateToString(getState())); 989 sb.append(" hostConnection:"); 990 sb.append(mConferenceHost); 991 sb.append(" participants:"); 992 sb.append(mConferenceParticipantConnections.size()); 993 sb.append("]"); 994 return sb.toString(); 995 } 996 997 private boolean canHoldImsCalls() { 998 PersistableBundle b = getCarrierConfig(); 999 // Return true if the CarrierConfig is unavailable 1000 return b == null || b.getBoolean(CarrierConfigManager.KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL); 1001 } 1002 1003 private PersistableBundle getCarrierConfig() { 1004 if (mConferenceHost == null) { 1005 return null; 1006 } 1007 1008 Phone phone = mConferenceHost.getPhone(); 1009 if (phone == null) { 1010 return null; 1011 } 1012 return PhoneGlobals.getInstance().getCarrierConfigForSubId(phone.getSubId()); 1013 } 1014 1015 /** 1016 * @return {@code true} if the carrier associated with the conference requires that the maximum 1017 * size of the conference is enforced, {@code false} otherwise. 1018 */ 1019 public boolean isMaximumConferenceSizeEnforced() { 1020 PersistableBundle b = getCarrierConfig(); 1021 // Return false if the CarrierConfig is unavailable 1022 return b != null && b.getBoolean( 1023 CarrierConfigManager.KEY_IS_IMS_CONFERENCE_SIZE_ENFORCED_BOOL); 1024 } 1025 1026 /** 1027 * @return The maximum size of a conference call where 1028 * {@link #isMaximumConferenceSizeEnforced()} is true. 1029 */ 1030 public int getMaximumConferenceSize() { 1031 PersistableBundle b = getCarrierConfig(); 1032 1033 // If there is no carrier config its really a problem, but we'll still define a sane limit 1034 // of 5 so that we can still make a conference. 1035 if (b == null) { 1036 Log.w(this, "getMaximumConferenceSize - failed to get conference size"); 1037 return 5; 1038 } 1039 return b.getInt(CarrierConfigManager.KEY_IMS_CONFERENCE_SIZE_LIMIT_INT); 1040 } 1041 1042 /** 1043 * @return The number of participants in the conference. 1044 */ 1045 public int getNumberOfParticipants() { 1046 return mConferenceParticipantConnections.size(); 1047 } 1048 1049 /** 1050 * @return {@code True} if the carrier enforces a maximum conference size, and the number of 1051 * participants in the conference has reached the limit, {@code false} otherwise. 1052 */ 1053 public boolean isFullConference() { 1054 return isMaximumConferenceSizeEnforced() 1055 && getNumberOfParticipants() >= getMaximumConferenceSize(); 1056 } 1057} 1058