• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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