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