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