summaryrefslogtreecommitdiff
path: root/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
blob: af79da36a5c9d5500eb349cce578ae8dbce9bd59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.telecom;

import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;

import android.os.BugreportManager;
import android.os.DropBoxManager;
import android.provider.DeviceConfig;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telephony.TelephonyManager;
import android.util.LocalLog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

/**
 * The EmergencyCallDiagnosticsLogger monitors information required to diagnose potential outgoing
 * ecall failures on the device. When a potential failure is detected, it calls a Telephony API to
 * persist relevant information (dumpsys, logcat etc.) to the dropbox. This acts as a central place
 * to determine when and what to collect.
 *
 * <p>When a bugreport is triggered, this module will read the dropbox entries and add them to the
 * telecom dump.
 */
public class EmergencyCallDiagnosticLogger extends CallsManagerListenerBase
        implements Call.Listener {

    public static final int REPORT_REASON_RANGE_START = -1; //!!DO NOT CHANGE
    public static final int REPORT_REASON_RANGE_END = 5; //increment this and add new reason above
    public static final int COLLECTION_TYPE_BUGREPORT = 10;
    public static final int COLLECTION_TYPE_TELECOM_STATE = 11;
    public static final int COLLECTION_TYPE_TELEPHONY_STATE = 12;
    public static final int COLLECTION_TYPE_LOGCAT_BUFFERS = 13;
    private static final int REPORT_REASON_STUCK_CALL_DETECTED = 0;
    private static final int REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY = 1;
    private static final int REPORT_REASON_CALL_FAILED = 2;
    private static final int REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED = 3;
    private static final int REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE = 4;
    private static final String DROPBOX_TAG = "ecall_diagnostic_data";
    private static final String ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
            "enable_bugreport_collection_for_emergency_call_diagnostics";
    private static final String ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
            "enable_telecom_dump_collection_for_emergency_call_diagnostics";

    private static final String ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
            "enable_logcat_collection_for_emergency_call_diagnostics";
    private static final String ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
            "enable_telephony_dump_collection_for_emergency_call_diagnostics";

    private static final String DUMPSYS_ARG_FOR_DIAGNOSTICS = "EmergencyDiagnostics";

    // max text size to read from dropbox entry
    private static final int DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY = 500000;
    private static final String MAX_BYTES_PER_DROP_BOX_ENTRY = "max_bytes_per_dropbox_entry";
    private static final int MAX_DROPBOX_ENTRIES_TO_DUMP = 6;

    private final Timeouts.Adapter mTimeoutAdapter;
    // This map holds all calls, but keeps pruning non-emergency calls when we can determine it
    private final Map<Call, CallEventTimestamps> mEmergencyCallsMap = new ConcurrentHashMap<>(2);
    private final DropBoxManager mDropBoxManager;
    private final LocalLog mLocalLog = new LocalLog(10);
    private final TelephonyManager mTelephonyManager;
    private final BugreportManager mBugreportManager;
    private final Executor mAsyncTaskExecutor;
    private final ClockProxy mClockProxy;

    public EmergencyCallDiagnosticLogger(
            TelephonyManager tm,
            BugreportManager brm,
            Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager,
            Executor asyncTaskExecutor, ClockProxy clockProxy) {
        mTimeoutAdapter = timeoutAdapter;
        mDropBoxManager = dropBoxManager;
        mTelephonyManager = tm;
        mBugreportManager = brm;
        mAsyncTaskExecutor = asyncTaskExecutor;
        mClockProxy = clockProxy;
    }

    // this calculates time from ACTIVE --> removed
    private static long getCallTimeInActiveStateSec(CallEventTimestamps ts) {
        if (ts.getCallActiveTime() == 0 || ts.getCallRemovedTime() == 0) {
            return 0;
        } else {
            return (ts.getCallRemovedTime() - ts.getCallActiveTime()) / 1000;
        }
    }

    // this calculates time from call created --> removed
    private static long getTotalCallTimeSec(CallEventTimestamps ts) {
        if (ts.getCallRemovedTime() == 0 || ts.getCallCreatedTime() == 0) {
            return 0;
        } else {
            return (ts.getCallRemovedTime() - ts.getCallCreatedTime()) / 1000;
        }
    }

    //determines what to collect based on fail reason
    //if COLLECTION_TYPE_BUGREPORT is present in the returned list, then that
    //should be the only collection type in the list
    @VisibleForTesting
    public static List<Integer> getDataCollectionTypes(int reason) {
        switch (reason) {
            case REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE:
                return Arrays.asList(COLLECTION_TYPE_TELECOM_STATE);
            case REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED:
                return Arrays.asList(
                        COLLECTION_TYPE_TELECOM_STATE, COLLECTION_TYPE_TELEPHONY_STATE);
            case REPORT_REASON_CALL_FAILED:
            case REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY:
            case REPORT_REASON_STUCK_CALL_DETECTED:
                return Arrays.asList(
                        COLLECTION_TYPE_TELECOM_STATE,
                        COLLECTION_TYPE_TELEPHONY_STATE,
                        COLLECTION_TYPE_LOGCAT_BUFFERS);
            default:
        }
        return new ArrayList<>();
    }

    private int getMaxBytesPerDropboxEntry() {
        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
                MAX_BYTES_PER_DROP_BOX_ENTRY, DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY);
    }

    @VisibleForTesting
    public Map<Call, CallEventTimestamps> getEmergencyCallsMap() {
        return mEmergencyCallsMap;
    }

    private void triggerDiagnosticsCollection(Call call, int reason) {
        Log.i(this, "Triggering diagnostics for call %s reason: %d", call.getId(), reason);
        List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
        boolean invokeTelephonyPersistApi = false;
        CallEventTimestamps ts = mEmergencyCallsMap.get(call);
        EmergencyCallDiagnosticParams dp =
                new EmergencyCallDiagnosticParams();
        for (Integer dataCollectionType : dataCollectionTypes) {
            switch (dataCollectionType) {
                case COLLECTION_TYPE_TELECOM_STATE:
                    if (isTelecomDumpCollectionEnabled()) {
                        dp.setTelecomDumpSysCollection(true);
                        invokeTelephonyPersistApi = true;
                    }
                    break;
                case COLLECTION_TYPE_TELEPHONY_STATE:
                    if (isTelephonyDumpCollectionEnabled()) {
                        dp.setTelephonyDumpSysCollection(true);
                        invokeTelephonyPersistApi = true;
                    }
                    break;
                case COLLECTION_TYPE_LOGCAT_BUFFERS:
                    if (isLogcatCollectionEnabled()) {
                        dp.setLogcatCollection(true, ts.getCallCreatedTime());
                        invokeTelephonyPersistApi = true;
                    }
                    break;
                case COLLECTION_TYPE_BUGREPORT:
                    if (isBugreportCollectionEnabled()) {
                        mAsyncTaskExecutor.execute(new Runnable() {
                            @Override
                            public void run() {
                                persistBugreport();
                            }
                        });
                    }
                    break;
                default:
            }
        }
        if (invokeTelephonyPersistApi) {
            mAsyncTaskExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
                    try {
                        mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
                    } catch (Exception e) {
                        Log.w(this,
                                "Exception while invoking "
                                        + "Telephony#persistEmergencyCallDiagnosticData  %s",
                                e.toString());
                    }
                }
            });
        }
    }

    private boolean isBugreportCollectionEnabled() {
        return DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_TELEPHONY,
                ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
                false);
    }

    private boolean isTelecomDumpCollectionEnabled() {
        return DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_TELEPHONY,
                ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
                true);
    }

    private boolean isLogcatCollectionEnabled() {
        return DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_TELEPHONY,
                ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
                true);
    }

    private boolean isTelephonyDumpCollectionEnabled() {
        return DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_TELEPHONY,
                ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
                true);
    }

    private void persistBugreport() {
        if (isBugreportCollectionEnabled()) {
            // TODO:
        }
    }

    private boolean shouldTrackCall(Call call) {
        return (call != null && call.isEmergencyCall() && call.isOutgoing());
    }

    public void reportStuckCall(Call call) {
        if (shouldTrackCall(call)) {
            Log.i(this, "Triggering diagnostics for stuck call %s", call.getId());
            triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED);
            call.removeListener(this);
            mEmergencyCallsMap.remove(call);
        }
    }

    @Override
    public void onStartCreateConnection(Call call) {
        if (shouldTrackCall(call)) {
            long currentTime = mClockProxy.currentTimeMillis();
            call.addListener(this);
            Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime);
            mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime));
        }
    }

    @Override
    public void onCreateConnectionFailed(Call call) {
        if (shouldTrackCall(call)) {
            Log.i(this, "Triggering diagnostics for  call %s that was never added", call.getId());
            triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED);
            call.removeListener(this);
            mEmergencyCallsMap.remove(call);
        }
    }

    /**
     * Override of {@link CallsManagerListenerBase} to track when calls are removed
     *
     * @param call the call
     */
    @Override
    public void onCallRemoved(Call call) {
        if (call != null && (mEmergencyCallsMap.get(call) != null)) {
            call.removeListener(this);

            CallEventTimestamps ts = mEmergencyCallsMap.get(call);
            long currentTime = mClockProxy.currentTimeMillis();
            ts.setCallRemovedTime(currentTime);

            maybeTriggerDiagnosticsCollection(call, ts);
            mEmergencyCallsMap.remove(call);
        }
    }

    // !NOTE!: this method should only be called after we get onCallRemoved
    private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) {
        Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId());
        boolean wentActive = (ts.getCallActiveTime() != 0);
        long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0);
        long timeSinceCallCreatedSec = getTotalCallTimeSec(ts);
        int dc = removedCall.getDisconnectCause().getCode();

        if (wentActive) {
            if (callActiveTimeSec
                    < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) {
                // call connected but did not go on for long
                triggerDiagnosticsCollection(
                        removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE);
            }
        } else {

            if (dc == DisconnectCause.LOCAL
                    && timeSinceCallCreatedSec
                    > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()
                    / 1000) {
                // call was disconnected by the user (but not immediately)
                triggerDiagnosticsCollection(
                        removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY);
            } else if (dc != DisconnectCause.LOCAL) {
                // this can be a case for a full bugreport
                triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED);
            }
        }
    }

    /**
     * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
     * call state changes.
     *
     * @param call     the call
     * @param oldState its old state
     * @param newState the new state
     */
    @Override
    public void onCallStateChanged(Call call, int oldState, int newState) {

        if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) {
            CallEventTimestamps ts = mEmergencyCallsMap.get(call);
            if (ts != null) {
                long currentTime = mClockProxy.currentTimeMillis();
                ts.setCallActiveTime(currentTime);
            }
        }
    }

    private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) {
        pw.increaseIndent();
        pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
        int totalEntriesDumped = 0;
        long currentTime = mClockProxy.currentTimeMillis();
        long entriesAfterTime =
                currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24
                        * 60L * 60L * 1000L);
        Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime);
        DropBoxManager.Entry entry;
        entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime);
        while (entry != null) {
            Log.i(this, "found entry with ts: %d", entry.getTimeMillis());
            String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split(
                    System.lineSeparator());
            long entryTime = entry.getTimeMillis();
            if (content != null) {
                pw.increaseIndent();
                pw.println("------------BEGIN ENTRY (" + entryTime + ")--------");
                for (String line : content) {
                    pw.println(line);
                }
                pw.println("--------END ENTRY--------");
                pw.decreaseIndent();
                totalEntriesDumped++;
            }
            entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime);
            if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) {
                /*
                Since Emergency calls are a rare/once in a lifetime time occurrence for most users,
                we should not be seeing too many entries. This code just guards against edge case
                like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in
                such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the
                bugreport

                The Dropbox API in its current state does not allow to query Entries in reverse
                chronological order efficiently.
                 */

                Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped);
                break;
            }
        }
        pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
        pw.decreaseIndent();
    }

    public void dump(IndentingPrintWriter pw, String[] args) {
        pw.increaseIndent();
        mLocalLog.dump(pw);
        pw.decreaseIndent();
        if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) {
            //dont read dropbox entries since this dump is triggered by telephony for diagnostics
            Log.i(this, "skipped dumping diagnostic data");
            return;
        }
        dumpDiagnosticDataFromDropbox(pw);
    }

    private static class CallEventTimestamps {

        private final long mCallCreatedTime;
        private long mCallActiveTime;
        private long mCallRemovedTime;

        public CallEventTimestamps(long createdTime) {
            mCallCreatedTime = createdTime;
        }

        public long getCallActiveTime() {
            return mCallActiveTime;
        }

        public void setCallActiveTime(long callActiveTime) {
            this.mCallActiveTime = callActiveTime;
        }

        public long getCallCreatedTime() {
            return mCallCreatedTime;
        }

        public long getCallRemovedTime() {
            return mCallRemovedTime;
        }

        public void setCallRemovedTime(long callRemovedTime) {
            this.mCallRemovedTime = callRemovedTime;
        }
    }
}