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.EmergencyCallDiagnosticData;
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         EmergencyCallDiagnosticData.Builder callDiagnosticBuilder =
160                 new EmergencyCallDiagnosticData.Builder();
161         for (Integer dataCollectionType : dataCollectionTypes) {
162             switch (dataCollectionType) {
163                 case COLLECTION_TYPE_TELECOM_STATE:
164                     if (isTelecomDumpCollectionEnabled()) {
165                         callDiagnosticBuilder.setTelecomDumpsysCollectionEnabled(true);
166                         invokeTelephonyPersistApi = true;
167                     }
168                     break;
169                 case COLLECTION_TYPE_TELEPHONY_STATE:
170                     if (isTelephonyDumpCollectionEnabled()) {
171                         callDiagnosticBuilder.setTelephonyDumpsysCollectionEnabled(true);
172                         invokeTelephonyPersistApi = true;
173                     }
174                     break;
175                 case COLLECTION_TYPE_LOGCAT_BUFFERS:
176                     if (isLogcatCollectionEnabled()) {
177                         callDiagnosticBuilder.setLogcatCollectionStartTimeMillis(
178                                 ts.getCallCreatedTime());
179                         invokeTelephonyPersistApi = true;
180                     }
181                     break;
182                 case COLLECTION_TYPE_BUGREPORT:
183                     if (isBugreportCollectionEnabled()) {
184                         mAsyncTaskExecutor.execute(new Runnable() {
185                             @Override
186                             public void run() {
187                                 persistBugreport();
188                             }
189                         });
190                     }
191                     break;
192                 default:
193             }
194         }
195         EmergencyCallDiagnosticData ecdData = callDiagnosticBuilder.build();
196         if (invokeTelephonyPersistApi) {
197             mAsyncTaskExecutor.execute(new Runnable() {
198                 @Override
199                 public void run() {
200                     Log.i(this, "Requesting Telephony to persist data %s", ecdData.toString());
201                     try {
202                         mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, ecdData);
203                     } catch (Exception e) {
204                         Log.w(this,
205                                 "Exception while invoking "
206                                         + "Telephony#persistEmergencyCallDiagnosticData  %s",
207                                 e.toString());
208                     }
209                 }
210             });
211         }
212     }
213 
isBugreportCollectionEnabled()214     private boolean isBugreportCollectionEnabled() {
215         return DeviceConfig.getBoolean(
216                 DeviceConfig.NAMESPACE_TELEPHONY,
217                 ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
218                 false);
219     }
220 
isTelecomDumpCollectionEnabled()221     private boolean isTelecomDumpCollectionEnabled() {
222         return DeviceConfig.getBoolean(
223                 DeviceConfig.NAMESPACE_TELEPHONY,
224                 ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
225                 true);
226     }
227 
isLogcatCollectionEnabled()228     private boolean isLogcatCollectionEnabled() {
229         return DeviceConfig.getBoolean(
230                 DeviceConfig.NAMESPACE_TELEPHONY,
231                 ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
232                 true);
233     }
234 
isTelephonyDumpCollectionEnabled()235     private boolean isTelephonyDumpCollectionEnabled() {
236         return DeviceConfig.getBoolean(
237                 DeviceConfig.NAMESPACE_TELEPHONY,
238                 ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
239                 true);
240     }
241 
persistBugreport()242     private void persistBugreport() {
243         if (isBugreportCollectionEnabled()) {
244             // TODO:
245         }
246     }
247 
shouldTrackCall(Call call)248     private boolean shouldTrackCall(Call call) {
249         return (call != null && call.isEmergencyCall() && call.isOutgoing());
250     }
251 
reportStuckCall(Call call)252     public void reportStuckCall(Call call) {
253         if (shouldTrackCall(call)) {
254             Log.i(this, "Triggering diagnostics for stuck call %s", call.getId());
255             triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED);
256             call.removeListener(this);
257             mEmergencyCallsMap.remove(call);
258         }
259     }
260 
261     @Override
onStartCreateConnection(Call call)262     public void onStartCreateConnection(Call call) {
263         if (shouldTrackCall(call)) {
264             long currentTime = mClockProxy.currentTimeMillis();
265             call.addListener(this);
266             Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime);
267             mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime));
268         }
269     }
270 
271     @Override
onCreateConnectionFailed(Call call)272     public void onCreateConnectionFailed(Call call) {
273         if (shouldTrackCall(call)) {
274             Log.i(this, "Triggering diagnostics for  call %s that was never added", call.getId());
275             triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED);
276             call.removeListener(this);
277             mEmergencyCallsMap.remove(call);
278         }
279     }
280 
281     /**
282      * Override of {@link CallsManagerListenerBase} to track when calls are removed
283      *
284      * @param call the call
285      */
286     @Override
onCallRemoved(Call call)287     public void onCallRemoved(Call call) {
288         if (call != null && (mEmergencyCallsMap.get(call) != null)) {
289             call.removeListener(this);
290 
291             CallEventTimestamps ts = mEmergencyCallsMap.get(call);
292             long currentTime = mClockProxy.currentTimeMillis();
293             ts.setCallRemovedTime(currentTime);
294 
295             maybeTriggerDiagnosticsCollection(call, ts);
296             mEmergencyCallsMap.remove(call);
297         }
298     }
299 
300     // !NOTE!: this method should only be called after we get onCallRemoved
maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts)301     private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) {
302         Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId());
303         boolean wentActive = (ts.getCallActiveTime() != 0);
304         long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0);
305         long timeSinceCallCreatedSec = getTotalCallTimeSec(ts);
306         int dc = removedCall.getDisconnectCause().getCode();
307 
308         if (wentActive) {
309             if (callActiveTimeSec
310                     < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) {
311                 // call connected but did not go on for long
312                 triggerDiagnosticsCollection(
313                         removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE);
314             }
315         } else {
316 
317             if (dc == DisconnectCause.LOCAL
318                     && timeSinceCallCreatedSec
319                     > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()
320                     / 1000) {
321                 // call was disconnected by the user (but not immediately)
322                 triggerDiagnosticsCollection(
323                         removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY);
324             } else if (dc != DisconnectCause.LOCAL) {
325                 // this can be a case for a full bugreport
326                 triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED);
327             }
328         }
329     }
330 
331     /**
332      * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
333      * call state changes.
334      *
335      * @param call     the call
336      * @param oldState its old state
337      * @param newState the new state
338      */
339     @Override
onCallStateChanged(Call call, int oldState, int newState)340     public void onCallStateChanged(Call call, int oldState, int newState) {
341 
342         CallEventTimestamps ts = mEmergencyCallsMap.get(call);
343         if (call != null && ts != null && newState == CallState.ACTIVE
344                 && ts.getCallActiveTime() == 0) {
345             long currentTime = mClockProxy.currentTimeMillis();
346             ts.setCallActiveTime(currentTime);
347         }
348     }
349 
dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw)350     private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) {
351         pw.increaseIndent();
352         pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
353         int totalEntriesDumped = 0;
354         long currentTime = mClockProxy.currentTimeMillis();
355         long entriesAfterTime =
356                 currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24
357                         * 60L * 60L * 1000L);
358         Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime);
359         DropBoxManager.Entry entry;
360         entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime);
361         while (entry != null) {
362             Log.i(this, "found entry with ts: %d", entry.getTimeMillis());
363             String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split(
364                     System.lineSeparator());
365             long entryTime = entry.getTimeMillis();
366             if (content != null) {
367                 pw.increaseIndent();
368                 pw.println("------------BEGIN ENTRY (" + entryTime + ")--------");
369                 for (String line : content) {
370                     pw.println(line);
371                 }
372                 pw.println("--------END ENTRY--------");
373                 pw.decreaseIndent();
374                 totalEntriesDumped++;
375             }
376             entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime);
377             if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) {
378                 /*
379                 Since Emergency calls are a rare/once in a lifetime time occurrence for most users,
380                 we should not be seeing too many entries. This code just guards against edge case
381                 like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in
382                 such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the
383                 bugreport
384 
385                 The Dropbox API in its current state does not allow to query Entries in reverse
386                 chronological order efficiently.
387                  */
388 
389                 Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped);
390                 break;
391             }
392         }
393         pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
394         pw.decreaseIndent();
395     }
396 
dump(IndentingPrintWriter pw, String[] args)397     public void dump(IndentingPrintWriter pw, String[] args) {
398         pw.increaseIndent();
399         mLocalLog.dump(pw);
400         pw.decreaseIndent();
401         if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) {
402             //dont read dropbox entries since this dump is triggered by telephony for diagnostics
403             Log.i(this, "skipped dumping diagnostic data");
404             return;
405         }
406         try {
407             dumpDiagnosticDataFromDropbox(pw);
408         } catch (Exception e) {
409             pw.println("Exception was thrown while dumping diagnostic data from DropBox");
410             e.printStackTrace();
411         }
412     }
413 
414     private static class CallEventTimestamps {
415 
416         private final long mCallCreatedTime;
417         private long mCallActiveTime;
418         private long mCallRemovedTime;
419 
CallEventTimestamps(long createdTime)420         public CallEventTimestamps(long createdTime) {
421             mCallCreatedTime = createdTime;
422         }
423 
getCallActiveTime()424         public long getCallActiveTime() {
425             return mCallActiveTime;
426         }
427 
setCallActiveTime(long callActiveTime)428         public void setCallActiveTime(long callActiveTime) {
429             this.mCallActiveTime = callActiveTime;
430         }
431 
getCallCreatedTime()432         public long getCallCreatedTime() {
433             return mCallCreatedTime;
434         }
435 
getCallRemovedTime()436         public long getCallRemovedTime() {
437             return mCallRemovedTime;
438         }
439 
setCallRemovedTime(long callRemovedTime)440         public void setCallRemovedTime(long callRemovedTime) {
441             this.mCallRemovedTime = callRemovedTime;
442         }
443     }
444 }
445