Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright 2014, The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.telecom;
     18 
     19 import android.content.Context;
     20 import android.telecom.DisconnectCause;
     21 import android.telecom.Log;
     22 import android.telecom.ParcelableConnection;
     23 import android.telecom.PhoneAccount;
     24 import android.telecom.PhoneAccountHandle;
     25 
     26 // TODO: Needed for move to system service: import com.android.internal.R;
     27 
     28 import com.android.internal.annotations.VisibleForTesting;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Collection;
     32 import java.util.HashSet;
     33 import java.util.Iterator;
     34 import java.util.List;
     35 import java.util.Objects;
     36 
     37 /**
     38  * This class creates connections to place new outgoing calls or to attach to an existing incoming
     39  * call. In either case, this class cycles through a set of connection services until:
     40  *   - a connection service returns a newly created connection in which case the call is displayed
     41  *     to the user
     42  *   - a connection service cancels the process, in which case the call is aborted
     43  */
     44 @VisibleForTesting
     45 public class CreateConnectionProcessor implements CreateConnectionResponse {
     46 
     47     // Describes information required to attempt to make a phone call
     48     private static class CallAttemptRecord {
     49         // The PhoneAccount describing the target connection service which we will
     50         // contact in order to process an attempt
     51         public final PhoneAccountHandle connectionManagerPhoneAccount;
     52         // The PhoneAccount which we will tell the target connection service to use
     53         // for attempting to make the actual phone call
     54         public final PhoneAccountHandle targetPhoneAccount;
     55 
     56         public CallAttemptRecord(
     57                 PhoneAccountHandle connectionManagerPhoneAccount,
     58                 PhoneAccountHandle targetPhoneAccount) {
     59             this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
     60             this.targetPhoneAccount = targetPhoneAccount;
     61         }
     62 
     63         @Override
     64         public String toString() {
     65             return "CallAttemptRecord("
     66                     + Objects.toString(connectionManagerPhoneAccount) + ","
     67                     + Objects.toString(targetPhoneAccount) + ")";
     68         }
     69 
     70         /**
     71          * Determines if this instance of {@code CallAttemptRecord} has the same underlying
     72          * {@code PhoneAccountHandle}s as another instance.
     73          *
     74          * @param obj The other instance to compare against.
     75          * @return {@code True} if the {@code CallAttemptRecord}s are equal.
     76          */
     77         @Override
     78         public boolean equals(Object obj) {
     79             if (obj instanceof CallAttemptRecord) {
     80                 CallAttemptRecord other = (CallAttemptRecord) obj;
     81                 return Objects.equals(connectionManagerPhoneAccount,
     82                         other.connectionManagerPhoneAccount) &&
     83                         Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
     84             }
     85             return false;
     86         }
     87     }
     88 
     89     private final Call mCall;
     90     private final ConnectionServiceRepository mRepository;
     91     private List<CallAttemptRecord> mAttemptRecords;
     92     private Iterator<CallAttemptRecord> mAttemptRecordIterator;
     93     private CreateConnectionResponse mCallResponse;
     94     private DisconnectCause mLastErrorDisconnectCause;
     95     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     96     private final Context mContext;
     97     private CreateConnectionTimeout mTimeout;
     98     private ConnectionServiceWrapper mService;
     99     private int mConnectionAttempt;
    100 
    101     @VisibleForTesting
    102     public CreateConnectionProcessor(
    103             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
    104             PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
    105         Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
    106         mCall = call;
    107         mRepository = repository;
    108         mCallResponse = response;
    109         mPhoneAccountRegistrar = phoneAccountRegistrar;
    110         mContext = context;
    111         mConnectionAttempt = 0;
    112     }
    113 
    114     boolean isProcessingComplete() {
    115         return mCallResponse == null;
    116     }
    117 
    118     boolean isCallTimedOut() {
    119         return mTimeout != null && mTimeout.isCallTimedOut();
    120     }
    121 
    122     public int getConnectionAttempt() {
    123         return mConnectionAttempt;
    124     }
    125 
    126     @VisibleForTesting
    127     public void process() {
    128         Log.v(this, "process");
    129         clearTimeout();
    130         mAttemptRecords = new ArrayList<>();
    131         if (mCall.getTargetPhoneAccount() != null) {
    132             mAttemptRecords.add(new CallAttemptRecord(
    133                     mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
    134         }
    135         if (!mCall.isSelfManaged()) {
    136             adjustAttemptsForConnectionManager();
    137             adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
    138         }
    139         mAttemptRecordIterator = mAttemptRecords.iterator();
    140         attemptNextPhoneAccount();
    141     }
    142 
    143     boolean hasMorePhoneAccounts() {
    144         return mAttemptRecordIterator.hasNext();
    145     }
    146 
    147     void continueProcessingIfPossible(CreateConnectionResponse response,
    148             DisconnectCause disconnectCause) {
    149         Log.v(this, "continueProcessingIfPossible");
    150         mCallResponse = response;
    151         mLastErrorDisconnectCause = disconnectCause;
    152         attemptNextPhoneAccount();
    153     }
    154 
    155     void abort() {
    156         Log.v(this, "abort");
    157 
    158         // Clear the response first to prevent attemptNextConnectionService from attempting any
    159         // more services.
    160         CreateConnectionResponse response = mCallResponse;
    161         mCallResponse = null;
    162         clearTimeout();
    163 
    164         ConnectionServiceWrapper service = mCall.getConnectionService();
    165         if (service != null) {
    166             service.abort(mCall);
    167             mCall.clearConnectionService();
    168         }
    169         if (response != null) {
    170             response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL));
    171         }
    172     }
    173 
    174     private void attemptNextPhoneAccount() {
    175         Log.v(this, "attemptNextPhoneAccount");
    176         CallAttemptRecord attempt = null;
    177         if (mAttemptRecordIterator.hasNext()) {
    178             attempt = mAttemptRecordIterator.next();
    179 
    180             if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
    181                     attempt.connectionManagerPhoneAccount)) {
    182                 Log.w(this,
    183                         "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for "
    184                                 + "attempt: %s", attempt);
    185                 attemptNextPhoneAccount();
    186                 return;
    187             }
    188 
    189             // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
    190             // also requires the BIND_TELECOM_CONNECTION_SERVICE permission.
    191             if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
    192                     !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission(
    193                             attempt.targetPhoneAccount)) {
    194                 Log.w(this,
    195                         "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for "
    196                                 + "attempt: %s", attempt);
    197                 attemptNextPhoneAccount();
    198                 return;
    199             }
    200         }
    201 
    202         if (mCallResponse != null && attempt != null) {
    203             Log.i(this, "Trying attempt %s", attempt);
    204             PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
    205             mService = mRepository.getService(phoneAccount.getComponentName(),
    206                     phoneAccount.getUserHandle());
    207             if (mService == null) {
    208                 Log.i(this, "Found no connection service for attempt %s", attempt);
    209                 attemptNextPhoneAccount();
    210             } else {
    211                 mConnectionAttempt++;
    212                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
    213                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
    214                 mCall.setConnectionService(mService);
    215                 setTimeoutIfNeeded(mService, attempt);
    216                 if (mCall.isIncoming()) {
    217                     mService.createConnection(mCall, CreateConnectionProcessor.this);
    218                 } else {
    219                     // Start to create the connection for outgoing call after the ConnectionService
    220                     // of the call has gained the focus.
    221                     mCall.getConnectionServiceFocusManager().requestFocus(
    222                             mCall,
    223                             new CallsManager.RequestCallback(new CallsManager.PendingAction() {
    224                                 @Override
    225                                 public void performAction() {
    226                                     Log.d(this, "perform create connection");
    227                                     mService.createConnection(
    228                                             mCall,
    229                                             CreateConnectionProcessor.this);
    230                                 }
    231                             }));
    232 
    233                 }
    234             }
    235         } else {
    236             Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
    237             DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ?
    238                     mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR);
    239             notifyCallConnectionFailure(disconnectCause);
    240         }
    241     }
    242 
    243     private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
    244         clearTimeout();
    245 
    246         CreateConnectionTimeout timeout = new CreateConnectionTimeout(
    247                 mContext, mPhoneAccountRegistrar, service, mCall);
    248         if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
    249                 attempt.connectionManagerPhoneAccount)) {
    250             mTimeout = timeout;
    251             timeout.registerTimeout();
    252         }
    253     }
    254 
    255     private void clearTimeout() {
    256         if (mTimeout != null) {
    257             mTimeout.unregisterTimeout();
    258             mTimeout = null;
    259         }
    260     }
    261 
    262     private boolean shouldSetConnectionManager() {
    263         if (mAttemptRecords.size() == 0) {
    264             return false;
    265         }
    266 
    267         if (mAttemptRecords.size() > 1) {
    268             Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
    269                     + "than 1 record");
    270             return false;
    271         }
    272 
    273         PhoneAccountHandle connectionManager =
    274                 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
    275         if (connectionManager == null) {
    276             return false;
    277         }
    278 
    279         PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
    280         if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
    281             return false;
    282         }
    283 
    284         // Connection managers are only allowed to manage SIM subscriptions.
    285         // TODO: Should this really be checking the "calling user" test for phone account?
    286         PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar
    287                 .getPhoneAccountUnchecked(targetPhoneAccountHandle);
    288         if (targetPhoneAccount == null) {
    289             Log.d(this, "shouldSetConnectionManager, phone account not found");
    290             return false;
    291         }
    292         boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
    293                 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
    294         if (!isSimSubscription) {
    295             return false;
    296         }
    297 
    298         return true;
    299     }
    300 
    301     // If there exists a registered connection manager then use it.
    302     private void adjustAttemptsForConnectionManager() {
    303         if (shouldSetConnectionManager()) {
    304             CallAttemptRecord record = new CallAttemptRecord(
    305                     mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall),
    306                     mAttemptRecords.get(0).targetPhoneAccount);
    307             Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
    308             mAttemptRecords.add(0, record);
    309         } else {
    310             Log.v(this, "setConnectionManager, not changing");
    311         }
    312     }
    313 
    314     // If we are possibly attempting to call a local emergency number, ensure that the
    315     // plain PSTN connection services are listed, and nothing else.
    316     private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) {
    317         if (mCall.isEmergencyCall()) {
    318             Log.i(this, "Emergency number detected");
    319             mAttemptRecords.clear();
    320             // Phone accounts in profile do not handle emergency call, use phone accounts in
    321             // current user.
    322             List<PhoneAccount> allAccounts = mPhoneAccountRegistrar
    323                     .getAllPhoneAccountsOfCurrentUser();
    324 
    325             if (allAccounts.isEmpty()) {
    326                 // If the list of phone accounts is empty at this point, it means Telephony hasn't
    327                 // registered any phone accounts yet. Add a fallback emergency phone account so
    328                 // that emergency calls can still go through. We create a new ArrayLists here just
    329                 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
    330                 // list.
    331                 allAccounts = new ArrayList<PhoneAccount>();
    332                 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
    333             }
    334 
    335             // First, possibly add the SIM phone account that the user prefers
    336             PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
    337                     preferredPAH);
    338             if (preferredPA != null &&
    339                     preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
    340                     preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
    341                 Log.i(this, "Will try PSTN account %s for emergency",
    342                         preferredPA.getAccountHandle());
    343                 mAttemptRecords.add(new CallAttemptRecord(preferredPAH, preferredPAH));
    344             }
    345 
    346             // Next, add all SIM phone accounts which can place emergency calls.
    347             TelephonyUtil.sortSimPhoneAccounts(mContext, allAccounts);
    348 
    349             // If preferredPA already has an emergency PhoneAccount, do not add others since the
    350             // emergency call be redialed in Telephony.
    351             if (mAttemptRecords.isEmpty()) {
    352                 for (PhoneAccount phoneAccount : allAccounts) {
    353                     if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
    354                             && phoneAccount.hasCapabilities(
    355                             PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
    356                         PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
    357                         Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle);
    358                         mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
    359                                 phoneAccountHandle));
    360                         // Add only one emergency SIM PhoneAccount to the attempt list.
    361                         break;
    362                     }
    363                 }
    364             }
    365 
    366             // Next, add the connection manager account as a backup if it can place emergency calls.
    367             PhoneAccountHandle callManagerHandle =
    368                     mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
    369             if (callManagerHandle != null) {
    370                 // TODO: Should this really be checking the "calling user" test for phone account?
    371                 PhoneAccount callManager = mPhoneAccountRegistrar
    372                         .getPhoneAccountUnchecked(callManagerHandle);
    373                 if (callManager != null && callManager.hasCapabilities(
    374                         PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
    375                     CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
    376                             mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
    377                                     mCall.getHandle() == null
    378                                             ? null : mCall.getHandle().getScheme()));
    379                     if (!mAttemptRecords.contains(callAttemptRecord)) {
    380                         Log.i(this, "Will try Connection Manager account %s for emergency",
    381                                 callManager);
    382                         mAttemptRecords.add(callAttemptRecord);
    383                     }
    384                 }
    385             }
    386         }
    387     }
    388 
    389     /** Returns all connection services used by the call attempt records. */
    390     private static Collection<PhoneAccountHandle> getConnectionServices(
    391             List<CallAttemptRecord> records) {
    392         HashSet<PhoneAccountHandle> result = new HashSet<>();
    393         for (CallAttemptRecord record : records) {
    394             result.add(record.connectionManagerPhoneAccount);
    395         }
    396         return result;
    397     }
    398 
    399 
    400     private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) {
    401         if (mCallResponse != null) {
    402             clearTimeout();
    403             mCallResponse.handleCreateConnectionFailure(errorDisconnectCause);
    404             mCallResponse = null;
    405             mCall.clearConnectionService();
    406         }
    407     }
    408 
    409     @Override
    410     public void handleCreateConnectionSuccess(
    411             CallIdMapper idMapper,
    412             ParcelableConnection connection) {
    413         if (mCallResponse == null) {
    414             // Nobody is listening for this connection attempt any longer; ask the responsible
    415             // ConnectionService to tear down any resources associated with the call
    416             mService.abort(mCall);
    417         } else {
    418             // Success -- share the good news and remember that we are no longer interested
    419             // in hearing about any more attempts
    420             mCallResponse.handleCreateConnectionSuccess(idMapper, connection);
    421             mCallResponse = null;
    422             // If there's a timeout running then don't clear it. The timeout can be triggered
    423             // after the call has successfully been created but before it has become active.
    424         }
    425     }
    426 
    427     private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) {
    428         // Connection Manager does not exist or does not match registered Connection Manager
    429         // Since Connection manager is a proxy for SIM, fall back to SIM
    430         PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
    431         if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall(
    432                 mCall))) {
    433             return false;
    434         }
    435 
    436         // The Call's Connection Service does not exist
    437         ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
    438         if (connectionManager == null) {
    439             return true;
    440         }
    441 
    442         // In this case, fall back to a sim because connection manager declined
    443         if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
    444             Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
    445                     + "call, falling back to not using a connection manager");
    446             return false;
    447         }
    448 
    449         if (!connectionManager.isServiceValid("createConnection")) {
    450             Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
    451                     + "create a connection, falling back to not using a connection manager");
    452             return false;
    453         }
    454 
    455         // Do not fall back from connection manager and simply fail call if the failure reason is
    456         // other
    457         Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " +
    458                 "error: " + cause.getReason() + ". Not falling back to SIM.");
    459         return true;
    460     }
    461 
    462     @Override
    463     public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
    464         // Failure of some sort; record the reasons for failure and try again if possible
    465         Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
    466         if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) {
    467             notifyCallConnectionFailure(errorDisconnectCause);
    468             return;
    469         }
    470         mLastErrorDisconnectCause = errorDisconnectCause;
    471         attemptNextPhoneAccount();
    472     }
    473 }
    474