1 /* 2 * Copyright 2014, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.telecom; 18 19 import android.content.Context; 20 import android.telecom.DisconnectCause; 21 import android.telecom.Log; 22 import android.telecom.ParcelableConference; 23 import android.telecom.ParcelableConnection; 24 import android.telecom.PhoneAccount; 25 import android.telecom.PhoneAccountHandle; 26 import android.telephony.SubscriptionManager; 27 import android.telephony.TelephonyManager; 28 29 // TODO: Needed for move to system service: import com.android.internal.R; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import java.util.ArrayList; 34 import java.util.Collection; 35 import java.util.Collections; 36 import java.util.Comparator; 37 import java.util.HashSet; 38 import java.util.Iterator; 39 import java.util.List; 40 import java.util.Objects; 41 42 /** 43 * This class creates connections to place new outgoing calls or to attach to an existing incoming 44 * call. In either case, this class cycles through a set of connection services until: 45 * - a connection service returns a newly created connection in which case the call is displayed 46 * to the user 47 * - a connection service cancels the process, in which case the call is aborted 48 */ 49 @VisibleForTesting 50 public class CreateConnectionProcessor implements CreateConnectionResponse { 51 52 // Describes information required to attempt to make a phone call 53 private static class CallAttemptRecord { 54 // The PhoneAccount describing the target connection service which we will 55 // contact in order to process an attempt 56 public final PhoneAccountHandle connectionManagerPhoneAccount; 57 // The PhoneAccount which we will tell the target connection service to use 58 // for attempting to make the actual phone call 59 public final PhoneAccountHandle targetPhoneAccount; 60 CallAttemptRecord( PhoneAccountHandle connectionManagerPhoneAccount, PhoneAccountHandle targetPhoneAccount)61 public CallAttemptRecord( 62 PhoneAccountHandle connectionManagerPhoneAccount, 63 PhoneAccountHandle targetPhoneAccount) { 64 this.connectionManagerPhoneAccount = connectionManagerPhoneAccount; 65 this.targetPhoneAccount = targetPhoneAccount; 66 } 67 68 @Override toString()69 public String toString() { 70 return "CallAttemptRecord(" 71 + Objects.toString(connectionManagerPhoneAccount) + "," 72 + Objects.toString(targetPhoneAccount) + ")"; 73 } 74 75 /** 76 * Determines if this instance of {@code CallAttemptRecord} has the same underlying 77 * {@code PhoneAccountHandle}s as another instance. 78 * 79 * @param obj The other instance to compare against. 80 * @return {@code True} if the {@code CallAttemptRecord}s are equal. 81 */ 82 @Override equals(Object obj)83 public boolean equals(Object obj) { 84 if (obj instanceof CallAttemptRecord) { 85 CallAttemptRecord other = (CallAttemptRecord) obj; 86 return Objects.equals(connectionManagerPhoneAccount, 87 other.connectionManagerPhoneAccount) && 88 Objects.equals(targetPhoneAccount, other.targetPhoneAccount); 89 } 90 return false; 91 } 92 } 93 94 @VisibleForTesting 95 public interface ITelephonyManagerAdapter { getSubIdForPhoneAccount(Context context, PhoneAccount account)96 int getSubIdForPhoneAccount(Context context, PhoneAccount account); getSlotIndex(int subId)97 int getSlotIndex(int subId); 98 } 99 100 private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapter() { 101 @Override 102 public int getSubIdForPhoneAccount(Context context, PhoneAccount account) { 103 TelephonyManager manager = context.getSystemService(TelephonyManager.class); 104 if (manager == null) { 105 return SubscriptionManager.INVALID_SUBSCRIPTION_ID; 106 } 107 return manager.getSubscriptionId(account.getAccountHandle()); 108 } 109 110 @Override 111 public int getSlotIndex(int subId) { 112 return SubscriptionManager.getSlotIndex(subId); 113 } 114 }; 115 116 private final Call mCall; 117 private final ConnectionServiceRepository mRepository; 118 private List<CallAttemptRecord> mAttemptRecords; 119 private Iterator<CallAttemptRecord> mAttemptRecordIterator; 120 private CreateConnectionResponse mCallResponse; 121 private DisconnectCause mLastErrorDisconnectCause; 122 private final PhoneAccountRegistrar mPhoneAccountRegistrar; 123 private final Context mContext; 124 private CreateConnectionTimeout mTimeout; 125 private ConnectionServiceWrapper mService; 126 private int mConnectionAttempt; 127 128 @VisibleForTesting CreateConnectionProcessor( Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, PhoneAccountRegistrar phoneAccountRegistrar, Context context)129 public CreateConnectionProcessor( 130 Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, 131 PhoneAccountRegistrar phoneAccountRegistrar, Context context) { 132 Log.v(this, "CreateConnectionProcessor created for Call = %s", call); 133 mCall = call; 134 mRepository = repository; 135 mCallResponse = response; 136 mPhoneAccountRegistrar = phoneAccountRegistrar; 137 mContext = context; 138 mConnectionAttempt = 0; 139 } 140 isProcessingComplete()141 boolean isProcessingComplete() { 142 return mCallResponse == null; 143 } 144 isCallTimedOut()145 boolean isCallTimedOut() { 146 return mTimeout != null && mTimeout.isCallTimedOut(); 147 } 148 getConnectionAttempt()149 public int getConnectionAttempt() { 150 return mConnectionAttempt; 151 } 152 153 @VisibleForTesting setTelephonyManagerAdapter(ITelephonyManagerAdapter adapter)154 public void setTelephonyManagerAdapter(ITelephonyManagerAdapter adapter) { 155 mTelephonyAdapter = adapter; 156 } 157 158 @VisibleForTesting process()159 public void process() { 160 Log.v(this, "process"); 161 clearTimeout(); 162 mAttemptRecords = new ArrayList<>(); 163 if (mCall.getTargetPhoneAccount() != null) { 164 mAttemptRecords.add(new CallAttemptRecord( 165 mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount())); 166 } 167 if (!mCall.isSelfManaged()) { 168 adjustAttemptsForConnectionManager(); 169 adjustAttemptsForEmergency(mCall.getTargetPhoneAccount()); 170 } 171 mAttemptRecordIterator = mAttemptRecords.iterator(); 172 attemptNextPhoneAccount(); 173 } 174 hasMorePhoneAccounts()175 boolean hasMorePhoneAccounts() { 176 return mAttemptRecordIterator.hasNext(); 177 } 178 continueProcessingIfPossible(CreateConnectionResponse response, DisconnectCause disconnectCause)179 void continueProcessingIfPossible(CreateConnectionResponse response, 180 DisconnectCause disconnectCause) { 181 Log.v(this, "continueProcessingIfPossible"); 182 mCallResponse = response; 183 mLastErrorDisconnectCause = disconnectCause; 184 attemptNextPhoneAccount(); 185 } 186 abort()187 void abort() { 188 Log.v(this, "abort"); 189 190 // Clear the response first to prevent attemptNextConnectionService from attempting any 191 // more services. 192 CreateConnectionResponse response = mCallResponse; 193 mCallResponse = null; 194 clearTimeout(); 195 196 ConnectionServiceWrapper service = mCall.getConnectionService(); 197 if (service != null) { 198 service.abort(mCall); 199 mCall.clearConnectionService(); 200 } 201 if (response != null) { 202 response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL)); 203 } 204 } 205 attemptNextPhoneAccount()206 private void attemptNextPhoneAccount() { 207 Log.v(this, "attemptNextPhoneAccount"); 208 CallAttemptRecord attempt = null; 209 if (mAttemptRecordIterator.hasNext()) { 210 attempt = mAttemptRecordIterator.next(); 211 212 if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( 213 attempt.connectionManagerPhoneAccount)) { 214 Log.w(this, 215 "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for " 216 + "attempt: %s", attempt); 217 attemptNextPhoneAccount(); 218 return; 219 } 220 221 // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it 222 // also requires the BIND_TELECOM_CONNECTION_SERVICE permission. 223 if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) && 224 !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( 225 attempt.targetPhoneAccount)) { 226 Log.w(this, 227 "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for " 228 + "attempt: %s", attempt); 229 attemptNextPhoneAccount(); 230 return; 231 } 232 } 233 234 if (mCallResponse != null && attempt != null) { 235 Log.i(this, "Trying attempt %s", attempt); 236 PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount; 237 mService = mRepository.getService(phoneAccount.getComponentName(), 238 phoneAccount.getUserHandle()); 239 if (mService == null) { 240 Log.i(this, "Found no connection service for attempt %s", attempt); 241 attemptNextPhoneAccount(); 242 } else { 243 mConnectionAttempt++; 244 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount); 245 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount); 246 mCall.setConnectionService(mService); 247 setTimeoutIfNeeded(mService, attempt); 248 if (mCall.isIncoming()) { 249 if (mCall.isAdhocConferenceCall()) { 250 mService.createConference(mCall, CreateConnectionProcessor.this); 251 } else { 252 mService.createConnection(mCall, CreateConnectionProcessor.this); 253 } 254 } else { 255 // Start to create the connection for outgoing call after the ConnectionService 256 // of the call has gained the focus. 257 mCall.getConnectionServiceFocusManager().requestFocus( 258 mCall, 259 new CallsManager.RequestCallback(new CallsManager.PendingAction() { 260 @Override 261 public void performAction() { 262 if (mCall.isAdhocConferenceCall()) { 263 Log.d(this, "perform create conference"); 264 mService.createConference(mCall, 265 CreateConnectionProcessor.this); 266 } else { 267 Log.d(this, "perform create connection"); 268 mService.createConnection( 269 mCall, 270 CreateConnectionProcessor.this); 271 } 272 } 273 })); 274 275 } 276 } 277 } else { 278 Log.v(this, "attemptNextPhoneAccount, no more accounts, failing"); 279 DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ? 280 mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR); 281 if (mCall.isAdhocConferenceCall()) { 282 notifyConferenceCallFailure(disconnectCause); 283 } else { 284 notifyCallConnectionFailure(disconnectCause); 285 } 286 } 287 } 288 setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt)289 private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) { 290 clearTimeout(); 291 292 CreateConnectionTimeout timeout = new CreateConnectionTimeout( 293 mContext, mPhoneAccountRegistrar, service, mCall); 294 if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords), 295 attempt.connectionManagerPhoneAccount)) { 296 mTimeout = timeout; 297 timeout.registerTimeout(); 298 } 299 } 300 clearTimeout()301 private void clearTimeout() { 302 if (mTimeout != null) { 303 mTimeout.unregisterTimeout(); 304 mTimeout = null; 305 } 306 } 307 shouldSetConnectionManager()308 private boolean shouldSetConnectionManager() { 309 if (mAttemptRecords.size() == 0) { 310 return false; 311 } 312 313 if (mAttemptRecords.size() > 1) { 314 Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more " 315 + "than 1 record"); 316 return false; 317 } 318 319 PhoneAccountHandle connectionManager = 320 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall); 321 if (connectionManager == null) { 322 return false; 323 } 324 325 PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount; 326 if (Objects.equals(connectionManager, targetPhoneAccountHandle)) { 327 return false; 328 } 329 330 // Connection managers are only allowed to manage SIM subscriptions. 331 // TODO: Should this really be checking the "calling user" test for phone account? 332 PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar 333 .getPhoneAccountUnchecked(targetPhoneAccountHandle); 334 if (targetPhoneAccount == null) { 335 Log.d(this, "shouldSetConnectionManager, phone account not found"); 336 return false; 337 } 338 boolean isSimSubscription = (targetPhoneAccount.getCapabilities() & 339 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0; 340 if (!isSimSubscription) { 341 return false; 342 } 343 344 return true; 345 } 346 347 // If there exists a registered connection manager then use it. adjustAttemptsForConnectionManager()348 private void adjustAttemptsForConnectionManager() { 349 if (shouldSetConnectionManager()) { 350 CallAttemptRecord record = new CallAttemptRecord( 351 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall), 352 mAttemptRecords.get(0).targetPhoneAccount); 353 Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record); 354 mAttemptRecords.add(0, record); 355 } else { 356 Log.v(this, "setConnectionManager, not changing"); 357 } 358 } 359 360 // This function is used after previous attempts to find emergency PSTN connections 361 // do not find any SIM phone accounts with emergency capability. 362 // It attempts to add any accounts with CAPABILITY_PLACE_EMERGENCY_CALLS even if 363 // accounts are not SIM accounts. adjustAttemptsForEmergencyNoSimRequired(List<PhoneAccount> allAccounts)364 private void adjustAttemptsForEmergencyNoSimRequired(List<PhoneAccount> allAccounts) { 365 // Add all phone accounts which can place emergency calls. 366 if (mAttemptRecords.isEmpty()) { 367 for (PhoneAccount phoneAccount : allAccounts) { 368 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 369 PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); 370 Log.i(this, "Will try account %s for emergency", phoneAccountHandle); 371 mAttemptRecords.add( 372 new CallAttemptRecord(phoneAccountHandle, phoneAccountHandle)); 373 // Add only one emergency PhoneAccount to the attempt list. 374 break; 375 } 376 } 377 } 378 } 379 380 // If we are possibly attempting to call a local emergency number, ensure that the 381 // plain PSTN connection services are listed, and nothing else. adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH)382 private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) { 383 if (mCall.isEmergencyCall()) { 384 Log.i(this, "Emergency number detected"); 385 mAttemptRecords.clear(); 386 // Phone accounts in profile do not handle emergency call, use phone accounts in 387 // current user. 388 List<PhoneAccount> allAccounts = mPhoneAccountRegistrar 389 .getAllPhoneAccountsOfCurrentUser(); 390 391 if (allAccounts.isEmpty()) { 392 // If the list of phone accounts is empty at this point, it means Telephony hasn't 393 // registered any phone accounts yet. Add a fallback emergency phone account so 394 // that emergency calls can still go through. We create a new ArrayLists here just 395 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable 396 // list. 397 allAccounts = new ArrayList<PhoneAccount>(); 398 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount()); 399 } 400 401 // When testing emergency calls, we want the calls to go through to the test connection 402 // service, not the telephony ConnectionService. 403 if (mCall.isTestEmergencyCall()) { 404 allAccounts = mPhoneAccountRegistrar.filterRestrictedPhoneAccounts(allAccounts); 405 } 406 407 // Get user preferred PA if it exists. 408 PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked( 409 preferredPAH); 410 // Next, add all SIM phone accounts which can place emergency calls. 411 sortSimPhoneAccountsForEmergency(allAccounts, preferredPA); 412 // and pick the fist one that can place emergency calls. 413 for (PhoneAccount phoneAccount : allAccounts) { 414 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) 415 && phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 416 PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); 417 Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle); 418 mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle, 419 phoneAccountHandle)); 420 // Add only one emergency SIM PhoneAccount to the attempt list, telephony will 421 // perform retries if the call fails. 422 break; 423 } 424 } 425 426 // Next, add the connection manager account as a backup if it can place emergency calls. 427 PhoneAccountHandle callManagerHandle = 428 mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser(); 429 if (callManagerHandle != null) { 430 // TODO: Should this really be checking the "calling user" test for phone account? 431 PhoneAccount callManager = mPhoneAccountRegistrar 432 .getPhoneAccountUnchecked(callManagerHandle); 433 if (callManager != null && callManager.hasCapabilities( 434 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 435 CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle, 436 mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser( 437 mCall.getHandle() == null 438 ? null : mCall.getHandle().getScheme())); 439 // If the target phone account is null, we'll run into a NPE during the retry 440 // process, so skip it now if it's null. 441 if (callAttemptRecord.targetPhoneAccount != null 442 && !mAttemptRecords.contains(callAttemptRecord)) { 443 Log.i(this, "Will try Connection Manager account %s for emergency", 444 callManager); 445 mAttemptRecords.add(callAttemptRecord); 446 } 447 } 448 } 449 450 if (mAttemptRecords.isEmpty()) { 451 // Last best-effort attempt: choose any account with emergency capability even 452 // without SIM capability. 453 adjustAttemptsForEmergencyNoSimRequired(allAccounts); 454 } 455 } 456 } 457 458 /** Returns all connection services used by the call attempt records. */ getConnectionServices( List<CallAttemptRecord> records)459 private static Collection<PhoneAccountHandle> getConnectionServices( 460 List<CallAttemptRecord> records) { 461 HashSet<PhoneAccountHandle> result = new HashSet<>(); 462 for (CallAttemptRecord record : records) { 463 result.add(record.connectionManagerPhoneAccount); 464 } 465 return result; 466 } 467 468 notifyCallConnectionFailure(DisconnectCause errorDisconnectCause)469 private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) { 470 if (mCallResponse != null) { 471 clearTimeout(); 472 mCallResponse.handleCreateConnectionFailure(errorDisconnectCause); 473 mCallResponse = null; 474 mCall.clearConnectionService(); 475 } 476 } 477 notifyConferenceCallFailure(DisconnectCause errorDisconnectCause)478 private void notifyConferenceCallFailure(DisconnectCause errorDisconnectCause) { 479 if (mCallResponse != null) { 480 clearTimeout(); 481 mCallResponse.handleCreateConferenceFailure(errorDisconnectCause); 482 mCallResponse = null; 483 mCall.clearConnectionService(); 484 } 485 } 486 487 488 @Override handleCreateConnectionSuccess( CallIdMapper idMapper, ParcelableConnection connection)489 public void handleCreateConnectionSuccess( 490 CallIdMapper idMapper, 491 ParcelableConnection connection) { 492 if (mCallResponse == null) { 493 // Nobody is listening for this connection attempt any longer; ask the responsible 494 // ConnectionService to tear down any resources associated with the call 495 mService.abort(mCall); 496 } else { 497 // Success -- share the good news and remember that we are no longer interested 498 // in hearing about any more attempts 499 mCallResponse.handleCreateConnectionSuccess(idMapper, connection); 500 mCallResponse = null; 501 // If there's a timeout running then don't clear it. The timeout can be triggered 502 // after the call has successfully been created but before it has become active. 503 } 504 } 505 506 @Override handleCreateConferenceSuccess( CallIdMapper idMapper, ParcelableConference conference)507 public void handleCreateConferenceSuccess( 508 CallIdMapper idMapper, 509 ParcelableConference conference) { 510 if (mCallResponse == null) { 511 // Nobody is listening for this conference attempt any longer; ask the responsible 512 // ConnectionService to tear down any resources associated with the call 513 mService.abort(mCall); 514 } else { 515 // Success -- share the good news and remember that we are no longer interested 516 // in hearing about any more attempts 517 mCallResponse.handleCreateConferenceSuccess(idMapper, conference); 518 mCallResponse = null; 519 // If there's a timeout running then don't clear it. The timeout can be triggered 520 // after the call has successfully been created but before it has become active. 521 } 522 } 523 524 shouldFailCallIfConnectionManagerFails(DisconnectCause cause)525 private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) { 526 // Connection Manager does not exist or does not match registered Connection Manager 527 // Since Connection manager is a proxy for SIM, fall back to SIM 528 PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount(); 529 if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall( 530 mCall))) { 531 return false; 532 } 533 534 // The Call's Connection Service does not exist 535 ConnectionServiceWrapper connectionManager = mCall.getConnectionService(); 536 if (connectionManager == null) { 537 return true; 538 } 539 540 // In this case, fall back to a sim because connection manager declined 541 if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) { 542 Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the " 543 + "call, falling back to not using a connection manager"); 544 return false; 545 } 546 547 if (!connectionManager.isServiceValid("createConnection")) { 548 Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying " 549 + "create a connection, falling back to not using a connection manager"); 550 return false; 551 } 552 553 // Do not fall back from connection manager and simply fail call if the failure reason is 554 // other 555 Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " + 556 "error: " + cause.getReason() + ". Not falling back to SIM."); 557 return true; 558 } 559 560 @Override handleCreateConnectionFailure(DisconnectCause errorDisconnectCause)561 public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) { 562 // Failure of some sort; record the reasons for failure and try again if possible 563 Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause); 564 if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) { 565 notifyCallConnectionFailure(errorDisconnectCause); 566 return; 567 } 568 mLastErrorDisconnectCause = errorDisconnectCause; 569 attemptNextPhoneAccount(); 570 } 571 572 @Override handleCreateConferenceFailure(DisconnectCause errorDisconnectCause)573 public void handleCreateConferenceFailure(DisconnectCause errorDisconnectCause) { 574 // Failure of some sort; record the reasons for failure and try again if possible 575 Log.d(CreateConnectionProcessor.this, "Conference failed: (%s)", errorDisconnectCause); 576 if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) { 577 notifyConferenceCallFailure(errorDisconnectCause); 578 return; 579 } 580 mLastErrorDisconnectCause = errorDisconnectCause; 581 attemptNextPhoneAccount(); 582 } 583 sortSimPhoneAccountsForEmergency(List<PhoneAccount> accounts, PhoneAccount userPreferredAccount)584 public void sortSimPhoneAccountsForEmergency(List<PhoneAccount> accounts, 585 PhoneAccount userPreferredAccount) { 586 // Sort the accounts according to how we want to display them (ascending order). 587 accounts.sort((account1, account2) -> { 588 int retval = 0; 589 590 // SIM accounts go first 591 boolean isSim1 = account1.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); 592 boolean isSim2 = account2.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); 593 if (isSim1 ^ isSim2) { 594 return isSim1 ? -1 : 1; 595 } 596 597 // Start with the account that Telephony considers as the "emergency preferred" 598 // account, which overrides the user's choice. 599 boolean isSim1Preferred = account1.hasCapabilities( 600 PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED); 601 boolean isSim2Preferred = account2.hasCapabilities( 602 PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED); 603 // Perform XOR, we only sort if one is considered emergency preferred (should 604 // always be the case). 605 if (isSim1Preferred ^ isSim2Preferred) { 606 return isSim1Preferred ? -1 : 1; 607 } 608 609 // Return the PhoneAccount associated with a valid logical slot. 610 int subId1 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account1); 611 int subId2 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account2); 612 int slotId1 = (subId1 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) 613 ? mTelephonyAdapter.getSlotIndex(subId1) 614 : SubscriptionManager.INVALID_SIM_SLOT_INDEX; 615 int slotId2 = (subId2 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) 616 ? mTelephonyAdapter.getSlotIndex(subId2) 617 : SubscriptionManager.INVALID_SIM_SLOT_INDEX; 618 // Make sure both slots are valid, if one is not, prefer the one that is valid. 619 if ((slotId1 == SubscriptionManager.INVALID_SIM_SLOT_INDEX) ^ 620 (slotId2 == SubscriptionManager.INVALID_SIM_SLOT_INDEX)) { 621 retval = (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) ? -1 : 1; 622 } 623 if (retval != 0) { 624 return retval; 625 } 626 627 // Prefer the user's choice if all PhoneAccounts are associated with valid logical 628 // slots. 629 if (userPreferredAccount != null) { 630 if (account1.equals(userPreferredAccount)) { 631 return -1; 632 } else if (account2.equals(userPreferredAccount)) { 633 return 1; 634 } 635 } 636 637 // because of the xor above, slotId1 and slotId2 are either both invalid or valid at 638 // this point. If valid, prefer the lower slot index. 639 if (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { 640 // Assuming the slots are different, we should not have slotId1 == slotId2. 641 return (slotId1 < slotId2) ? -1 : 1; 642 } 643 644 // Then order by package 645 String pkg1 = account1.getAccountHandle().getComponentName().getPackageName(); 646 String pkg2 = account2.getAccountHandle().getComponentName().getPackageName(); 647 retval = pkg1.compareTo(pkg2); 648 if (retval != 0) { 649 return retval; 650 } 651 652 // then order by label 653 String label1 = nullToEmpty(account1.getLabel().toString()); 654 String label2 = nullToEmpty(account2.getLabel().toString()); 655 retval = label1.compareTo(label2); 656 if (retval != 0) { 657 return retval; 658 } 659 660 // then by hashcode 661 return account1.hashCode() - account2.hashCode(); 662 }); 663 } 664 nullToEmpty(String str)665 private static String nullToEmpty(String str) { 666 return str == null ? "" : str; 667 } 668 } 669