• 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 android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
20 
21 import android.os.BugreportManager;
22 import android.os.DropBoxManager;
23 import android.provider.DeviceConfig;
24 import android.telecom.DisconnectCause;
25 import android.telecom.Log;
26 import android.telephony.TelephonyManager;
27 import android.util.LocalLog;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.IndentingPrintWriter;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.concurrent.Executor;
38 
39 /**
40  * The EmergencyCallDiagnosticsLogger monitors information required to diagnose potential outgoing
41  * ecall failures on the device. When a potential failure is detected, it calls a Telephony API to
42  * persist relevant information (dumpsys, logcat etc.) to the dropbox. This acts as a central place
43  * to determine when and what to collect.
44  *
45  * <p>When a bugreport is triggered, this module will read the dropbox entries and add them to the
46  * telecom dump.
47  */
48 public class EmergencyCallDiagnosticLogger extends CallsManagerListenerBase
49         implements Call.Listener {
50 
51     public static final int REPORT_REASON_RANGE_START = -1; //!!DO NOT CHANGE
52     public static final int REPORT_REASON_RANGE_END = 5; //increment this and add new reason above
53     public static final int COLLECTION_TYPE_BUGREPORT = 10;
54     public static final int COLLECTION_TYPE_TELECOM_STATE = 11;
55     public static final int COLLECTION_TYPE_TELEPHONY_STATE = 12;
56     public static final int COLLECTION_TYPE_LOGCAT_BUFFERS = 13;
57     private static final int REPORT_REASON_STUCK_CALL_DETECTED = 0;
58     private static final int REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY = 1;
59     private static final int REPORT_REASON_CALL_FAILED = 2;
60     private static final int REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED = 3;
61     private static final int REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE = 4;
62     private static final String DROPBOX_TAG = "ecall_diagnostic_data";
63     private static final String ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
64             "enable_bugreport_collection_for_emergency_call_diagnostics";
65     private static final String ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
66             "enable_telecom_dump_collection_for_emergency_call_diagnostics";
67 
68     private static final String ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
69             "enable_logcat_collection_for_emergency_call_diagnostics";
70     private static final String ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
71             "enable_telephony_dump_collection_for_emergency_call_diagnostics";
72 
73     private static final String DUMPSYS_ARG_FOR_DIAGNOSTICS = "EmergencyDiagnostics";
74 
75     // max text size to read from dropbox entry
76     private static final int DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY = 500000;
77     private static final String MAX_BYTES_PER_DROP_BOX_ENTRY = "max_bytes_per_dropbox_entry";
78     private static final int MAX_DROPBOX_ENTRIES_TO_DUMP = 6;
79 
80     private final Timeouts.Adapter mTimeoutAdapter;
81     // This map holds all calls, but keeps pruning non-emergency calls when we can determine it
82     private final Map<Call, CallEventTimestamps> mEmergencyCallsMap = new ConcurrentHashMap<>(2);
83     private final DropBoxManager mDropBoxManager;
84     private final LocalLog mLocalLog = new LocalLog(10);
85     private final TelephonyManager mTelephonyManager;
86     private final BugreportManager mBugreportManager;
87     private final Executor mAsyncTaskExecutor;
88     private final ClockProxy mClockProxy;
89 
EmergencyCallDiagnosticLogger( TelephonyManager tm, BugreportManager brm, Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager, Executor asyncTaskExecutor, ClockProxy clockProxy)90     public EmergencyCallDiagnosticLogger(
91             TelephonyManager tm,
92             BugreportManager brm,
93             Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager,
94             Executor asyncTaskExecutor, ClockProxy clockProxy) {
95         mTimeoutAdapter = timeoutAdapter;
96         mDropBoxManager = dropBoxManager;
97         mTelephonyManager = tm;
98         mBugreportManager = brm;
99         mAsyncTaskExecutor = asyncTaskExecutor;
100         mClockProxy = clockProxy;
101     }
102 
103     // this calculates time from ACTIVE --> removed
getCallTimeInActiveStateSec(CallEventTimestamps ts)104     private static long getCallTimeInActiveStateSec(CallEventTimestamps ts) {
105         if (ts.getCallActiveTime() == 0 || ts.getCallRemovedTime() == 0) {
106             return 0;
107         } else {
108             return (ts.getCallRemovedTime() - ts.getCallActiveTime()) / 1000;
109         }
110     }
111 
112     // this calculates time from call created --> removed
getTotalCallTimeSec(CallEventTimestamps ts)113     private static long getTotalCallTimeSec(CallEventTimestamps ts) {
114         if (ts.getCallRemovedTime() == 0 || ts.getCallCreatedTime() == 0) {
115             return 0;
116         } else {
117             return (ts.getCallRemovedTime() - ts.getCallCreatedTime()) / 1000;
118         }
119     }
120 
121     //determines what to collect based on fail reason
122     //if COLLECTION_TYPE_BUGREPORT is present in the returned list, then that
123     //should be the only collection type in the list
124     @VisibleForTesting
getDataCollectionTypes(int reason)125     public static List<Integer> getDataCollectionTypes(int reason) {
126         switch (reason) {
127             case REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE:
128                 return Arrays.asList(COLLECTION_TYPE_TELECOM_STATE);
129             case REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED:
130                 return Arrays.asList(
131                         COLLECTION_TYPE_TELECOM_STATE, COLLECTION_TYPE_TELEPHONY_STATE);
132             case REPORT_REASON_CALL_FAILED:
133             case REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY:
134             case REPORT_REASON_STUCK_CALL_DETECTED:
135                 return Arrays.asList(
136                         COLLECTION_TYPE_TELECOM_STATE,
137                         COLLECTION_TYPE_TELEPHONY_STATE,
138                         COLLECTION_TYPE_LOGCAT_BUFFERS);
139             default:
140         }
141         return new ArrayList<>();
142     }
143 
getMaxBytesPerDropboxEntry()144     private int getMaxBytesPerDropboxEntry() {
145         return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
146                 MAX_BYTES_PER_DROP_BOX_ENTRY, DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY);
147     }
148 
149     @VisibleForTesting
getEmergencyCallsMap()150     public Map<Call, CallEventTimestamps> getEmergencyCallsMap() {
151         return mEmergencyCallsMap;
152     }
153 
triggerDiagnosticsCollection(Call call, int reason)154     private void triggerDiagnosticsCollection(Call call, int reason) {
155         Log.i(this, "Triggering diagnostics for call %s reason: %d", call.getId(), reason);
156         List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
157         boolean invokeTelephonyPersistApi = false;
158         CallEventTimestamps ts = mEmergencyCallsMap.get(call);
159         EmergencyCallDiagnosticParams dp =
160                 new EmergencyCallDiagnosticParams();
161         for (Integer dataCollectionType : dataCollectionTypes) {
162             switch (dataCollectionType) {
163                 case COLLECTION_TYPE_TELECOM_STATE:
164                     if (isTelecomDumpCollectionEnabled()) {
165                         dp.setTelecomDumpSysCollection(true);
166                         invokeTelephonyPersistApi = true;
167                     }
168                     break;
169                 case COLLECTION_TYPE_TELEPHONY_STATE:
170                     if (isTelephonyDumpCollectionEnabled()) {
171                         dp.setTelephonyDumpSysCollection(true);
172                         invokeTelephonyPersistApi = true;
173                     }
174                     break;
175                 case COLLECTION_TYPE_LOGCAT_BUFFERS:
176                     if (isLogcatCollectionEnabled()) {
177                         dp.setLogcatCollection(true, ts.getCallCreatedTime());
178                         invokeTelephonyPersistApi = true;
179                     }
180                     break;
181                 case COLLECTION_TYPE_BUGREPORT:
182                     if (isBugreportCollectionEnabled()) {
183                         mAsyncTaskExecutor.execute(new Runnable() {
184                             @Override
185                             public void run() {
186                                 persistBugreport();
187                             }
188                         });
189                     }
190                     break;
191                 default:
192             }
193         }
194         if (invokeTelephonyPersistApi) {
195             mAsyncTaskExecutor.execute(new Runnable() {
196                 @Override
197                 public void run() {
198                     Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
199                     try {
200                         mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
201                     } catch (Exception e) {
202                         Log.w(this,
203                                 "Exception while invoking "
204                                         + "Telephony#persistEmergencyCallDiagnosticData  %s",
205                                 e.toString());
206                     }
207                 }
208             });
209         }
210     }
211 
isBugreportCollectionEnabled()212     private boolean isBugreportCollectionEnabled() {
213         return DeviceConfig.getBoolean(
214                 DeviceConfig.NAMESPACE_TELEPHONY,
215                 ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
216                 false);
217     }
218 
isTelecomDumpCollectionEnabled()219     private boolean isTelecomDumpCollectionEnabled() {
220         return DeviceConfig.getBoolean(
221                 DeviceConfig.NAMESPACE_TELEPHONY,
222                 ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
223                 true);
224     }
225 
isLogcatCollectionEnabled()226     private boolean isLogcatCollectionEnabled() {
227         return DeviceConfig.getBoolean(
228                 DeviceConfig.NAMESPACE_TELEPHONY,
229                 ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
230                 true);
231     }
232 
isTelephonyDumpCollectionEnabled()233     private boolean isTelephonyDumpCollectionEnabled() {
234         return DeviceConfig.getBoolean(
235                 DeviceConfig.NAMESPACE_TELEPHONY,
236                 ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
237                 true);
238     }
239 
persistBugreport()240     private void persistBugreport() {
241         if (isBugreportCollectionEnabled()) {
242             // TODO:
243         }
244     }
245 
shouldTrackCall(Call call)246     private boolean shouldTrackCall(Call call) {
247         return (call != null && call.isEmergencyCall() && call.isOutgoing());
248     }
249 
reportStuckCall(Call call)250     public void reportStuckCall(Call call) {
251         if (shouldTrackCall(call)) {
252             Log.i(this, "Triggering diagnostics for stuck call %s", call.getId());
253             triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED);
254             call.removeListener(this);
255             mEmergencyCallsMap.remove(call);
256         }
257     }
258 
259     @Override
onStartCreateConnection(Call call)260     public void onStartCreateConnection(Call call) {
261         if (shouldTrackCall(call)) {
262             long currentTime = mClockProxy.currentTimeMillis();
263             call.addListener(this);
264             Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime);
265             mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime));
266         }
267     }
268 
269     @Override
onCreateConnectionFailed(Call call)270     public void onCreateConnectionFailed(Call call) {
271         if (shouldTrackCall(call)) {
272             Log.i(this, "Triggering diagnostics for  call %s that was never added", call.getId());
273             triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED);
274             call.removeListener(this);
275             mEmergencyCallsMap.remove(call);
276         }
277     }
278 
279     /**
280      * Override of {@link CallsManagerListenerBase} to track when calls are removed
281      *
282      * @param call the call
283      */
284     @Override
onCallRemoved(Call call)285     public void onCallRemoved(Call call) {
286         if (call != null && (mEmergencyCallsMap.get(call) != null)) {
287             call.removeListener(this);
288 
289             CallEventTimestamps ts = mEmergencyCallsMap.get(call);
290             long currentTime = mClockProxy.currentTimeMillis();
291             ts.setCallRemovedTime(currentTime);
292 
293             maybeTriggerDiagnosticsCollection(call, ts);
294             mEmergencyCallsMap.remove(call);
295         }
296     }
297 
298     // !NOTE!: this method should only be called after we get onCallRemoved
maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts)299     private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) {
300         Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId());
301         boolean wentActive = (ts.getCallActiveTime() != 0);
302         long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0);
303         long timeSinceCallCreatedSec = getTotalCallTimeSec(ts);
304         int dc = removedCall.getDisconnectCause().getCode();
305 
306         if (wentActive) {
307             if (callActiveTimeSec
308                     < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) {
309                 // call connected but did not go on for long
310                 triggerDiagnosticsCollection(
311                         removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE);
312             }
313         } else {
314 
315             if (dc == DisconnectCause.LOCAL
316                     && timeSinceCallCreatedSec
317                     > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()
318                     / 1000) {
319                 // call was disconnected by the user (but not immediately)
320                 triggerDiagnosticsCollection(
321                         removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY);
322             } else if (dc != DisconnectCause.LOCAL) {
323                 // this can be a case for a full bugreport
324                 triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED);
325             }
326         }
327     }
328 
329     /**
330      * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
331      * call state changes.
332      *
333      * @param call     the call
334      * @param oldState its old state
335      * @param newState the new state
336      */
337     @Override
onCallStateChanged(Call call, int oldState, int newState)338     public void onCallStateChanged(Call call, int oldState, int newState) {
339 
340         if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) {
341             CallEventTimestamps ts = mEmergencyCallsMap.get(call);
342             if (ts != null) {
343                 long currentTime = mClockProxy.currentTimeMillis();
344                 ts.setCallActiveTime(currentTime);
345             }
346         }
347     }
348 
dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw)349     private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) {
350         pw.increaseIndent();
351         pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
352         int totalEntriesDumped = 0;
353         long currentTime = mClockProxy.currentTimeMillis();
354         long entriesAfterTime =
355                 currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24
356                         * 60L * 60L * 1000L);
357         Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime);
358         DropBoxManager.Entry entry;
359         entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime);
360         while (entry != null) {
361             Log.i(this, "found entry with ts: %d", entry.getTimeMillis());
362             String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split(
363                     System.lineSeparator());
364             long entryTime = entry.getTimeMillis();
365             if (content != null) {
366                 pw.increaseIndent();
367                 pw.println("------------BEGIN ENTRY (" + entryTime + ")--------");
368                 for (String line : content) {
369                     pw.println(line);
370                 }
371                 pw.println("--------END ENTRY--------");
372                 pw.decreaseIndent();
373                 totalEntriesDumped++;
374             }
375             entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime);
376             if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) {
377                 /*
378                 Since Emergency calls are a rare/once in a lifetime time occurrence for most users,
379                 we should not be seeing too many entries. This code just guards against edge case
380                 like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in
381                 such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the
382                 bugreport
383 
384                 The Dropbox API in its current state does not allow to query Entries in reverse
385                 chronological order efficiently.
386                  */
387 
388                 Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped);
389                 break;
390             }
391         }
392         pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
393         pw.decreaseIndent();
394     }
395 
dump(IndentingPrintWriter pw, String[] args)396     public void dump(IndentingPrintWriter pw, String[] args) {
397         pw.increaseIndent();
398         mLocalLog.dump(pw);
399         pw.decreaseIndent();
400         if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) {
401             //dont read dropbox entries since this dump is triggered by telephony for diagnostics
402             Log.i(this, "skipped dumping diagnostic data");
403             return;
404         }
405         dumpDiagnosticDataFromDropbox(pw);
406     }
407 
408     private static class CallEventTimestamps {
409 
410         private final long mCallCreatedTime;
411         private long mCallActiveTime;
412         private long mCallRemovedTime;
413 
CallEventTimestamps(long createdTime)414         public CallEventTimestamps(long createdTime) {
415             mCallCreatedTime = createdTime;
416         }
417 
getCallActiveTime()418         public long getCallActiveTime() {
419             return mCallActiveTime;
420         }
421 
setCallActiveTime(long callActiveTime)422         public void setCallActiveTime(long callActiveTime) {
423             this.mCallActiveTime = callActiveTime;
424         }
425 
getCallCreatedTime()426         public long getCallCreatedTime() {
427             return mCallCreatedTime;
428         }
429 
getCallRemovedTime()430         public long getCallRemovedTime() {
431             return mCallRemovedTime;
432         }
433 
setCallRemovedTime(long callRemovedTime)434         public void setCallRemovedTime(long callRemovedTime) {
435             this.mCallRemovedTime = callRemovedTime;
436         }
437     }
438 }
439