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