ImsConference.java revision 6d83e5eb90ee8b4551d61c4789bb8f124c4c12d2
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 * @param phoneAccountHandle The phone account handle associated with the conference. 240 */ 241 public ImsConference(TelephonyConnectionService telephonyConnectionService, 242 TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle) { 243 244 super(phoneAccountHandle); 245 246 // Specify the connection time of the conference to be the connection time of the original 247 // connection. 248 long connectTime = conferenceHost.getOriginalConnection().getConnectTime(); 249 setConnectTimeMillis(connectTime); 250 // Set the connectTime in the connection as well. 251 conferenceHost.setConnectTimeMillis(connectTime); 252 253 mTelephonyConnectionService = telephonyConnectionService; 254 setConferenceHost(conferenceHost); 255 256 int capabilities = Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD | 257 Connection.CAPABILITY_MUTE | Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 258 capabilities = applyHostCapabilities(capabilities, 259 mConferenceHost.getConnectionCapabilities()); 260 setConnectionCapabilities(capabilities); 261 262 } 263 264 /** 265 * Transfers capabilities from the conference host to the conference itself. 266 * 267 * @param conferenceCapabilities The current conference capabilities. 268 * @param capabilities The new conference host capabilities. 269 * @return The merged capabilities to be applied to the conference. 270 */ 271 private int applyHostCapabilities(int conferenceCapabilities, int capabilities) { 272 if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)) { 273 conferenceCapabilities = applyCapability(conferenceCapabilities, 274 Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL); 275 } else { 276 conferenceCapabilities = removeCapability(conferenceCapabilities, 277 Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL); 278 } 279 280 if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)) { 281 conferenceCapabilities = applyCapability(conferenceCapabilities, 282 Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL); 283 } else { 284 conferenceCapabilities = removeCapability(conferenceCapabilities, 285 Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL); 286 } 287 288 if (can(capabilities, Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO)) { 289 conferenceCapabilities = applyCapability(conferenceCapabilities, 290 Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO); 291 } else { 292 conferenceCapabilities = removeCapability(conferenceCapabilities, 293 Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO); 294 } 295 296 if (can(capabilities, Connection.CAPABILITY_HIGH_DEF_AUDIO)) { 297 conferenceCapabilities = applyCapability(conferenceCapabilities, 298 Connection.CAPABILITY_HIGH_DEF_AUDIO); 299 } else { 300 conferenceCapabilities = removeCapability(conferenceCapabilities, 301 Connection.CAPABILITY_HIGH_DEF_AUDIO); 302 } 303 return conferenceCapabilities; 304 } 305 306 /** 307 * Not used by the IMS conference controller. 308 * 309 * @return {@code Null}. 310 */ 311 @Override 312 public android.telecom.Connection getPrimaryConnection() { 313 return null; 314 } 315 316 /** 317 * Returns VideoProvider of the conference. This can be null. 318 * 319 * @hide 320 */ 321 @Override 322 public VideoProvider getVideoProvider() { 323 if (mConferenceHost != null) { 324 return mConferenceHost.getVideoProvider(); 325 } 326 return null; 327 } 328 329 /** 330 * Returns video state of conference 331 * 332 * @hide 333 */ 334 @Override 335 public int getVideoState() { 336 if (mConferenceHost != null) { 337 return mConferenceHost.getVideoState(); 338 } 339 return VideoProfile.STATE_AUDIO_ONLY; 340 } 341 342 /** 343 * Invoked when the Conference and all its {@link Connection}s should be disconnected. 344 * <p> 345 * Hangs up the call via the conference host connection. When the host connection has been 346 * successfully disconnected, the {@link #mConferenceHostListener} listener receives an 347 * {@code onDestroyed} event, which triggers the conference participant connections to be 348 * disconnected. 349 */ 350 @Override 351 public void onDisconnect() { 352 Log.v(this, "onDisconnect: hanging up conference host."); 353 if (mConferenceHost == null) { 354 return; 355 } 356 357 Call call = mConferenceHost.getCall(); 358 if (call != null) { 359 try { 360 call.hangup(); 361 } catch (CallStateException e) { 362 Log.e(this, e, "Exception thrown trying to hangup conference"); 363 } 364 } 365 } 366 367 /** 368 * Invoked when the specified {@link android.telecom.Connection} should be separated from the 369 * conference call. 370 * <p> 371 * IMS does not support separating connections from the conference. 372 * 373 * @param connection The connection to separate. 374 */ 375 @Override 376 public void onSeparate(android.telecom.Connection connection) { 377 Log.wtf(this, "Cannot separate connections from an IMS conference."); 378 } 379 380 /** 381 * Invoked when the specified {@link android.telecom.Connection} should be merged into the 382 * conference call. 383 * 384 * @param connection The {@code Connection} to merge. 385 */ 386 @Override 387 public void onMerge(android.telecom.Connection connection) { 388 try { 389 Phone phone = ((TelephonyConnection) connection).getPhone(); 390 if (phone != null) { 391 phone.conference(); 392 } 393 } catch (CallStateException e) { 394 Log.e(this, e, "Exception thrown trying to merge call into a conference"); 395 } 396 } 397 398 /** 399 * Invoked when the conference should be put on hold. 400 */ 401 @Override 402 public void onHold() { 403 if (mConferenceHost == null) { 404 return; 405 } 406 mConferenceHost.performHold(); 407 } 408 409 /** 410 * Invoked when the conference should be moved from hold to active. 411 */ 412 @Override 413 public void onUnhold() { 414 if (mConferenceHost == null) { 415 return; 416 } 417 mConferenceHost.performUnhold(); 418 } 419 420 /** 421 * Invoked to play a DTMF tone. 422 * 423 * @param c A DTMF character. 424 */ 425 @Override 426 public void onPlayDtmfTone(char c) { 427 if (mConferenceHost == null) { 428 return; 429 } 430 mConferenceHost.onPlayDtmfTone(c); 431 } 432 433 /** 434 * Invoked to stop playing a DTMF tone. 435 */ 436 @Override 437 public void onStopDtmfTone() { 438 if (mConferenceHost == null) { 439 return; 440 } 441 mConferenceHost.onStopDtmfTone(); 442 } 443 444 /** 445 * Handles the addition of connections to the {@link ImsConference}. The 446 * {@link ImsConferenceController} does not add connections to the conference. 447 * 448 * @param connection The newly added connection. 449 */ 450 @Override 451 public void onConnectionAdded(android.telecom.Connection connection) { 452 // No-op 453 } 454 455 private int applyCapability(int capabilities, int capability) { 456 int newCapabilities = capabilities | capability; 457 return newCapabilities; 458 } 459 460 private int removeCapability(int capabilities, int capability) { 461 int newCapabilities = capabilities & ~capability; 462 return newCapabilities; 463 } 464 465 /** 466 * Determines if this conference is hosted on the current device or the peer device. 467 * 468 * @return {@code true} if this conference is hosted on the current device, {@code false} if it 469 * is hosted on the peer device. 470 */ 471 public boolean isConferenceHost() { 472 if (mConferenceHost == null) { 473 return false; 474 } 475 com.android.internal.telephony.Connection originalConnection = 476 mConferenceHost.getOriginalConnection(); 477 478 return originalConnection.isMultiparty() && originalConnection.isConferenceHost(); 479 } 480 481 /** 482 * Updates the manage conference capability of the conference. Where there are one or more 483 * conference event package participants, the conference management is permitted. Where there 484 * are no conference event package participants, conference management is not permitted. 485 * <p> 486 * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure 487 * that the conference is represented appropriately on Bluetooth devices. 488 */ 489 private void updateManageConference() { 490 boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE); 491 boolean canManageConference = !mConferenceParticipantConnections.isEmpty(); 492 Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N", 493 canManageConference ? "Y" : "N"); 494 495 if (couldManageConference != canManageConference) { 496 int capabilities = getConnectionCapabilities(); 497 498 if (canManageConference) { 499 capabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE; 500 capabilities &= ~Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 501 } else { 502 capabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE; 503 capabilities |= Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; 504 } 505 506 setConnectionCapabilities(capabilities); 507 } 508 } 509 510 /** 511 * Sets the connection hosting the conference and registers for callbacks. 512 * 513 * @param conferenceHost The connection hosting the conference. 514 */ 515 private void setConferenceHost(TelephonyConnection conferenceHost) { 516 if (Log.VERBOSE) { 517 Log.v(this, "setConferenceHost " + conferenceHost); 518 } 519 520 mConferenceHost = conferenceHost; 521 522 // Attempt to get the conference host's address (e.g. the host's own phone number). 523 // We need to look at the default phone for the ImsPhone when creating the phone account 524 // for the 525 if (mConferenceHost.getPhone() != null && 526 mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 527 // Look up the conference host's address; we need this later for filtering out the 528 // conference host in conference event package data. 529 Phone imsPhone = mConferenceHost.getPhone(); 530 mConferenceHostPhoneAccountHandle = 531 PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); 532 mConferenceHostAddress = TelecomAccountRegistry.getInstance(mTelephonyConnectionService) 533 .getAddress(mConferenceHostPhoneAccountHandle); 534 } 535 536 mConferenceHost.addConnectionListener(mConferenceHostListener); 537 mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener); 538 setState(mConferenceHost.getState()); 539 540 updateStatusHints(); 541 } 542 543 /** 544 * Handles state changes for conference participant(s). The participants data passed in 545 * 546 * @param parent The connection which was notified of the conference participant. 547 * @param participants The conference participant information. 548 */ 549 private void handleConferenceParticipantsUpdate( 550 TelephonyConnection parent, List<ConferenceParticipant> participants) { 551 552 if (participants == null) { 553 return; 554 } 555 556 // Perform the update in a synchronized manner. It is possible for the IMS framework to 557 // trigger two onConferenceParticipantsChanged callbacks in quick succession. If the first 558 // update adds new participants, and the second does something like update the status of one 559 // of the participants, we can get into a situation where the participant is added twice. 560 synchronized (mUpdateSyncRoot) { 561 boolean newParticipantsAdded = false; 562 boolean oldParticipantsRemoved = false; 563 ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size()); 564 HashSet<Uri> participantUserEntities = new HashSet<>(participants.size()); 565 566 // Add any new participants and update existing. 567 for (ConferenceParticipant participant : participants) { 568 Uri userEntity = participant.getHandle(); 569 570 participantUserEntities.add(userEntity); 571 if (!mConferenceParticipantConnections.containsKey(userEntity)) { 572 // Some carriers will also include the conference host in the CEP. We will 573 // filter that out here. 574 if (!isParticipantHost(mConferenceHostAddress, userEntity)) { 575 createConferenceParticipantConnection(parent, participant); 576 newParticipants.add(participant); 577 newParticipantsAdded = true; 578 } 579 } else { 580 ConferenceParticipantConnection connection = 581 mConferenceParticipantConnections.get(userEntity); 582 connection.updateState(participant.getState()); 583 } 584 } 585 586 // Set state of new participants. 587 if (newParticipantsAdded) { 588 // Set the state of the new participants at once and add to the conference 589 for (ConferenceParticipant newParticipant : newParticipants) { 590 ConferenceParticipantConnection connection = 591 mConferenceParticipantConnections.get(newParticipant.getHandle()); 592 connection.updateState(newParticipant.getState()); 593 } 594 } 595 596 // Finally, remove any participants from the conference that no longer exist in the 597 // conference event package data. 598 Iterator<Map.Entry<Uri, ConferenceParticipantConnection>> entryIterator = 599 mConferenceParticipantConnections.entrySet().iterator(); 600 while (entryIterator.hasNext()) { 601 Map.Entry<Uri, ConferenceParticipantConnection> entry = entryIterator.next(); 602 603 if (!participantUserEntities.contains(entry.getKey())) { 604 ConferenceParticipantConnection participant = entry.getValue(); 605 participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); 606 participant.removeConnectionListener(mParticipantListener); 607 mTelephonyConnectionService.removeConnection(participant); 608 removeConnection(participant); 609 entryIterator.remove(); 610 oldParticipantsRemoved = true; 611 } 612 } 613 614 // If new participants were added or old ones were removed, we need to ensure the state 615 // of the manage conference capability is updated. 616 if (newParticipantsAdded || oldParticipantsRemoved) { 617 updateManageConference(); 618 } 619 } 620 } 621 622 /** 623 * Creates a new {@link ConferenceParticipantConnection} to represent a 624 * {@link ConferenceParticipant}. 625 * <p> 626 * The new connection is added to the conference controller and connection service. 627 * 628 * @param parent The connection which was notified of the participant change (e.g. the 629 * parent connection). 630 * @param participant The conference participant information. 631 */ 632 private void createConferenceParticipantConnection( 633 TelephonyConnection parent, ConferenceParticipant participant) { 634 635 // Create and add the new connection in holding state so that it does not become the 636 // active call. 637 ConferenceParticipantConnection connection = new ConferenceParticipantConnection( 638 parent.getOriginalConnection(), participant); 639 connection.addConnectionListener(mParticipantListener); 640 connection.setConnectTimeMillis(parent.getConnectTimeMillis()); 641 642 if (Log.VERBOSE) { 643 Log.v(this, "createConferenceParticipantConnection: %s", connection); 644 } 645 646 synchronized(mUpdateSyncRoot) { 647 mConferenceParticipantConnections.put(participant.getHandle(), connection); 648 } 649 mTelephonyConnectionService.addExistingConnection(mConferenceHostPhoneAccountHandle, 650 connection); 651 addConnection(connection); 652 } 653 654 /** 655 * Removes a conference participant from the conference. 656 * 657 * @param participant The participant to remove. 658 */ 659 private void removeConferenceParticipant(ConferenceParticipantConnection participant) { 660 Log.d(this, "removeConferenceParticipant: %s", participant); 661 662 participant.removeConnectionListener(mParticipantListener); 663 synchronized(mUpdateSyncRoot) { 664 mConferenceParticipantConnections.remove(participant.getUserEntity()); 665 } 666 mTelephonyConnectionService.removeConnection(participant); 667 } 668 669 /** 670 * Disconnects all conference participants from the conference. 671 */ 672 private void disconnectConferenceParticipants() { 673 Log.v(this, "disconnectConferenceParticipants"); 674 675 synchronized(mUpdateSyncRoot) { 676 for (ConferenceParticipantConnection connection : 677 mConferenceParticipantConnections.values()) { 678 679 connection.removeConnectionListener(mParticipantListener); 680 // Mark disconnect cause as cancelled to ensure that the call is not logged in the 681 // call log. 682 connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); 683 mTelephonyConnectionService.removeConnection(connection); 684 connection.destroy(); 685 } 686 mConferenceParticipantConnections.clear(); 687 } 688 } 689 690 /** 691 * Determines if the passed in participant handle is the same as the conference host's handle. 692 * Starts with a simple equality check. However, the handles from a conference event package 693 * will be a SIP uri, so we need to pull that apart to look for the participant's phone number. 694 * 695 * @param hostHandle The handle of the connection hosting the conference. 696 * @param handle The handle of the conference participant. 697 * @return {@code true} if the host's handle matches the participant's handle, {@code false} 698 * otherwise. 699 */ 700 private boolean isParticipantHost(Uri hostHandle, Uri handle) { 701 // If host and participant handles are the same, bail early. 702 if (Objects.equals(hostHandle, handle)) { 703 Log.v(this, "isParticipantHost(Y) : uris equal"); 704 return true; 705 } 706 707 // If there is no host handle or not participant handle, bail early. 708 if (hostHandle == null || handle == null) { 709 Log.v(this, "isParticipantHost(N) : host or participant uri null"); 710 return false; 711 } 712 713 // Conference event package participants are identified using SIP URIs (see RFC3261). 714 // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers 715 // Per RFC3261, the "user" can be a telephone number. 716 // For example: sip:1650555121;phone-context=blah.com@host.com 717 // In this case, the phone number is in the user field of the URI, and the parameters can be 718 // ignored. 719 // 720 // A SIP URI can also specify a phone number in a format similar to: 721 // sip:+1-212-555-1212@something.com;user=phone 722 // In this case, the phone number is again in user field and the parameters can be ignored. 723 // We can get the user field in these instances by splitting the string on the @, ;, or : 724 // and looking at the first found item. 725 726 String number = handle.getSchemeSpecificPart(); 727 String numberParts[] = number.split("[@;:]"); 728 729 if (numberParts.length == 0) { 730 Log.v(this, "isParticipantHost(N) : no number in participant handle"); 731 return false; 732 } 733 number = numberParts[0]; 734 735 // The host number will be a tel: uri. Per RFC3966, the part after tel: is the phone 736 // number. 737 String hostNumber = hostHandle.getSchemeSpecificPart(); 738 739 // Use a loose comparison of the phone numbers. This ensures that numbers that differ by 740 // special characters are counted as equal. 741 // E.g. +16505551212 would be the same as 16505551212 742 boolean isHost = PhoneNumberUtils.compare(hostNumber, number); 743 744 Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"), 745 Log.pii(hostNumber), Log.pii(number)); 746 return isHost; 747 } 748 749 /** 750 * Handles a change in the original connection backing the conference host connection. This can 751 * happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to 752 * GSM or CDMA. 753 * <p> 754 * If this happens, we will add the conference host connection to telecom and tear down the 755 * conference. 756 */ 757 private void handleOriginalConnectionChange() { 758 if (mConferenceHost == null) { 759 Log.w(this, "handleOriginalConnectionChange; conference host missing."); 760 return; 761 } 762 763 com.android.internal.telephony.Connection originalConnection = 764 mConferenceHost.getOriginalConnection(); 765 766 if (originalConnection.getPhoneType() != PhoneConstants.PHONE_TYPE_IMS) { 767 if (Log.VERBOSE) { 768 Log.v(this, 769 "Original connection for conference host is no longer an IMS connection; " + 770 "new connection: %s", originalConnection); 771 } 772 773 PhoneAccountHandle phoneAccountHandle = null; 774 if (mConferenceHost.getPhone() != null && 775 mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) { 776 Phone imsPhone = mConferenceHost.getPhone(); 777 // The phone account handle for an ImsPhone is based on the default phone (ie the 778 // base GSM or CDMA phone, not on the ImsPhone itself). 779 phoneAccountHandle = 780 PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone()); 781 } 782 783 if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) { 784 GsmConnection c = new GsmConnection(originalConnection, getTelecomCallId()); 785 c.updateState(); 786 // Copy the connect time from the conferenceHost 787 c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis()); 788 mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c); 789 mTelephonyConnectionService.addConnectionToConferenceController(c); 790 } // CDMA case not applicable for SRVCC 791 mConferenceHost.removeConnectionListener(mConferenceHostListener); 792 mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener); 793 mConferenceHost = null; 794 setDisconnected(new DisconnectCause(DisconnectCause.OTHER)); 795 disconnectConferenceParticipants(); 796 destroy(); 797 } 798 799 updateStatusHints(); 800 } 801 802 /** 803 * Changes the state of the Ims conference. 804 * 805 * @param state the new state. 806 */ 807 public void setState(int state) { 808 Log.v(this, "setState %s", Connection.stateToString(state)); 809 810 switch (state) { 811 case Connection.STATE_INITIALIZING: 812 case Connection.STATE_NEW: 813 case Connection.STATE_RINGING: 814 // No-op -- not applicable. 815 break; 816 case Connection.STATE_DIALING: 817 setDialing(); 818 break; 819 case Connection.STATE_DISCONNECTED: 820 DisconnectCause disconnectCause; 821 if (mConferenceHost == null) { 822 disconnectCause = new DisconnectCause(DisconnectCause.CANCELED); 823 } else { 824 disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause( 825 mConferenceHost.getOriginalConnection().getDisconnectCause()); 826 } 827 setDisconnected(disconnectCause); 828 disconnectConferenceParticipants(); 829 destroy(); 830 break; 831 case Connection.STATE_ACTIVE: 832 setActive(); 833 break; 834 case Connection.STATE_HOLDING: 835 setOnHold(); 836 break; 837 } 838 } 839 840 private void updateStatusHints() { 841 if (mConferenceHost == null) { 842 setStatusHints(null); 843 return; 844 } 845 846 if (mConferenceHost.isWifi()) { 847 Phone phone = mConferenceHost.getPhone(); 848 if (phone != null) { 849 Context context = phone.getContext(); 850 setStatusHints(new StatusHints( 851 context.getString(R.string.status_hint_label_wifi_call), 852 Icon.createWithResource( 853 context.getResources(), 854 R.drawable.ic_signal_wifi_4_bar_24dp), 855 null /* extras */)); 856 } 857 } else { 858 setStatusHints(null); 859 } 860 } 861 862 /** 863 * Builds a string representation of the {@link ImsConference}. 864 * 865 * @return String representing the conference. 866 */ 867 public String toString() { 868 StringBuilder sb = new StringBuilder(); 869 sb.append("[ImsConference objId:"); 870 sb.append(System.identityHashCode(this)); 871 sb.append(" telecomCallID:"); 872 sb.append(getTelecomCallId()); 873 sb.append(" state:"); 874 sb.append(Connection.stateToString(getState())); 875 sb.append(" hostConnection:"); 876 sb.append(mConferenceHost); 877 sb.append(" participants:"); 878 sb.append(mConferenceParticipantConnections.size()); 879 sb.append("]"); 880 return sb.toString(); 881 } 882} 883