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