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