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