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 CallAttemptRecord( PhoneAccountHandle connectionManagerPhoneAccount, PhoneAccountHandle targetPhoneAccount)56 public CallAttemptRecord( 57 PhoneAccountHandle connectionManagerPhoneAccount, 58 PhoneAccountHandle targetPhoneAccount) { 59 this.connectionManagerPhoneAccount = connectionManagerPhoneAccount; 60 this.targetPhoneAccount = targetPhoneAccount; 61 } 62 63 @Override toString()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 equals(Object obj)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 CreateConnectionProcessor( Call call, ConnectionServiceRepository repository, CreateConnectionResponse response, PhoneAccountRegistrar phoneAccountRegistrar, Context context)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 isProcessingComplete()114 boolean isProcessingComplete() { 115 return mCallResponse == null; 116 } 117 isCallTimedOut()118 boolean isCallTimedOut() { 119 return mTimeout != null && mTimeout.isCallTimedOut(); 120 } 121 getConnectionAttempt()122 public int getConnectionAttempt() { 123 return mConnectionAttempt; 124 } 125 126 @VisibleForTesting process()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 hasMorePhoneAccounts()143 boolean hasMorePhoneAccounts() { 144 return mAttemptRecordIterator.hasNext(); 145 } 146 continueProcessingIfPossible(CreateConnectionResponse response, DisconnectCause disconnectCause)147 void continueProcessingIfPossible(CreateConnectionResponse response, 148 DisconnectCause disconnectCause) { 149 Log.v(this, "continueProcessingIfPossible"); 150 mCallResponse = response; 151 mLastErrorDisconnectCause = disconnectCause; 152 attemptNextPhoneAccount(); 153 } 154 abort()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 attemptNextPhoneAccount()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 217 mService.createConnection(mCall, this); 218 } 219 } else { 220 Log.v(this, "attemptNextPhoneAccount, no more accounts, failing"); 221 DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ? 222 mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR); 223 notifyCallConnectionFailure(disconnectCause); 224 } 225 } 226 setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt)227 private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) { 228 clearTimeout(); 229 230 CreateConnectionTimeout timeout = new CreateConnectionTimeout( 231 mContext, mPhoneAccountRegistrar, service, mCall); 232 if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords), 233 attempt.connectionManagerPhoneAccount)) { 234 mTimeout = timeout; 235 timeout.registerTimeout(); 236 } 237 } 238 clearTimeout()239 private void clearTimeout() { 240 if (mTimeout != null) { 241 mTimeout.unregisterTimeout(); 242 mTimeout = null; 243 } 244 } 245 shouldSetConnectionManager()246 private boolean shouldSetConnectionManager() { 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 = 258 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall); 259 if (connectionManager == null) { 260 return false; 261 } 262 263 PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount; 264 if (Objects.equals(connectionManager, targetPhoneAccountHandle)) { 265 return false; 266 } 267 268 // Connection managers are only allowed to manage SIM subscriptions. 269 // TODO: Should this really be checking the "calling user" test for phone account? 270 PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar 271 .getPhoneAccountUnchecked(targetPhoneAccountHandle); 272 if (targetPhoneAccount == null) { 273 Log.d(this, "shouldSetConnectionManager, phone account not found"); 274 return false; 275 } 276 boolean isSimSubscription = (targetPhoneAccount.getCapabilities() & 277 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0; 278 if (!isSimSubscription) { 279 return false; 280 } 281 282 return true; 283 } 284 285 // If there exists a registered connection manager then use it. adjustAttemptsForConnectionManager()286 private void adjustAttemptsForConnectionManager() { 287 if (shouldSetConnectionManager()) { 288 CallAttemptRecord record = new CallAttemptRecord( 289 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall), 290 mAttemptRecords.get(0).targetPhoneAccount); 291 Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record); 292 mAttemptRecords.add(0, record); 293 } else { 294 Log.v(this, "setConnectionManager, not changing"); 295 } 296 } 297 298 // If we are possibly attempting to call a local emergency number, ensure that the 299 // plain PSTN connection services are listed, and nothing else. adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH)300 private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) { 301 if (mCall.isEmergencyCall()) { 302 Log.i(this, "Emergency number detected"); 303 mAttemptRecords.clear(); 304 // Phone accounts in profile do not handle emergency call, use phone accounts in 305 // current user. 306 List<PhoneAccount> allAccounts = mPhoneAccountRegistrar 307 .getAllPhoneAccountsOfCurrentUser(); 308 309 if (allAccounts.isEmpty()) { 310 // If the list of phone accounts is empty at this point, it means Telephony hasn't 311 // registered any phone accounts yet. Add a fallback emergency phone account so 312 // that emergency calls can still go through. We create a new ArrayLists here just 313 // in case the implementation of PhoneAccountRegistrar ever returns an unmodifiable 314 // list. 315 allAccounts = new ArrayList<PhoneAccount>(); 316 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount()); 317 } 318 319 // First, possibly add the SIM phone account that the user prefers 320 PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked( 321 preferredPAH); 322 if (preferredPA != null && 323 preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) && 324 preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 325 Log.i(this, "Will try PSTN account %s for emergency", 326 preferredPA.getAccountHandle()); 327 mAttemptRecords.add(new CallAttemptRecord(preferredPAH, preferredPAH)); 328 } 329 330 // Next, add all SIM phone accounts which can place emergency calls. 331 TelephonyUtil.sortSimPhoneAccounts(mContext, allAccounts); 332 for (PhoneAccount phoneAccount : allAccounts) { 333 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) && 334 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 335 PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle(); 336 // Don't add the preferred account since it has already been added previously. 337 if (!phoneAccountHandle.equals(preferredPAH)) { 338 Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle); 339 mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle, 340 phoneAccountHandle)); 341 } 342 } 343 } 344 345 // Next, add the connection manager account as a backup if it can place emergency calls. 346 PhoneAccountHandle callManagerHandle = 347 mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser(); 348 if (callManagerHandle != null) { 349 // TODO: Should this really be checking the "calling user" test for phone account? 350 PhoneAccount callManager = mPhoneAccountRegistrar 351 .getPhoneAccountUnchecked(callManagerHandle); 352 if (callManager != null && callManager.hasCapabilities( 353 PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) { 354 CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle, 355 mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser( 356 mCall.getHandle().getScheme())); 357 if (!mAttemptRecords.contains(callAttemptRecord)) { 358 Log.i(this, "Will try Connection Manager account %s for emergency", 359 callManager); 360 mAttemptRecords.add(callAttemptRecord); 361 } 362 } 363 } 364 } 365 } 366 367 /** Returns all connection services used by the call attempt records. */ getConnectionServices( List<CallAttemptRecord> records)368 private static Collection<PhoneAccountHandle> getConnectionServices( 369 List<CallAttemptRecord> records) { 370 HashSet<PhoneAccountHandle> result = new HashSet<>(); 371 for (CallAttemptRecord record : records) { 372 result.add(record.connectionManagerPhoneAccount); 373 } 374 return result; 375 } 376 377 notifyCallConnectionFailure(DisconnectCause errorDisconnectCause)378 private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) { 379 if (mCallResponse != null) { 380 clearTimeout(); 381 mCallResponse.handleCreateConnectionFailure(errorDisconnectCause); 382 mCallResponse = null; 383 mCall.clearConnectionService(); 384 } 385 } 386 387 @Override handleCreateConnectionSuccess( CallIdMapper idMapper, ParcelableConnection connection)388 public void handleCreateConnectionSuccess( 389 CallIdMapper idMapper, 390 ParcelableConnection connection) { 391 if (mCallResponse == null) { 392 // Nobody is listening for this connection attempt any longer; ask the responsible 393 // ConnectionService to tear down any resources associated with the call 394 mService.abort(mCall); 395 } else { 396 // Success -- share the good news and remember that we are no longer interested 397 // in hearing about any more attempts 398 mCallResponse.handleCreateConnectionSuccess(idMapper, connection); 399 mCallResponse = null; 400 // If there's a timeout running then don't clear it. The timeout can be triggered 401 // after the call has successfully been created but before it has become active. 402 } 403 } 404 shouldFailCallIfConnectionManagerFails(DisconnectCause cause)405 private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) { 406 // Connection Manager does not exist or does not match registered Connection Manager 407 // Since Connection manager is a proxy for SIM, fall back to SIM 408 PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount(); 409 if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall( 410 mCall))) { 411 return false; 412 } 413 414 // The Call's Connection Service does not exist 415 ConnectionServiceWrapper connectionManager = mCall.getConnectionService(); 416 if (connectionManager == null) { 417 return true; 418 } 419 420 // In this case, fall back to a sim because connection manager declined 421 if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) { 422 Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the " 423 + "call, falling back to not using a connection manager"); 424 return false; 425 } 426 427 if (!connectionManager.isServiceValid("createConnection")) { 428 Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying " 429 + "create a connection, falling back to not using a connection manager"); 430 return false; 431 } 432 433 // Do not fall back from connection manager and simply fail call if the failure reason is 434 // other 435 Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " + 436 "error: " + cause.getReason() + ". Not falling back to SIM."); 437 return true; 438 } 439 440 @Override handleCreateConnectionFailure(DisconnectCause errorDisconnectCause)441 public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) { 442 // Failure of some sort; record the reasons for failure and try again if possible 443 Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause); 444 if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) { 445 notifyCallConnectionFailure(errorDisconnectCause); 446 return; 447 } 448 mLastErrorDisconnectCause = errorDisconnectCause; 449 attemptNextPhoneAccount(); 450 } 451 } 452