• 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.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