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