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.CallState;
     21 import android.telecom.DisconnectCause;
     22 import android.telecom.ParcelableConnection;
     23 import android.telecom.Phone;
     24 import android.telecom.PhoneAccount;
     25 import android.telecom.PhoneAccountHandle;
     26 import android.telephony.TelephonyManager;
     27 import android.telephony.PhoneStateListener;
     28 import android.telephony.ServiceState;
     29 
     30 // TODO: Needed for move to system service: import com.android.internal.R;
     31 
     32 import java.util.ArrayList;
     33 import java.util.Collection;
     34 import java.util.HashSet;
     35 import java.util.Iterator;
     36 import java.util.List;
     37 import java.util.Set;
     38 import java.util.Objects;
     39 
     40 /**
     41  * This class creates connections to place new outgoing calls or to attach to an existing incoming
     42  * call. In either case, this class cycles through a set of connection services until:
     43  *   - a connection service returns a newly created connection in which case the call is displayed
     44  *     to the user
     45  *   - a connection service cancels the process, in which case the call is aborted
     46  */
     47 final class CreateConnectionProcessor {
     48 
     49     // Describes information required to attempt to make a phone call
     50     private static class CallAttemptRecord {
     51         // The PhoneAccount describing the target connection service which we will
     52         // contact in order to process an attempt
     53         public final PhoneAccountHandle connectionManagerPhoneAccount;
     54         // The PhoneAccount which we will tell the target connection service to use
     55         // for attempting to make the actual phone call
     56         public final PhoneAccountHandle targetPhoneAccount;
     57 
     58         public CallAttemptRecord(
     59                 PhoneAccountHandle connectionManagerPhoneAccount,
     60                 PhoneAccountHandle targetPhoneAccount) {
     61             this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
     62             this.targetPhoneAccount = targetPhoneAccount;
     63         }
     64 
     65         @Override
     66         public String toString() {
     67             return "CallAttemptRecord("
     68                     + Objects.toString(connectionManagerPhoneAccount) + ","
     69                     + Objects.toString(targetPhoneAccount) + ")";
     70         }
     71 
     72         /**
     73          * Determines if this instance of {@code CallAttemptRecord} has the same underlying
     74          * {@code PhoneAccountHandle}s as another instance.
     75          *
     76          * @param obj The other instance to compare against.
     77          * @return {@code True} if the {@code CallAttemptRecord}s are equal.
     78          */
     79         @Override
     80         public boolean equals(Object obj) {
     81             if (obj instanceof CallAttemptRecord) {
     82                 CallAttemptRecord other = (CallAttemptRecord) obj;
     83                 return Objects.equals(connectionManagerPhoneAccount,
     84                         other.connectionManagerPhoneAccount) &&
     85                         Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
     86             }
     87             return false;
     88         }
     89     }
     90 
     91     private final Call mCall;
     92     private final ConnectionServiceRepository mRepository;
     93     private List<CallAttemptRecord> mAttemptRecords;
     94     private Iterator<CallAttemptRecord> mAttemptRecordIterator;
     95     private CreateConnectionResponse mResponse;
     96     private DisconnectCause mLastErrorDisconnectCause;
     97     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     98     private final Context mContext;
     99     private boolean mShouldUseConnectionManager = true;
    100     private CreateConnectionTimeout mTimeout;
    101 
    102     CreateConnectionProcessor(
    103             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
    104             PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
    105         mCall = call;
    106         mRepository = repository;
    107         mResponse = response;
    108         mPhoneAccountRegistrar = phoneAccountRegistrar;
    109         mContext = context;
    110     }
    111 
    112     boolean isProcessingComplete() {
    113         return mResponse == null;
    114     }
    115 
    116     boolean isCallTimedOut() {
    117         return mTimeout != null && mTimeout.isCallTimedOut();
    118     }
    119 
    120     void process() {
    121         Log.v(this, "process");
    122         clearTimeout();
    123         mAttemptRecords = new ArrayList<>();
    124         if (mCall.getTargetPhoneAccount() != null) {
    125             mAttemptRecords.add(new CallAttemptRecord(
    126                     mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
    127         }
    128         adjustAttemptsForConnectionManager();
    129         adjustAttemptsForEmergency();
    130         mAttemptRecordIterator = mAttemptRecords.iterator();
    131         attemptNextPhoneAccount();
    132     }
    133 
    134     boolean hasMorePhoneAccounts() {
    135         return mAttemptRecordIterator.hasNext();
    136     }
    137 
    138     void continueProcessingIfPossible(CreateConnectionResponse response,
    139             DisconnectCause disconnectCause) {
    140         Log.v(this, "continueProcessingIfPossible");
    141         mResponse = response;
    142         mLastErrorDisconnectCause = disconnectCause;
    143         attemptNextPhoneAccount();
    144     }
    145 
    146     void abort() {
    147         Log.v(this, "abort");
    148 
    149         // Clear the response first to prevent attemptNextConnectionService from attempting any
    150         // more services.
    151         CreateConnectionResponse response = mResponse;
    152         mResponse = null;
    153         clearTimeout();
    154 
    155         ConnectionServiceWrapper service = mCall.getConnectionService();
    156         if (service != null) {
    157             service.abort(mCall);
    158             mCall.clearConnectionService();
    159         }
    160         if (response != null) {
    161             response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL));
    162         }
    163     }
    164 
    165     private void attemptNextPhoneAccount() {
    166         Log.v(this, "attemptNextPhoneAccount");
    167         CallAttemptRecord attempt = null;
    168         if (mAttemptRecordIterator.hasNext()) {
    169             attempt = mAttemptRecordIterator.next();
    170 
    171             if (!mPhoneAccountRegistrar.phoneAccountHasPermission(
    172                     attempt.connectionManagerPhoneAccount)) {
    173                 Log.w(this,
    174                         "Connection mgr does not have BIND_CONNECTION_SERVICE for attempt: %s",
    175                         attempt);
    176                 attemptNextPhoneAccount();
    177                 return;
    178             }
    179 
    180             // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
    181             // also has BIND_CONNECTION_SERVICE permission.
    182             if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
    183                     !mPhoneAccountRegistrar.phoneAccountHasPermission(attempt.targetPhoneAccount)) {
    184                 Log.w(this,
    185                         "Target PhoneAccount does not have BIND_CONNECTION_SERVICE for attempt: %s",
    186                         attempt);
    187                 attemptNextPhoneAccount();
    188                 return;
    189             }
    190         }
    191 
    192         if (mResponse != null && attempt != null) {
    193             Log.i(this, "Trying attempt %s", attempt);
    194             PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
    195             ConnectionServiceWrapper service =
    196                     mRepository.getService(
    197                             phoneAccount.getComponentName(),
    198                             phoneAccount.getUserHandle());
    199             if (service == null) {
    200                 Log.i(this, "Found no connection service for attempt %s", attempt);
    201                 attemptNextPhoneAccount();
    202             } else {
    203                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
    204                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
    205                 mCall.setConnectionService(service);
    206                 setTimeoutIfNeeded(service, attempt);
    207 
    208                 Log.i(this, "Attempting to call from %s", service.getComponentName());
    209                 service.createConnection(mCall, new Response(service));
    210             }
    211         } else {
    212             Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
    213             if (mResponse != null) {
    214                 clearTimeout();
    215                 mResponse.handleCreateConnectionFailure(mLastErrorDisconnectCause != null ?
    216                         mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR));
    217                 mResponse = null;
    218                 mCall.clearConnectionService();
    219             }
    220         }
    221     }
    222 
    223     private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
    224         clearTimeout();
    225 
    226         CreateConnectionTimeout timeout = new CreateConnectionTimeout(
    227                 mContext, mPhoneAccountRegistrar, service, mCall);
    228         if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
    229                 attempt.connectionManagerPhoneAccount)) {
    230             mTimeout = timeout;
    231             timeout.registerTimeout();
    232         }
    233     }
    234 
    235     private void clearTimeout() {
    236         if (mTimeout != null) {
    237             mTimeout.unregisterTimeout();
    238             mTimeout = null;
    239         }
    240     }
    241 
    242     private boolean shouldSetConnectionManager() {
    243         if (!mShouldUseConnectionManager) {
    244             return false;
    245         }
    246 
    247         if (mAttemptRecords.size() == 0) {
    248             return false;
    249         }
    250 
    251         if (mAttemptRecords.size() > 1) {
    252             Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
    253                     + "than 1 record");
    254             return false;
    255         }
    256 
    257         PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManager();
    258         if (connectionManager == null) {
    259             return false;
    260         }
    261 
    262         PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
    263         if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
    264             return false;
    265         }
    266 
    267         // Connection managers are only allowed to manage SIM subscriptions.
    268         PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccount(
    269                 targetPhoneAccountHandle);
    270         boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
    271                 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
    272         if (!isSimSubscription) {
    273             return false;
    274         }
    275 
    276         return true;
    277     }
    278 
    279     // If there exists a registered connection manager then use it.
    280     private void adjustAttemptsForConnectionManager() {
    281         if (shouldSetConnectionManager()) {
    282             CallAttemptRecord record = new CallAttemptRecord(
    283                     mPhoneAccountRegistrar.getSimCallManager(),
    284                     mAttemptRecords.get(0).targetPhoneAccount);
    285             Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
    286             mAttemptRecords.set(0, record);
    287         } else {
    288             Log.v(this, "setConnectionManager, not changing");
    289         }
    290     }
    291 
    292     // If we are possibly attempting to call a local emergency number, ensure that the
    293     // plain PSTN connection services are listed, and nothing else.
    294     private void adjustAttemptsForEmergency()  {
    295         if (TelephonyUtil.shouldProcessAsEmergency(mContext, mCall.getHandle())) {
    296             Log.i(this, "Emergency number detected");
    297             mAttemptRecords.clear();
    298             List<PhoneAccount> allAccounts = mPhoneAccountRegistrar.getAllPhoneAccounts();
    299 
    300             if (allAccounts.isEmpty()) {
    301                 // If the list of phone accounts is empty at this point, it means Telephony hasn't
    302                 // registered any phone accounts yet. Add a fallback emergency phone account so
    303                 // that emergency calls can still go through. We create a new ArrayLists here just
    304                 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable
    305                 // list.
    306                 allAccounts = new ArrayList<PhoneAccount>();
    307                 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
    308             }
    309 
    310 
    311             // First, add SIM phone accounts which can place emergency calls.
    312             for (PhoneAccount phoneAccount : allAccounts) {
    313                 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
    314                         phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
    315                     Log.i(this, "Will try PSTN account %s for emergency",
    316                             phoneAccount.getAccountHandle());
    317                     mAttemptRecords.add(
    318                             new CallAttemptRecord(
    319                                     phoneAccount.getAccountHandle(),
    320                                     phoneAccount.getAccountHandle()));
    321                 }
    322             }
    323 
    324             // Next, add the connection manager account as a backup if it can place emergency calls.
    325             PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManager();
    326             if (mShouldUseConnectionManager && callManagerHandle != null) {
    327                 PhoneAccount callManager = mPhoneAccountRegistrar
    328                         .getPhoneAccount(callManagerHandle);
    329                 if (callManager.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
    330                     CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
    331                             mPhoneAccountRegistrar.
    332                                     getDefaultOutgoingPhoneAccount(mCall.getHandle().getScheme())
    333                     );
    334 
    335                     if (!mAttemptRecords.contains(callAttemptRecord)) {
    336                         Log.i(this, "Will try Connection Manager account %s for emergency",
    337                                 callManager);
    338                         mAttemptRecords.add(callAttemptRecord);
    339                     }
    340                 }
    341             }
    342         }
    343     }
    344 
    345     /** Returns all connection services used by the call attempt records. */
    346     private static Collection<PhoneAccountHandle> getConnectionServices(
    347             List<CallAttemptRecord> records) {
    348         HashSet<PhoneAccountHandle> result = new HashSet<>();
    349         for (CallAttemptRecord record : records) {
    350             result.add(record.connectionManagerPhoneAccount);
    351         }
    352         return result;
    353     }
    354 
    355     private class Response implements CreateConnectionResponse {
    356         private final ConnectionServiceWrapper mService;
    357 
    358         Response(ConnectionServiceWrapper service) {
    359             mService = service;
    360         }
    361 
    362         @Override
    363         public void handleCreateConnectionSuccess(
    364                 CallIdMapper idMapper,
    365                 ParcelableConnection connection) {
    366             if (mResponse == null) {
    367                 // Nobody is listening for this connection attempt any longer; ask the responsible
    368                 // ConnectionService to tear down any resources associated with the call
    369                 mService.abort(mCall);
    370             } else {
    371                 // Success -- share the good news and remember that we are no longer interested
    372                 // in hearing about any more attempts
    373                 mResponse.handleCreateConnectionSuccess(idMapper, connection);
    374                 mResponse = null;
    375                 // If there's a timeout running then don't clear it. The timeout can be triggered
    376                 // after the call has successfully been created but before it has become active.
    377             }
    378         }
    379 
    380         private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
    381             PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
    382             if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
    383                 return false;
    384             }
    385 
    386             ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
    387             if (connectionManager == null) {
    388                 return false;
    389             }
    390 
    391             if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
    392                 Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
    393                         + "call, falling back to not using a connection manager");
    394                 return true;
    395             }
    396 
    397             if (!connectionManager.isServiceValid("createConnection")) {
    398                 Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
    399                         + "create a connection, falling back to not using a connection manager");
    400                 return true;
    401             }
    402 
    403             return false;
    404         }
    405 
    406         @Override
    407         public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
    408             // Failure of some sort; record the reasons for failure and try again if possible
    409             Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
    410             mLastErrorDisconnectCause = errorDisconnectCause;
    411             if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
    412                 mShouldUseConnectionManager = false;
    413                 // Restart from the beginning.
    414                 process();
    415             } else {
    416                 attemptNextPhoneAccount();
    417             }
    418         }
    419     }
    420 }
    421