/* * Copyright 2014, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.telecom; import android.content.Context; import android.telecom.DisconnectCause; import android.telecom.Log; import android.telecom.ParcelableConnection; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; // TODO: Needed for move to system service: import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; /** * This class creates connections to place new outgoing calls or to attach to an existing incoming * call. In either case, this class cycles through a set of connection services until: * - a connection service returns a newly created connection in which case the call is displayed * to the user * - a connection service cancels the process, in which case the call is aborted */ @VisibleForTesting public class CreateConnectionProcessor implements CreateConnectionResponse { // Describes information required to attempt to make a phone call private static class CallAttemptRecord { // The PhoneAccount describing the target connection service which we will // contact in order to process an attempt public final PhoneAccountHandle connectionManagerPhoneAccount; // The PhoneAccount which we will tell the target connection service to use // for attempting to make the actual phone call public final PhoneAccountHandle targetPhoneAccount; public CallAttemptRecord( PhoneAccountHandle connectionManagerPhoneAccount, PhoneAccountHandle targetPhoneAccount) { this.connectionManagerPhoneAccount = connectionManagerPhoneAccount; this.targetPhoneAccount = targetPhoneAccount; } @Override public String toString() { return "CallAttemptRecord(" + Objects.toString(connectionManagerPhoneAccount) + "," + Objects.toString(targetPhoneAccount) + ")"; } /** * Determines if this instance of {@code CallAttemptRecord} has the same underlying * {@code PhoneAccountHandle}s as another instance. * * @param obj The other instance to compare against. * @return {@code True} if the {@code CallAttemptRecord}s are equal. */ @Override public boolean equals(Object obj) { if (obj instanceof CallAttemptRecord) { CallAttemptRecord other = (CallAttemptRecord) obj; return Objects.equals(connectionManagerPhoneAccount, other.connectionManagerPhoneAccount) && Objects.equals(targetPhoneAccount, other.targetPhoneAccount); } return false; } } @VisibleForTesting public interface ITelephonyManagerAdapter { int getSubIdForPhoneAccount(Context context, PhoneAccount account); int getSlotIndex(int subId); } private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapter() { @Override public int getSubIdForPhoneAccount(Context context, PhoneAccount account) { TelephonyManager manager = context.getSystemService(TelephonyManager.class); if (manager == null) { return SubscriptionManager.INVALID_SUBSCRIPTION_ID; } return manager.getSubIdForPhoneAccount(account); } @Override public int getSlotIndex(int subId) { return SubscriptionManager.getSlotIndex(subId); } }; private final Call mCall; private final ConnectionServiceRepository mRepository; private List mAttemptRecords; private Iterator mAttemptRecordIterator; private CreateConnectionResponse mCallResponse; private DisconnectCause mLastErrorDisconnectCause; private final PhoneAccountRegistrar mPhoneAccountRegistrar; private final Context mContext; private CreateConnectionTimeout mTimeout; private ConnectionServiceWrapper mService; private int mConnectionAttempt; @VisibleForTesting public CreateConnectionProcessor( Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, PhoneAccountRegistrar phoneAccountRegistrar, Context context) { Log.v(this, "CreateConnectionProcessor created for Call = %s", call); mCall = call; mRepository = repository; mCallResponse = response; mPhoneAccountRegistrar = phoneAccountRegistrar; mContext = context; mConnectionAttempt = 0; } boolean isProcessingComplete() { return mCallResponse == null; } boolean isCallTimedOut() { return mTimeout != null && mTimeout.isCallTimedOut(); } public int getConnectionAttempt() { return mConnectionAttempt; } @VisibleForTesting public void setTelephonyManagerAdapter(ITelephonyManagerAdapter adapter) { mTelephonyAdapter = adapter; } @VisibleForTesting public void process() { Log.v(this, "process"); clearTimeout(); mAttemptRecords = new ArrayList<>(); if (mCall.getTargetPhoneAccount() != null) { mAttemptRecords.add(new CallAttemptRecord( mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount())); } if (!mCall.isSelfManaged()) { adjustAttemptsForConnectionManager(); adjustAttemptsForEmergency(mCall.getTargetPhoneAccount()); } mAttemptRecordIterator = mAttemptRecords.iterator(); attemptNextPhoneAccount(); } boolean hasMorePhoneAccounts() { return mAttemptRecordIterator.hasNext(); } void continueProcessingIfPossible(CreateConnectionResponse response, DisconnectCause disconnectCause) { Log.v(this, "continueProcessingIfPossible"); mCallResponse = response; mLastErrorDisconnectCause = disconnectCause; attemptNextPhoneAccount(); } void abort() { Log.v(this, "abort"); // Clear the response first to prevent attemptNextConnectionService from attempting any // more services. CreateConnectionResponse response = mCallResponse; mCallResponse = null; clearTimeout(); ConnectionServiceWrapper service = mCall.getConnectionService(); if (service != null) { service.abort(mCall); mCall.clearConnectionService(); } if (response != null) { response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL)); } } private void attemptNextPhoneAccount() { Log.v(this, "attemptNextPhoneAccount"); CallAttemptRecord attempt = null; if (mAttemptRecordIterator.hasNext()) { attempt = mAttemptRecordIterator.next(); if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( attempt.connectionManagerPhoneAccount)) { Log.w(this, "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for " + "attempt: %s", attempt); attemptNextPhoneAccount(); return; } // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it // also requires the BIND_TELECOM_CONNECTION_SERVICE permission. if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) && !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( attempt.targetPhoneAccount)) { Log.w(this, "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for " + "attempt: %s", attempt); attemptNextPhoneAccount(); return; } } if (mCallResponse != null && attempt != null) { Log.i(this, "Trying attempt %s", attempt); PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount; mService = mRepository.getService(phoneAccount.getComponentName(), phoneAccount.getUserHandle()); if (mService == null) { Log.i(this, "Found no connection service for attempt %s", attempt); attemptNextPhoneAccount(); } else { mConnectionAttempt++; mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount); mCall.setTargetPhoneAccount(attempt.targetPhoneAccount); mCall.setConnectionService(mService); setTimeoutIfNeeded(mService, attempt); if (mCall.isIncoming()) { mService.createConnection(mCall, CreateConnectionProcessor.this); } else { // Start to create the connection for outgoing call after the ConnectionService // of the call has gained the focus. mCall.getConnectionServiceFocusManager().requestFocus( mCall, new CallsManager.RequestCallback(new CallsManager.PendingAction() { @Override public void performAction() { Log.d(this, "perform create connection"); mService.createConnection( mCall, CreateConnectionProcessor.this); } })); } } } else { Log.v(this, "attemptNextPhoneAccount, no more accounts, failing"); DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ? mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR); notifyCallConnectionFailure(disconnectCause); } } private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) { clearTimeout(); CreateConnectionTimeout timeout = new CreateConnectionTimeout( mContext, mPhoneAccountRegistrar, service, mCall); if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords), attempt.connectionManagerPhoneAccount)) { mTimeout = timeout; timeout.registerTimeout(); } } private void clearTimeout() { if (mTimeout != null) { mTimeout.unregisterTimeout(); mTimeout = null; } } private boolean shouldSetConnectionManager() { if (mAttemptRecords.size() == 0) { return false; } if (mAttemptRecords.size() > 1) { Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more " + "than 1 record"); return false; } PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall); if (connectionManager == null) { return false; } PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount; if (Objects.equals(connectionManager, targetPhoneAccountHandle)) { return false; } // Connection managers are only allowed to manage SIM subscriptions. // TODO: Should this really be checking the "calling user" test for phone account? PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar .getPhoneAccountUnchecked(targetPhoneAccountHandle); if (targetPhoneAccount == null) { Log.d(this, "shouldSetConnectionManager, phone account not found"); return false; } boolean isSimSubscription = (targetPhoneAccount.getCapabilities() & PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0; if (!isSimSubscription) { return false; } return true; } // If there exists a registered connection manager then use it. private void adjustAttemptsForConnectionManager() { if (shouldSetConnectionManager()) { CallAttemptRecord record = new CallAttemptRecord( mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall), mAttemptRecords.get(0).targetPhoneAccount); Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record); mAttemptRecords.add(0, record); } else { Log.v(this, "setConnectionManager, not changing"); } } // This function is used after previous attempts to find emergency PSTN connections // do not find any SIM phone accounts with emergency capability. // It attempts to add any accounts with CAPABILITY_PLACE_EMERGENCY_CALLS even if // accounts are not SIM accounts. private void adjustAttemptsForEmergencyNoSimRequired(List allAccounts) { // Add all phone accounts which can place emergency calls. if (mAttemptRecords.isEmpty()) { for (PhoneAccount phoneAccount : allAccounts) { if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); Log.i(this, "Will try account %s for emergency", phoneAccountHandle); mAttemptRecords.add( new CallAttemptRecord(phoneAccountHandle, phoneAccountHandle)); // Add only one emergency PhoneAccount to the attempt list. break; } } } } // If we are possibly attempting to call a local emergency number, ensure that the // plain PSTN connection services are listed, and nothing else. private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) { if (mCall.isEmergencyCall()) { Log.i(this, "Emergency number detected"); mAttemptRecords.clear(); // Phone accounts in profile do not handle emergency call, use phone accounts in // current user. List allAccounts = mPhoneAccountRegistrar .getAllPhoneAccountsOfCurrentUser(); if (allAccounts.isEmpty()) { // If the list of phone accounts is empty at this point, it means Telephony hasn't // registered any phone accounts yet. Add a fallback emergency phone account so // that emergency calls can still go through. We create a new ArrayLists here just // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable // list. allAccounts = new ArrayList(); allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount()); } // Get user preferred PA if it exists. PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked( preferredPAH); // Next, add all SIM phone accounts which can place emergency calls. sortSimPhoneAccountsForEmergency(allAccounts, preferredPA); // and pick the fist one that can place emergency calls. for (PhoneAccount phoneAccount : allAccounts) { if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) && phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle); mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle, phoneAccountHandle)); // Add only one emergency SIM PhoneAccount to the attempt list, telephony will // perform retries if the call fails. break; } } // Next, add the connection manager account as a backup if it can place emergency calls. PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser(); if (callManagerHandle != null) { // TODO: Should this really be checking the "calling user" test for phone account? PhoneAccount callManager = mPhoneAccountRegistrar .getPhoneAccountUnchecked(callManagerHandle); if (callManager != null && callManager.hasCapabilities( PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle, mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser( mCall.getHandle() == null ? null : mCall.getHandle().getScheme())); if (!mAttemptRecords.contains(callAttemptRecord)) { Log.i(this, "Will try Connection Manager account %s for emergency", callManager); mAttemptRecords.add(callAttemptRecord); } } } if (mAttemptRecords.isEmpty()) { // Last best-effort attempt: choose any account with emergency capability even // without SIM capability. adjustAttemptsForEmergencyNoSimRequired(allAccounts); } } } /** Returns all connection services used by the call attempt records. */ private static Collection getConnectionServices( List records) { HashSet result = new HashSet<>(); for (CallAttemptRecord record : records) { result.add(record.connectionManagerPhoneAccount); } return result; } private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) { if (mCallResponse != null) { clearTimeout(); mCallResponse.handleCreateConnectionFailure(errorDisconnectCause); mCallResponse = null; mCall.clearConnectionService(); } } @Override public void handleCreateConnectionSuccess( CallIdMapper idMapper, ParcelableConnection connection) { if (mCallResponse == null) { // Nobody is listening for this connection attempt any longer; ask the responsible // ConnectionService to tear down any resources associated with the call mService.abort(mCall); } else { // Success -- share the good news and remember that we are no longer interested // in hearing about any more attempts mCallResponse.handleCreateConnectionSuccess(idMapper, connection); mCallResponse = null; // If there's a timeout running then don't clear it. The timeout can be triggered // after the call has successfully been created but before it has become active. } } private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) { // Connection Manager does not exist or does not match registered Connection Manager // Since Connection manager is a proxy for SIM, fall back to SIM PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount(); if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall( mCall))) { return false; } // The Call's Connection Service does not exist ConnectionServiceWrapper connectionManager = mCall.getConnectionService(); if (connectionManager == null) { return true; } // In this case, fall back to a sim because connection manager declined if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) { Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the " + "call, falling back to not using a connection manager"); return false; } if (!connectionManager.isServiceValid("createConnection")) { Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying " + "create a connection, falling back to not using a connection manager"); return false; } // Do not fall back from connection manager and simply fail call if the failure reason is // other Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " + "error: " + cause.getReason() + ". Not falling back to SIM."); return true; } @Override public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) { // Failure of some sort; record the reasons for failure and try again if possible Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause); if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) { notifyCallConnectionFailure(errorDisconnectCause); return; } mLastErrorDisconnectCause = errorDisconnectCause; attemptNextPhoneAccount(); } public void sortSimPhoneAccountsForEmergency(List accounts, PhoneAccount userPreferredAccount) { // Sort the accounts according to how we want to display them (ascending order). accounts.sort((account1, account2) -> { int retval = 0; // SIM accounts go first boolean isSim1 = account1.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); boolean isSim2 = account2.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); if (isSim1 ^ isSim2) { return isSim1 ? -1 : 1; } // Start with the account that Telephony considers as the "emergency preferred" // account, which overrides the user's choice. boolean isSim1Preferred = account1.hasCapabilities( PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED); boolean isSim2Preferred = account2.hasCapabilities( PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED); // Perform XOR, we only sort if one is considered emergency preferred (should // always be the case). if (isSim1Preferred ^ isSim2Preferred) { return isSim1Preferred ? -1 : 1; } // Return the PhoneAccount associated with a valid logical slot. int subId1 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account1); int subId2 = mTelephonyAdapter.getSubIdForPhoneAccount(mContext, account2); int slotId1 = (subId1 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) ? mTelephonyAdapter.getSlotIndex(subId1) : SubscriptionManager.INVALID_SIM_SLOT_INDEX; int slotId2 = (subId2 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) ? mTelephonyAdapter.getSlotIndex(subId2) : SubscriptionManager.INVALID_SIM_SLOT_INDEX; // Make sure both slots are valid, if one is not, prefer the one that is valid. if ((slotId1 == SubscriptionManager.INVALID_SIM_SLOT_INDEX) ^ (slotId2 == SubscriptionManager.INVALID_SIM_SLOT_INDEX)) { retval = (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) ? -1 : 1; } if (retval != 0) { return retval; } // Prefer the user's choice if all PhoneAccounts are associated with valid logical // slots. if (userPreferredAccount != null) { if (account1.equals(userPreferredAccount)) { return -1; } else if (account2.equals(userPreferredAccount)) { return 1; } } // because of the xor above, slotId1 and slotId2 are either both invalid or valid at // this point. If valid, prefer the lower slot index. if (slotId1 != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { // Assuming the slots are different, we should not have slotId1 == slotId2. return (slotId1 < slotId2) ? -1 : 1; } // Then order by package String pkg1 = account1.getAccountHandle().getComponentName().getPackageName(); String pkg2 = account2.getAccountHandle().getComponentName().getPackageName(); retval = pkg1.compareTo(pkg2); if (retval != 0) { return retval; } // then order by label String label1 = nullToEmpty(account1.getLabel().toString()); String label2 = nullToEmpty(account2.getLabel().toString()); retval = label1.compareTo(label2); if (retval != 0) { return retval; } // then by hashcode return account1.hashCode() - account2.hashCode(); }); } private static String nullToEmpty(String str) { return str == null ? "" : str; } }