1 /* 2 * Copyright (C) 2022 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 static com.android.server.telecom.LogUtils.Events.STATE_TIMEOUT; 20 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.os.Build; 25 import android.os.UserHandle; 26 import android.provider.DeviceConfig; 27 import android.telecom.ConnectionService; 28 import android.telecom.DisconnectCause; 29 import android.telecom.Log; 30 import android.telecom.PhoneAccountHandle; 31 import android.util.LocalLog; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.util.IndentingPrintWriter; 35 import com.android.server.telecom.metrics.TelecomMetricsController; 36 import com.android.server.telecom.stats.CallStateChangedAtomWriter; 37 import com.android.server.telecom.flags.FeatureFlags; 38 39 import java.util.Collections; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 import java.util.UUID; 44 import java.util.concurrent.ConcurrentHashMap; 45 import java.util.concurrent.ScheduledExecutorService; 46 import java.util.concurrent.ScheduledFuture; 47 import java.util.concurrent.TimeUnit; 48 import java.util.stream.Collectors; 49 50 /** 51 * Watchdog class responsible for detecting potential anomalous conditions for {@link Call}s. 52 */ 53 public class CallAnomalyWatchdog extends CallsManagerListenerBase implements Call.Listener { 54 private final EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger; 55 56 /** 57 * Class used to track the call state as it pertains to the watchdog. The watchdog cares about 58 * both the call state and whether a {@link ConnectionService} has finished creating the 59 * connection. 60 */ 61 public static class WatchdogCallState { 62 public final int state; 63 public final boolean isCreateConnectionComplete; 64 public final long stateStartTimeMillis; 65 WatchdogCallState(int newState, boolean newIsCreateConnectionComplete, long newStateStartTimeMillis)66 public WatchdogCallState(int newState, boolean newIsCreateConnectionComplete, 67 long newStateStartTimeMillis) { 68 state = newState; 69 isCreateConnectionComplete = newIsCreateConnectionComplete; 70 stateStartTimeMillis = newStateStartTimeMillis; 71 } 72 73 @Override equals(Object o)74 public boolean equals(Object o) { 75 if (this == o) return true; 76 if (!(o instanceof WatchdogCallState)) return false; 77 WatchdogCallState that = (WatchdogCallState) o; 78 // don't include the state timestamp in the equality check. 79 return state == that.state 80 && isCreateConnectionComplete == that.isCreateConnectionComplete; 81 } 82 83 @Override hashCode()84 public int hashCode() { 85 return Objects.hash(state, isCreateConnectionComplete); 86 } 87 88 @Override toString()89 public String toString() { 90 return "[isCreateConnComplete=" + isCreateConnectionComplete + ", state=" 91 + CallState.toString(state) + "]"; 92 } 93 94 /** 95 * Determines if the current call is in a transitory state. A call is deemed to be in a 96 * transitory state if either {@link CallState#isTransitoryState(int)} returns true, OR 97 * if the call has been created but is not yet added to {@link CallsManager} (i.e. we are 98 * still waiting for the {@link ConnectionService} to create the connection. 99 * @return {@code true} if the call is in a transitory state, {@code false} otherwise. 100 */ isInTransitoryState()101 public boolean isInTransitoryState() { 102 return CallState.isTransitoryState(state) 103 // Consider it transitory if create connection hasn't completed, EXCEPT if we 104 // are in SELECT_PHONE_ACCOUNT state since that state will depend on user input. 105 || (!isCreateConnectionComplete && state != CallState.SELECT_PHONE_ACCOUNT); 106 } 107 108 /** 109 * Determines if the current call is in an intermediate state. A call is deemed to be in 110 * an intermediate state if either {@link CallState#isIntermediateState(int)} returns true, 111 * AND the call has been created to the connection. 112 * @return {@code true} if the call is in a intermediate state, {@code false} otherwise. 113 */ isInIntermediateState()114 public boolean isInIntermediateState() { 115 return CallState.isIntermediateState(state) && isCreateConnectionComplete; 116 } 117 } 118 119 // Handler for tracking pending timeouts. 120 private final ScheduledExecutorService mScheduledExecutorService; 121 private final TelecomSystem.SyncRoot mLock; 122 private final Timeouts.Adapter mTimeoutAdapter; 123 private final ClockProxy mClockProxy; 124 private final FeatureFlags mFeatureFlags; 125 private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl(); 126 // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm) 127 private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2); 128 private final Map<Call, WatchdogCallState> mWatchdogCallStateMap = new ConcurrentHashMap<>(2); 129 // Track the calls which are pending destruction. 130 // TODO: enhance to handle the case where a call never gets destroyed. 131 private final Set<Call> mCallsPendingDestruction = Collections.newSetFromMap( 132 new ConcurrentHashMap<>(2)); 133 private final LocalLog mLocalLog = new LocalLog(20); 134 private final TelecomMetricsController mMetricsController; 135 136 /** 137 * Enables the action to disconnect the call when the Transitory state and Intermediate state 138 * time expires. 139 */ 140 private static final String ENABLE_DISCONNECT_CALL_ON_STUCK_STATE = 141 "enable_disconnect_call_on_stuck_state"; 142 /** 143 * Anomaly Report UUIDs and corresponding event descriptions specific to CallAnomalyWatchdog. 144 */ 145 public static final UUID WATCHDOG_DISCONNECTED_STUCK_CALL_UUID = 146 UUID.fromString("4b093985-c78f-45e3-a9fe-5319f397b025"); 147 public static final String WATCHDOG_DISCONNECTED_STUCK_CALL_MSG = 148 "Telecom CallAnomalyWatchdog caught and disconnected a stuck/zombie call."; 149 public static final UUID WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID = 150 UUID.fromString("d57d8aab-d723-485e-a0dd-d1abb0f346c8"); 151 public static final String WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG = 152 "Telecom CallAnomalyWatchdog caught and disconnected a stuck/zombie emergency call."; 153 public static final UUID WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_UUID = 154 UUID.fromString("3fbecd12-059d-4fd3-87b7-6c3079891c23"); 155 public static final String WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_MSG = 156 "A VoIP call was flagged due to exceeding a one-minute threshold in the DIALING or " 157 + "RINGING state"; 158 159 160 @VisibleForTesting setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter)161 public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){ 162 mAnomalyReporter = mAnomalyReporterAdapter; 163 } 164 CallAnomalyWatchdog(ScheduledExecutorService executorService, TelecomSystem.SyncRoot lock, FeatureFlags featureFlags, Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy, EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger, TelecomMetricsController metricsController)165 public CallAnomalyWatchdog(ScheduledExecutorService executorService, 166 TelecomSystem.SyncRoot lock, 167 FeatureFlags featureFlags, 168 Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy, 169 EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger, 170 TelecomMetricsController metricsController) { 171 mScheduledExecutorService = executorService; 172 mLock = lock; 173 mFeatureFlags = featureFlags; 174 mTimeoutAdapter = timeoutAdapter; 175 mClockProxy = clockProxy; 176 mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger; 177 mMetricsController = metricsController; 178 } 179 180 /** 181 * Start tracking a call that we're waiting for a ConnectionService to create. 182 * @param call the call. 183 */ 184 @Override onStartCreateConnection(Call call)185 public void onStartCreateConnection(Call call) { 186 maybeTrackCall(call); 187 call.addListener(this); 188 } 189 190 @Override onCallAdded(Call call)191 public void onCallAdded(Call call) { 192 maybeTrackCall(call); 193 if (mFeatureFlags.telecomMetricsSupport()) { 194 mMetricsController.getCallStats().onCallStart(call); 195 } 196 } 197 198 /** 199 * Override of {@link CallsManagerListenerBase} to track when calls have failed to be created by 200 * a ConnectionService. These calls should no longer be tracked by the CallAnomalyWatchdog. 201 * @param call the call 202 */ 203 @Override onCreateConnectionFailed(Call call)204 public void onCreateConnectionFailed(Call call) { 205 Log.i(this, "onCreateConnectionFailed: call=%s", call.toString()); 206 stopTrackingCall(call); 207 } 208 209 /** 210 * Override of {@link CallsManagerListenerBase} to track when calls are removed 211 * @param call the call 212 */ 213 @Override onCallRemoved(Call call)214 public void onCallRemoved(Call call) { 215 Log.i(this, "onCallRemoved: call=%s", call.toString()); 216 stopTrackingCall(call); 217 if (mFeatureFlags.telecomMetricsSupport()) { 218 mMetricsController.getCallStats().onCallEnd(call); 219 } 220 } 221 222 /** 223 * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track 224 * call state changes. 225 * @param call the call 226 * @param oldState its old state 227 * @param newState the new state 228 */ 229 @Override onCallStateChanged(Call call, int oldState, int newState)230 public void onCallStateChanged(Call call, int oldState, int newState) { 231 Log.i(this, "onCallStateChanged: call=%s", call.toString()); 232 maybeTrackCall(call); 233 } 234 235 /** 236 * Override of {@link Call.Listener} so we can capture successful creation of calls. 237 * @param call the call 238 * @param callState the state the call is now in 239 */ 240 @Override onSuccessfulOutgoingCall(Call call, int callState)241 public void onSuccessfulOutgoingCall(Call call, int callState) { 242 maybeTrackCall(call); 243 } 244 245 /** 246 * Override of {@link Call.Listener} so we can capture failed call creation. 247 * @param call the call 248 * @param disconnectCause the disconnect cause 249 */ 250 @Override onFailedOutgoingCall(Call call, DisconnectCause disconnectCause)251 public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) { 252 Log.i(this, "onFailedOutgoingCall: call=%s", call.toString()); 253 stopTrackingCall(call); 254 } 255 256 /** 257 * Override of {@link Call.Listener} so we can capture successful creation of calls 258 * @param call the call 259 */ 260 @Override onSuccessfulIncomingCall(Call call)261 public void onSuccessfulIncomingCall(Call call) { 262 maybeTrackCall(call); 263 } 264 265 /** 266 * Override of {@link Call.Listener} so we can capture failed call creation. 267 * @param call the call 268 */ 269 @Override onFailedIncomingCall(Call call)270 public void onFailedIncomingCall(Call call) { 271 Log.i(this, "onFailedIncomingCall: call=%s", call.toString()); 272 stopTrackingCall(call); 273 } 274 275 /** 276 * Helper method used to stop CallAnomalyWatchdog from tracking or destroying the call. 277 * @param call the call. 278 */ stopTrackingCall(Call call)279 private void stopTrackingCall(Call call) { 280 if (mScheduledFutureMap.containsKey(call)) { 281 ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call); 282 existingTimeout.cancel(false /* cancelIfRunning */); 283 mScheduledFutureMap.remove(call); 284 } 285 if (mCallsPendingDestruction.contains(call)) { 286 mCallsPendingDestruction.remove(call); 287 } 288 if (mWatchdogCallStateMap.containsKey(call)) { 289 mWatchdogCallStateMap.remove(call); 290 } 291 call.removeListener(this); 292 } 293 294 /** 295 * Given a {@link Call}, potentially post a cleanup task to track when the call has been in a 296 * transitory state too long. 297 * @param call the call. 298 */ maybeTrackCall(Call call)299 private void maybeTrackCall(Call call) { 300 final WatchdogCallState currentState = mWatchdogCallStateMap.get(call); 301 boolean isCreateConnectionComplete = call.isCreateConnectionComplete(); 302 if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) { 303 isCreateConnectionComplete = 304 isCreateConnectionComplete || call.isTransactionalCall(); 305 } 306 final WatchdogCallState newState = new WatchdogCallState(call.getState(), 307 isCreateConnectionComplete, mClockProxy.elapsedRealtime()); 308 if (Objects.equals(currentState, newState)) { 309 // No state change; skip. 310 return; 311 } 312 mWatchdogCallStateMap.put(call, newState); 313 314 // The call's state has changed, so we will remove any existing state cleanup tasks. 315 if (mScheduledFutureMap.containsKey(call)) { 316 ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call); 317 existingTimeout.cancel(false /* cancelIfRunning */); 318 mScheduledFutureMap.remove(call); 319 } 320 321 Log.i(this, "maybePostCleanupTask; callId=%s, state=%s, createConnComplete=%b", 322 call.getId(), CallState.toString(call.getState()), 323 call.isCreateConnectionComplete()); 324 325 long timeoutMillis = getTimeoutMillis(call, newState); 326 boolean isEnabledDisconnect = isEnabledDisconnectForStuckCall(); 327 // If the call is now in a transitory or intermediate state, post a new cleanup task. 328 if (timeoutMillis > 0) { 329 Runnable cleanupRunnable = getCleanupRunnable(call, newState, timeoutMillis, 330 isEnabledDisconnect); 331 332 // Post cleanup to the executor service and cache the future, so we can cancel it if 333 // needed. 334 ScheduledFuture<?> future = mScheduledExecutorService.schedule(cleanupRunnable, 335 timeoutMillis, TimeUnit.MILLISECONDS); 336 mScheduledFutureMap.put(call, future); 337 } 338 } 339 getTimeoutMillis(Call call, WatchdogCallState state)340 public long getTimeoutMillis(Call call, WatchdogCallState state) { 341 boolean isVoip = call.getIsVoipAudioMode(); 342 boolean isEmergency = call.isEmergencyCall(); 343 344 if (state.isInTransitoryState()) { 345 if (isVoip) { 346 return (isEmergency) ? 347 mTimeoutAdapter.getVoipEmergencyCallTransitoryStateTimeoutMillis() : 348 mTimeoutAdapter.getVoipCallTransitoryStateTimeoutMillis(); 349 } 350 351 return (isEmergency) ? 352 mTimeoutAdapter.getNonVoipEmergencyCallTransitoryStateTimeoutMillis() : 353 mTimeoutAdapter.getNonVoipCallTransitoryStateTimeoutMillis(); 354 } 355 356 if (state.isInIntermediateState()) { 357 if (isVoip) { 358 return (isEmergency) ? 359 mTimeoutAdapter.getVoipEmergencyCallIntermediateStateTimeoutMillis() : 360 mTimeoutAdapter.getVoipCallIntermediateStateTimeoutMillis(); 361 } 362 363 return (isEmergency) ? 364 mTimeoutAdapter.getNonVoipEmergencyCallIntermediateStateTimeoutMillis() : 365 mTimeoutAdapter.getNonVoipCallIntermediateStateTimeoutMillis(); 366 } 367 368 return 0; 369 } 370 getCleanupRunnable(Call call, WatchdogCallState newState, long timeoutMillis, boolean isEnabledDisconnect)371 private Runnable getCleanupRunnable(Call call, WatchdogCallState newState, long timeoutMillis, 372 boolean isEnabledDisconnect) { 373 Runnable cleanupRunnable = new android.telecom.Logging.Runnable("CAW.mR", mLock) { 374 @Override 375 public void loggedRun() { 376 // If we're already pending a cleanup due to a state violation for this call. 377 if (mCallsPendingDestruction.contains(call)) { 378 return; 379 } 380 // Ensure that at timeout we are still in the original state when we posted the 381 // timeout. 382 boolean isCreateConnectionComplete = call.isCreateConnectionComplete(); 383 if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) { 384 isCreateConnectionComplete = 385 isCreateConnectionComplete || call.isTransactionalCall(); 386 } 387 final WatchdogCallState expiredState = new WatchdogCallState(call.getState(), 388 isCreateConnectionComplete, mClockProxy.elapsedRealtime()); 389 if (expiredState.equals(newState) 390 && getDurationInCurrentStateMillis(newState) > timeoutMillis) { 391 // The call has been in this transitory or intermediate state too long, 392 // so disconnect it and destroy it. 393 Log.addEvent(call, STATE_TIMEOUT, newState); 394 mLocalLog.log("STATE_TIMEOUT; callId=" + call.getId() + " in state " 395 + newState); 396 if (call.isEmergencyCall()){ 397 mAnomalyReporter.reportAnomaly( 398 WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID, 399 WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG); 400 mEmergencyCallDiagnosticLogger.reportStuckCall(call); 401 } else { 402 mAnomalyReporter.reportAnomaly( 403 WATCHDOG_DISCONNECTED_STUCK_CALL_UUID, 404 WATCHDOG_DISCONNECTED_STUCK_CALL_MSG); 405 } 406 407 if (isEnabledDisconnect || isInSelfManagedStuckStartingState(call)) { 408 call.setOverrideDisconnectCauseCode( 409 new DisconnectCause(DisconnectCause.ERROR, "state_timeout")); 410 call.disconnect("State timeout"); 411 } else { 412 writeCallStateChangedAtom(call); 413 } 414 415 mCallsPendingDestruction.add(call); 416 if (mWatchdogCallStateMap.containsKey(call)) { 417 mWatchdogCallStateMap.remove(call); 418 } 419 } 420 mScheduledFutureMap.remove(call); 421 } 422 }.prepare(); 423 return cleanupRunnable; 424 } 425 isInSelfManagedStuckStartingState(Call call)426 private boolean isInSelfManagedStuckStartingState(Call call) { 427 Context context = call.getContext(); 428 if (!mFeatureFlags.disconnectSelfManagedStuckStartupCalls() || context == null) { 429 return false; 430 } 431 int currentStuckState = call.getState(); 432 return call.isSelfManaged() && 433 (currentStuckState == CallState.NEW || 434 currentStuckState == CallState.RINGING || 435 currentStuckState == CallState.DIALING || 436 currentStuckState == CallState.CONNECTING) && 437 isVanillaIceCreamBuildOrHigher(context, call); 438 } 439 isVanillaIceCreamBuildOrHigher(Context context, Call call)440 private boolean isVanillaIceCreamBuildOrHigher(Context context, Call call) { 441 // report the anomaly for metrics purposes 442 mAnomalyReporter.reportAnomaly( 443 WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_UUID, 444 WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_MSG); 445 // only disconnect calls running on V and when the flag is enabled! 446 PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount(); 447 PackageManager pm = context.getPackageManager(); 448 if (pm == null || 449 phoneAccountHandle == null || 450 phoneAccountHandle.getComponentName() == null) { 451 return false; 452 } 453 String packageName = phoneAccountHandle.getComponentName().getPackageName(); 454 Log.d(this, "pah=[%s], user=[%s]", phoneAccountHandle, call.getAssociatedUser()); 455 ApplicationInfo applicationInfo; 456 try { 457 applicationInfo = pm.getApplicationInfoAsUser( 458 packageName, 459 0, 460 call.getAssociatedUser()); 461 } catch (Exception e) { 462 Log.e(this, e, "iVICBOH: pm.getApplicationInfoAsUser(...) exception"); 463 return false; 464 } 465 int targetSdk = (applicationInfo == null) ? 0 : applicationInfo.targetSdkVersion; 466 Log.i(this, "iVICBOH: packageName=[%s], sdk=[%d]", packageName, targetSdk); 467 return targetSdk >= Build.VERSION_CODES.VANILLA_ICE_CREAM; 468 } 469 470 /** 471 * Returns whether the action to disconnect the call when the Transitory state and 472 * Intermediate state time expires is enabled or disabled. 473 * @return {@code true} if the action is enabled, {@code false} if the action is disabled. 474 */ isEnabledDisconnectForStuckCall()475 private boolean isEnabledDisconnectForStuckCall() { 476 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TELEPHONY, 477 ENABLE_DISCONNECT_CALL_ON_STUCK_STATE, false); 478 } 479 480 /** 481 * Determines how long a call has been in a specific state. 482 * @param state the call state. 483 * @return the time in the state, in millis. 484 */ getDurationInCurrentStateMillis(WatchdogCallState state)485 private long getDurationInCurrentStateMillis(WatchdogCallState state) { 486 return mClockProxy.elapsedRealtime() - state.stateStartTimeMillis; 487 } 488 writeCallStateChangedAtom(Call call)489 private void writeCallStateChangedAtom(Call call) { 490 new CallStateChangedAtomWriter() 491 .setDisconnectCause(call.getDisconnectCause()) 492 .setSelfManaged(call.isSelfManaged()) 493 .setExternalCall(call.isExternalCall()) 494 .setEmergencyCall(call.isEmergencyCall()) 495 .write(call.getState()); 496 } 497 498 /** 499 * Dumps the state of the {@link CallAnomalyWatchdog}. 500 * 501 * @param pw The {@code IndentingPrintWriter} to write the state to. 502 */ dump(IndentingPrintWriter pw)503 public void dump(IndentingPrintWriter pw) { 504 pw.println("Anomaly log:"); 505 pw.increaseIndent(); 506 mLocalLog.dump(pw); 507 pw.decreaseIndent(); 508 pw.print("Pending timeouts: "); 509 pw.println(mScheduledFutureMap.keySet().stream().map(c -> c.getId()).collect( 510 Collectors.joining(","))); 511 pw.print("Pending destruction: "); 512 pw.println(mCallsPendingDestruction.stream().map(c -> c.getId()).collect( 513 Collectors.joining(","))); 514 } 515 516 @VisibleForTesting getNumberOfScheduledTimeouts()517 public int getNumberOfScheduledTimeouts() { 518 return mScheduledFutureMap.size(); 519 } 520 } 521