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