• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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