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