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                 allAccounts = mPhoneAccountRegistrar.filterRestrictedPhoneAccounts(allAccounts);
407             }
408 
409             // Get user preferred PA if it exists.
410             PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
411                     preferredPAH);
412             // Next, add all SIM phone accounts which can place emergency calls.
413             sortSimPhoneAccountsForEmergency(allAccounts, preferredPA);
414             // and pick the fist one that can place emergency calls.
415             for (PhoneAccount phoneAccount : allAccounts) {
416                 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
417                         && phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
418                     PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
419                     Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle);
420                     mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
421                             phoneAccountHandle));
422                     // Add only one emergency SIM PhoneAccount to the attempt list, telephony will
423                     // perform retries if the call fails.
424                     break;
425                 }
426             }
427 
428             // Next, add the connection manager account as a backup if it can place emergency calls.
429             PhoneAccountHandle callManagerHandle =
430                     mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
431             if (callManagerHandle != null) {
432                 // TODO: Should this really be checking the "calling user" test for phone account?
433                 PhoneAccount callManager = mPhoneAccountRegistrar
434                         .getPhoneAccountUnchecked(callManagerHandle);
435                 if (callManager != null && callManager.hasCapabilities(
436                         PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
437                     CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
438                             mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
439                                     mCall.getHandle() == null
440                                             ? null : mCall.getHandle().getScheme()));
441                     if (!mAttemptRecords.contains(callAttemptRecord)) {
442                         Log.i(this, "Will try Connection Manager account %s for emergency",
443                                 callManager);
444                         mAttemptRecords.add(callAttemptRecord);
445                     }
446                 }
447             }
448 
449             if (mAttemptRecords.isEmpty()) {
450                 // Last best-effort attempt: choose any account with emergency capability even
451                 // without SIM capability.
452                 adjustAttemptsForEmergencyNoSimRequired(allAccounts);
453             }
454         }
455     }
456 
457     /** Returns all connection services used by the call attempt records. */
getConnectionServices( List<CallAttemptRecord> records)458     private static Collection<PhoneAccountHandle> getConnectionServices(
459             List<CallAttemptRecord> records) {
460         HashSet<PhoneAccountHandle> result = new HashSet<>();
461         for (CallAttemptRecord record : records) {
462             result.add(record.connectionManagerPhoneAccount);
463         }
464         return result;
465     }
466 
467 
notifyCallConnectionFailure(DisconnectCause errorDisconnectCause)468     private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) {
469         if (mCallResponse != null) {
470             clearTimeout();
471             mCallResponse.handleCreateConnectionFailure(errorDisconnectCause);
472             mCallResponse = null;
473             mCall.clearConnectionService();
474         }
475     }
476 
notifyConferenceCallFailure(DisconnectCause errorDisconnectCause)477     private void notifyConferenceCallFailure(DisconnectCause errorDisconnectCause) {
478         if (mCallResponse != null) {
479             clearTimeout();
480             mCallResponse.handleCreateConferenceFailure(errorDisconnectCause);
481             mCallResponse = null;
482             mCall.clearConnectionService();
483         }
484     }
485 
486 
487     @Override
handleCreateConnectionSuccess( CallIdMapper idMapper, ParcelableConnection connection)488     public void handleCreateConnectionSuccess(
489             CallIdMapper idMapper,
490             ParcelableConnection connection) {
491         if (mCallResponse == null) {
492             // Nobody is listening for this connection attempt any longer; ask the responsible
493             // ConnectionService to tear down any resources associated with the call
494             mService.abort(mCall);
495         } else {
496             // Success -- share the good news and remember that we are no longer interested
497             // in hearing about any more attempts
498             mCallResponse.handleCreateConnectionSuccess(idMapper, connection);
499             mCallResponse = null;
500             // If there's a timeout running then don't clear it. The timeout can be triggered
501             // after the call has successfully been created but before it has become active.
502         }
503     }
504 
505     @Override
handleCreateConferenceSuccess( CallIdMapper idMapper, ParcelableConference conference)506     public void handleCreateConferenceSuccess(
507             CallIdMapper idMapper,
508             ParcelableConference conference) {
509         if (mCallResponse == null) {
510             // Nobody is listening for this conference attempt any longer; ask the responsible
511             // ConnectionService to tear down any resources associated with the call
512             mService.abort(mCall);
513         } else {
514             // Success -- share the good news and remember that we are no longer interested
515             // in hearing about any more attempts
516             mCallResponse.handleCreateConferenceSuccess(idMapper, conference);
517             mCallResponse = null;
518             // If there's a timeout running then don't clear it. The timeout can be triggered
519             // after the call has successfully been created but before it has become active.
520         }
521     }
522 
523 
shouldFailCallIfConnectionManagerFails(DisconnectCause cause)524     private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) {
525         // Connection Manager does not exist or does not match registered Connection Manager
526         // Since Connection manager is a proxy for SIM, fall back to SIM
527         PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
528         if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall(
529                 mCall))) {
530             return false;
531         }
532 
533         // The Call's Connection Service does not exist
534         ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
535         if (connectionManager == null) {
536             return true;
537         }
538 
539         // In this case, fall back to a sim because connection manager declined
540         if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
541             Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
542                     + "call, falling back to not using a connection manager");
543             return false;
544         }
545 
546         if (!connectionManager.isServiceValid("createConnection")) {
547             Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
548                     + "create a connection, falling back to not using a connection manager");
549             return false;
550         }
551 
552         // Do not fall back from connection manager and simply fail call if the failure reason is
553         // other
554         Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " +
555                 "error: " + cause.getReason() + ". Not falling back to SIM.");
556         return true;
557     }
558 
559     @Override
handleCreateConnectionFailure(DisconnectCause errorDisconnectCause)560     public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
561         // Failure of some sort; record the reasons for failure and try again if possible
562         Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
563         if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) {
564             notifyCallConnectionFailure(errorDisconnectCause);
565             return;
566         }
567         mLastErrorDisconnectCause = errorDisconnectCause;
568         attemptNextPhoneAccount();
569     }
570 
571     @Override
handleCreateConferenceFailure(DisconnectCause errorDisconnectCause)572     public void handleCreateConferenceFailure(DisconnectCause errorDisconnectCause) {
573         // Failure of some sort; record the reasons for failure and try again if possible
574         Log.d(CreateConnectionProcessor.this, "Conference failed: (%s)", errorDisconnectCause);
575         if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) {
576             notifyConferenceCallFailure(errorDisconnectCause);
577             return;
578         }
579         mLastErrorDisconnectCause = errorDisconnectCause;
580         attemptNextPhoneAccount();
581     }
582 
sortSimPhoneAccountsForEmergency(List<PhoneAccount> accounts, PhoneAccount userPreferredAccount)583     public void sortSimPhoneAccountsForEmergency(List<PhoneAccount> accounts,
584             PhoneAccount userPreferredAccount) {
585         // Sort the accounts according to how we want to display them (ascending order).
586         accounts.sort((account1, account2) -> {
587             int retval = 0;
588 
589             // SIM accounts go first
590             boolean isSim1 = account1.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
591             boolean isSim2 = account2.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
592             if (isSim1 ^ isSim2) {
593                 return isSim1 ? -1 : 1;
594             }
595 
596             // Start with the account that Telephony considers as the "emergency preferred"
597             // account, which overrides the user's choice.
598             boolean isSim1Preferred = account1.hasCapabilities(
599                     PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED);
600             boolean isSim2Preferred = account2.hasCapabilities(
601                     PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED);
602             // Perform XOR, we only sort if one is considered emergency preferred (should
603             // always be the case).
604             if (isSim1Preferred ^ isSim2Preferred) {
605                 return isSim1Preferred ? -1 : 1;
606             }
607 
608             // Return the PhoneAccount associated with a valid logical slot.
609             int subId1 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account1);
610             int subId2 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account2);
611             int slotId1 = (subId1 != SubscriptionManager.INVALID_SUBSCRIPTION_ID)
612                     ? mTelephonyAdapter.getSlotIndex(subId1)
613                     : SubscriptionManager.INVALID_SIM_SLOT_INDEX;
614             int slotId2 = (subId2 != SubscriptionManager.INVALID_SUBSCRIPTION_ID)
615                     ? mTelephonyAdapter.getSlotIndex(subId2)
616                     : SubscriptionManager.INVALID_SIM_SLOT_INDEX;
617             // Make sure both slots are valid, if one is not, prefer the one that is valid.
618             if ((slotId1 == SubscriptionManager.INVALID_SIM_SLOT_INDEX) ^
619                     (slotId2 == SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
620                 retval = (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) ? -1 : 1;
621             }
622             if (retval != 0) {
623                 return retval;
624             }
625 
626             // Prefer the user's choice if all PhoneAccounts are associated with valid logical
627             // slots.
628             if (userPreferredAccount != null) {
629                 if (account1.equals(userPreferredAccount)) {
630                     return -1;
631                 } else if (account2.equals(userPreferredAccount)) {
632                     return 1;
633                 }
634             }
635 
636             // because of the xor above, slotId1 and slotId2 are either both invalid or valid at
637             // this point. If valid, prefer the lower slot index.
638             if (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
639                 // Assuming the slots are different, we should not have slotId1 == slotId2.
640                 return (slotId1 < slotId2) ? -1 : 1;
641             }
642 
643             // Then order by package
644             String pkg1 = account1.getAccountHandle().getComponentName().getPackageName();
645             String pkg2 = account2.getAccountHandle().getComponentName().getPackageName();
646             retval = pkg1.compareTo(pkg2);
647             if (retval != 0) {
648                 return retval;
649             }
650 
651             // then order by label
652             String label1 = nullToEmpty(account1.getLabel().toString());
653             String label2 = nullToEmpty(account2.getLabel().toString());
654             retval = label1.compareTo(label2);
655             if (retval != 0) {
656                 return retval;
657             }
658 
659             // then by hashcode
660             return account1.hashCode() - account2.hashCode();
661         });
662     }
663 
nullToEmpty(String str)664     private static String nullToEmpty(String str) {
665         return str == null ? "" : str;
666     }
667 }
668