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.os.UserHandle; 21 import android.telecom.DisconnectCause; 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 adjustAttemptsForConnectionManager(); 136 adjustAttemptsForEmergency(mCall.getTargetPhoneAccount()); 137 mAttemptRecordIterator = mAttemptRecords.iterator(); 138 attemptNextPhoneAccount(); 139 } 140 141 boolean hasMorePhoneAccounts() { 142 return mAttemptRecordIterator.hasNext(); 143 } 144 145 void continueProcessingIfPossible(CreateConnectionResponse response, 146 DisconnectCause disconnectCause) { 147 Log.v(this, "continueProcessingIfPossible"); 148 mCallResponse = response; 149 mLastErrorDisconnectCause = disconnectCause; 150 attemptNextPhoneAccount(); 151 } 152 153 void abort() { 154 Log.v(this, "abort"); 155 156 // Clear the response first to prevent attemptNextConnectionService from attempting any 157 // more services. 158 CreateConnectionResponse response = mCallResponse; 159 mCallResponse = null; 160 clearTimeout(); 161 162 ConnectionServiceWrapper service = mCall.getConnectionService(); 163 if (service != null) { 164 service.abort(mCall); 165 mCall.clearConnectionService(); 166 } 167 if (response != null) { 168 response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.LOCAL)); 169 } 170 } 171 172 private void attemptNextPhoneAccount() { 173 Log.v(this, "attemptNextPhoneAccount"); 174 CallAttemptRecord attempt = null; 175 if (mAttemptRecordIterator.hasNext()) { 176 attempt = mAttemptRecordIterator.next(); 177 178 if (!mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( 179 attempt.connectionManagerPhoneAccount)) { 180 Log.w(this, 181 "Connection mgr does not have BIND_TELECOM_CONNECTION_SERVICE for " 182 + "attempt: %s", attempt); 183 attemptNextPhoneAccount(); 184 return; 185 } 186 187 // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it 188 // also requires the BIND_TELECOM_CONNECTION_SERVICE permission. 189 if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) && 190 !mPhoneAccountRegistrar.phoneAccountRequiresBindPermission( 191 attempt.targetPhoneAccount)) { 192 Log.w(this, 193 "Target PhoneAccount does not have BIND_TELECOM_CONNECTION_SERVICE for " 194 + "attempt: %s", attempt); 195 attemptNextPhoneAccount(); 196 return; 197 } 198 } 199 200 if (mCallResponse != null && attempt != null) { 201 Log.i(this, "Trying attempt %s", attempt); 202 PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount; 203 mService = mRepository.getService(phoneAccount.getComponentName(), 204 phoneAccount.getUserHandle()); 205 if (mService == null) { 206 Log.i(this, "Found no connection service for attempt %s", attempt); 207 attemptNextPhoneAccount(); 208 } else { 209 mConnectionAttempt++; 210 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount); 211 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount); 212 mCall.setConnectionService(mService); 213 setTimeoutIfNeeded(mService, attempt); 214 215 mService.createConnection(mCall, this); 216 } 217 } else { 218 Log.v(this, "attemptNextPhoneAccount, no more accounts, failing"); 219 DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ? 220 mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR); 221 notifyCallConnectionFailure(disconnectCause); 222 } 223 } 224 225 private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) { 226 clearTimeout(); 227 228 CreateConnectionTimeout timeout = new CreateConnectionTimeout( 229 mContext, mPhoneAccountRegistrar, service, mCall); 230 if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords), 231 attempt.connectionManagerPhoneAccount)) { 232 mTimeout = timeout; 233 timeout.registerTimeout(); 234 } 235 } 236 237 private void clearTimeout() { 238 if (mTimeout != null) { 239 mTimeout.unregisterTimeout(); 240 mTimeout = null; 241 } 242 } 243 244 private boolean shouldSetConnectionManager() { 245 if (mAttemptRecords.size() == 0) { 246 return false; 247 } 248 249 if (mAttemptRecords.size() > 1) { 250 Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more " 251 + "than 1 record"); 252 return false; 253 } 254 255 PhoneAccountHandle connectionManager = 256 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall); 257 if (connectionManager == null) { 258 return false; 259 } 260 261 PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount; 262 if (Objects.equals(connectionManager, targetPhoneAccountHandle)) { 263 return false; 264 } 265 266 // Connection managers are only allowed to manage SIM subscriptions. 267 // TODO: Should this really be checking the "calling user" test for phone account? 268 PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar 269 .getPhoneAccountUnchecked(targetPhoneAccountHandle); 270 if (targetPhoneAccount == null) { 271 Log.d(this, "shouldSetConnectionManager, phone account not found"); 272 return false; 273 } 274 boolean isSimSubscription = (targetPhoneAccount.getCapabilities() & 275 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0; 276 if (!isSimSubscription) { 277 return false; 278 } 279 280 return true; 281 } 282 283 // If there exists a registered connection manager then use it. 284 private void adjustAttemptsForConnectionManager() { 285 if (shouldSetConnectionManager()) { 286 CallAttemptRecord record = new CallAttemptRecord( 287 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall), 288 mAttemptRecords.get(0).targetPhoneAccount); 289 Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record); 290 mAttemptRecords.add(0, record); 291 } else { 292 Log.v(this, "setConnectionManager, not changing"); 293 } 294 } 295 296 // If we are possibly attempting to call a local emergency number, ensure that the 297 // plain PSTN connection services are listed, and nothing else. 298 private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) { 299 if (mCall.isEmergencyCall()) { 300 Log.i(this, "Emergency number detected"); 301 mAttemptRecords.clear(); 302 // Phone accounts in profile do not handle emergency call, use phone accounts in 303 // current user. 304 List<PhoneAccount> allAccounts = mPhoneAccountRegistrar 305 .getAllPhoneAccountsOfCurrentUser(); 306 307 if (allAccounts.isEmpty()) { 308 // If the list of phone accounts is empty at this point, it means Telephony hasn't 309 // registered any phone accounts yet. Add a fallback emergency phone account so 310 // that emergency calls can still go through. We create a new ArrayLists here just 311 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable 312 // list. 313 allAccounts = new ArrayList<PhoneAccount>(); 314 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount()); 315 } 316 317 // First, possibly add the SIM phone account that the user prefers 318 PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked( 319 preferredPAH); 320 if (preferredPA != null && 321 preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) && 322 preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 323 Log.i(this, "Will try PSTN account %s for emergency", 324 preferredPA.getAccountHandle()); 325 mAttemptRecords.add(new CallAttemptRecord(preferredPAH, preferredPAH)); 326 } 327 328 // Next, add all SIM phone accounts which can place emergency calls. 329 TelephonyUtil.sortSimPhoneAccounts(mContext, allAccounts); 330 for (PhoneAccount phoneAccount : allAccounts) { 331 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) && 332 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 333 PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); 334 // Don't add the preferred account since it has already been added previously. 335 if (!phoneAccountHandle.equals(preferredPAH)) { 336 Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle); 337 mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle, 338 phoneAccountHandle)); 339 } 340 } 341 } 342 343 // Next, add the connection manager account as a backup if it can place emergency calls. 344 PhoneAccountHandle callManagerHandle = 345 mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser(); 346 if (callManagerHandle != null) { 347 // TODO: Should this really be checking the "calling user" test for phone account? 348 PhoneAccount callManager = mPhoneAccountRegistrar 349 .getPhoneAccountUnchecked(callManagerHandle); 350 if (callManager != null && callManager.hasCapabilities( 351 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 352 CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle, 353 mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser( 354 mCall.getHandle().getScheme())); 355 if (!mAttemptRecords.contains(callAttemptRecord)) { 356 Log.i(this, "Will try Connection Manager account %s for emergency", 357 callManager); 358 mAttemptRecords.add(callAttemptRecord); 359 } 360 } 361 } 362 } 363 } 364 365 /** Returns all connection services used by the call attempt records. */ 366 private static Collection<PhoneAccountHandle> getConnectionServices( 367 List<CallAttemptRecord> records) { 368 HashSet<PhoneAccountHandle> result = new HashSet<>(); 369 for (CallAttemptRecord record : records) { 370 result.add(record.connectionManagerPhoneAccount); 371 } 372 return result; 373 } 374 375 376 private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) { 377 if (mCallResponse != null) { 378 clearTimeout(); 379 mCallResponse.handleCreateConnectionFailure(errorDisconnectCause); 380 mCallResponse = null; 381 mCall.clearConnectionService(); 382 } 383 } 384 385 @Override 386 public void handleCreateConnectionSuccess( 387 CallIdMapper idMapper, 388 ParcelableConnection connection) { 389 if (mCallResponse == null) { 390 // Nobody is listening for this connection attempt any longer; ask the responsible 391 // ConnectionService to tear down any resources associated with the call 392 mService.abort(mCall); 393 } else { 394 // Success -- share the good news and remember that we are no longer interested 395 // in hearing about any more attempts 396 mCallResponse.handleCreateConnectionSuccess(idMapper, connection); 397 mCallResponse = null; 398 // If there's a timeout running then don't clear it. The timeout can be triggered 399 // after the call has successfully been created but before it has become active. 400 } 401 } 402 403 private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) { 404 // Connection Manager does not exist or does not match registered Connection Manager 405 // Since Connection manager is a proxy for SIM, fall back to SIM 406 PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount(); 407 if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall( 408 mCall))) { 409 return false; 410 } 411 412 // The Call's Connection Service does not exist 413 ConnectionServiceWrapper connectionManager = mCall.getConnectionService(); 414 if (connectionManager == null) { 415 return true; 416 } 417 418 // In this case, fall back to a sim because connection manager declined 419 if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) { 420 Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the " 421 + "call, falling back to not using a connection manager"); 422 return false; 423 } 424 425 if (!connectionManager.isServiceValid("createConnection")) { 426 Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying " 427 + "create a connection, falling back to not using a connection manager"); 428 return false; 429 } 430 431 // Do not fall back from connection manager and simply fail call if the failure reason is 432 // other 433 Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " + 434 "error: " + cause.getReason() + ". Not falling back to SIM."); 435 return true; 436 } 437 438 @Override 439 public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) { 440 // Failure of some sort; record the reasons for failure and try again if possible 441 Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause); 442 if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) { 443 notifyCallConnectionFailure(errorDisconnectCause); 444 return; 445 } 446 mLastErrorDisconnectCause = errorDisconnectCause; 447 attemptNextPhoneAccount(); 448 } 449 } 450