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