1 /*
2  * Copyright (C) 2023 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.phone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.WorkerThread;
21 import android.os.DropBoxManager;
22 import android.os.SystemClock;
23 import android.os.TransactionTooLargeException;
24 import android.telephony.AnomalyReporter;
25 import android.telephony.TelephonyManager;
26 import android.util.Log;
27 
28 import java.io.BufferedReader;
29 import java.io.IOException;
30 import java.io.InputStreamReader;
31 import java.text.SimpleDateFormat;
32 import java.util.Arrays;
33 import java.util.Date;
34 import java.util.Locale;
35 import java.util.UUID;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.TimeUnit;
38 
39 /**
40  * A class to help collect dumpsys/logcat and persist it to the
41  * on-device dropbox service. It is purely a utility and does
42  * not make decisions on if/when to collect.
43  */
44 public class DiagnosticDataCollector {
45 
46     //error msg that is appended to output if cmd execution results in error
47     public static final String ERROR_MSG = "DiagnosticDataCollector error executing cmd";
48     private static final String TAG = "DDC";
49     private static final String[] TELECOM_DUMPSYS_COMMAND =
50             {"/system/bin/dumpsys", "telecom", "EmergencyDiagnostics"};
51     private static final String[] TELEPHONY_DUMPSYS_COMMAND =
52             {"/system/bin/dumpsys", "telephony.registry", "EmergencyDiagnostics"};
53     private static final String LOGCAT_BINARY =
54             "/system/bin/logcat";
55     private static final String LOGCAT_BUFFERS = "system,radio";
56     private static final long LOG_TIME_OFFSET_MILLIS = 75L;
57     private static final String DUMPSYS_BINARY = "/system/bin/dumpsys";
58     private final Runtime mJavaRuntime;
59     private final Executor mAsyncTaskExecutor;
60     private final DropBoxManager mDropBoxManager;
61     private final SimpleDateFormat mDateFormat = new SimpleDateFormat("MM-dd HH:mm:ss.mmm",
62             Locale.US);
63     private final boolean mIsLowRamDevice;
64     public static final UUID DROPBOX_TRANSACTION_TOO_LARGE_EXCEPTION =
65             UUID.fromString("ab27e97a-ef7b-11ed-a05b-0242ac120003");
66     public static final String DROPBOX_TRANSACTION_TOO_LARGE_MSG =
67             "DiagnosticDataCollector: transaction too large";
DiagnosticDataCollector(Runtime javaRuntime, Executor asyncTaskExecutor, DropBoxManager dropBoxManager, boolean isLowRamDevice)68     public DiagnosticDataCollector(Runtime javaRuntime, Executor asyncTaskExecutor,
69             DropBoxManager dropBoxManager, boolean isLowRamDevice) {
70         mJavaRuntime = javaRuntime;
71         mAsyncTaskExecutor = asyncTaskExecutor;
72         mDropBoxManager = dropBoxManager;
73         mIsLowRamDevice = isLowRamDevice;
74     }
75 
persistEmergencyDianosticData(@onNull DataCollectorConfig.Adapter dc, @NonNull TelephonyManager.EmergencyCallDiagnosticData ecdData, @NonNull String tag)76     public void persistEmergencyDianosticData(@NonNull DataCollectorConfig.Adapter dc,
77             @NonNull TelephonyManager.EmergencyCallDiagnosticData ecdData, @NonNull String tag) {
78 
79         if (ecdData.isTelephonyDumpsysCollectionEnabled()) {
80             persistTelephonyState(dc, tag);
81         }
82         if (ecdData.isTelecomDumpsysCollectionEnabled()) {
83             persistTelecomState(dc, tag);
84         }
85         if (ecdData.isLogcatCollectionEnabled()) {
86             persistLogcat(dc, tag, ecdData.getLogcatCollectionStartTimeMillis());
87         }
88     }
89 
90 
91     @SuppressWarnings("JavaUtilDate") //just used for DateFormatter.format (required by logcat)
persistLogcat(DataCollectorConfig.Adapter dc, String tag, long logcatStartTime)92     private void persistLogcat(DataCollectorConfig.Adapter dc, String tag, long logcatStartTime) {
93         String startTime = mDateFormat.format(new Date(logcatStartTime - LOG_TIME_OFFSET_MILLIS));
94         Log.d(TAG, "Persisting Logcat");
95         int maxLines;
96         if (mIsLowRamDevice) {
97             maxLines = dc.getMaxLogcatLinesForLowMemDevice();
98         } else {
99             maxLines = dc.getMaxLogcatLines();
100         }
101         DiagnosticRunnable dr = new DiagnosticRunnable(
102                 new String[]{LOGCAT_BINARY, "-t", startTime, "-b", LOGCAT_BUFFERS},
103                 dc.getLogcatReadTimeoutMillis(), dc.getLogcatProcTimeoutMillis(),
104                 tag, dc.getMaxLogcatLinesForLowMemDevice());
105         mAsyncTaskExecutor.execute(dr);
106     }
107 
persistTelecomState(DataCollectorConfig.Adapter dc, String tag)108     private void persistTelecomState(DataCollectorConfig.Adapter dc, String tag) {
109         Log.d(TAG, "Persisting Telecom state");
110         DiagnosticRunnable dr = new DiagnosticRunnable(TELECOM_DUMPSYS_COMMAND,
111                 dc.getDumpsysReadTimeoutMillis(), dc.getDumpsysProcTimeoutMillis(),
112                 tag, dc.getMaxLogcatLines());
113         mAsyncTaskExecutor.execute(dr);
114     }
115 
persistTelephonyState(DataCollectorConfig.Adapter dc, String tag)116     private void persistTelephonyState(DataCollectorConfig.Adapter dc, String tag) {
117         Log.d(TAG, "Persisting Telephony state");
118         DiagnosticRunnable dr = new DiagnosticRunnable(TELEPHONY_DUMPSYS_COMMAND,
119                 dc.getDumpsysReadTimeoutMillis(),
120                 dc.getDumpsysProcTimeoutMillis(),
121                 tag, dc.getMaxLogcatLines());
122         mAsyncTaskExecutor.execute(dr);
123     }
124 
125     private class DiagnosticRunnable implements Runnable {
126 
127         private static final String TAG = "DDC-DiagnosticRunnable";
128         private final String[] mCmd;
129         private final String mDropBoxTag;
130         private final int mMaxLogcatLines;
131         private long mStreamTimeout;
132         private long mProcTimeout;
133 
DiagnosticRunnable(String[] cmd, long streamTimeout, long procTimeout, String dropboxTag, int maxLogcatLines)134         DiagnosticRunnable(String[] cmd, long streamTimeout, long procTimeout, String dropboxTag,
135                 int maxLogcatLines) {
136             mCmd = cmd;
137             mStreamTimeout = streamTimeout;
138             mProcTimeout = procTimeout;
139             mDropBoxTag = dropboxTag;
140             mMaxLogcatLines = maxLogcatLines;
141             Log.d(TAG, "Runnable created with cmd: " + Arrays.toString(cmd));
142         }
143 
144         @Override
145         @WorkerThread
run()146         public void run() {
147             Log.d(TAG, "Running async persist for tag" + mDropBoxTag);
148             getProcOutputAndPersist(mCmd,
149                     mStreamTimeout, mProcTimeout, mDropBoxTag, mMaxLogcatLines);
150         }
151 
152         @WorkerThread
getProcOutputAndPersist(String[] cmd, long streamTimeout, long procTimeout, String dropboxTag, int maxLogcatLines)153         private void getProcOutputAndPersist(String[] cmd, long streamTimeout, long procTimeout,
154                 String dropboxTag, int maxLogcatLines) {
155             Process process = null;
156             StringBuilder output = new StringBuilder();
157             long startProcTime = SystemClock.elapsedRealtime();
158             int outputSizeFromErrorStream = 0;
159             try {
160                 process = mJavaRuntime.exec(cmd);
161                 readStreamLinesWithTimeout(
162                         new BufferedReader(new InputStreamReader(process.getInputStream())), output,
163                         streamTimeout, maxLogcatLines);
164                 int outputSizeFromInputStream = output.length();
165                 readStreamLinesWithTimeout(
166                         new BufferedReader(new InputStreamReader(process.getErrorStream())), output,
167                         streamTimeout, maxLogcatLines);
168                 Log.d(TAG, "[" + cmd[0] + "]" + "streams read in " + (SystemClock.elapsedRealtime()
169                         - startProcTime) + " milliseconds");
170                 process.waitFor(procTimeout, TimeUnit.MILLISECONDS);
171                 outputSizeFromErrorStream = output.length() - outputSizeFromInputStream;
172             } catch (InterruptedException e) {
173                 output.append(ERROR_MSG + e.toString() + System.lineSeparator());
174             } catch (IOException e) {
175                 output.append(ERROR_MSG + e.toString() + System.lineSeparator());
176             } finally {
177                 if (process != null) {
178                     process.destroy();
179                 }
180             }
181             Log.d(TAG, "[" + cmd[0] + "]" + "output collected in " + (SystemClock.elapsedRealtime()
182                     - startProcTime) + " milliseconds. Size:" + output.toString().length());
183             if (outputSizeFromErrorStream > 0) {
184                 Log.w(TAG, "Cmd ran with errors");
185                 output.append(ERROR_MSG + System.lineSeparator());
186             }
187             try {
188                 mDropBoxManager.addText(dropboxTag, output.toString());
189             } catch (Exception e) {
190                 if (e instanceof TransactionTooLargeException) {
191                     AnomalyReporter.reportAnomaly(
192                             DROPBOX_TRANSACTION_TOO_LARGE_EXCEPTION,
193                             DROPBOX_TRANSACTION_TOO_LARGE_MSG);
194                 }
195                 Log.w(TAG, "Exception while writing to Dropbox " + e);
196             }
197         }
198 
199         @WorkerThread
readStreamLinesWithTimeout( BufferedReader inReader, StringBuilder outLines, long timeout, int maxLines)200         private void readStreamLinesWithTimeout(
201                 BufferedReader inReader, StringBuilder outLines, long timeout, int maxLines)
202                 throws IOException {
203             long startTimeMs = SystemClock.elapsedRealtime();
204             int totalLines = 0;
205             while (SystemClock.elapsedRealtime() < startTimeMs + timeout) {
206                 // If there is a burst of data, continue reading without checking for timeout.
207                 while (inReader.ready() && (totalLines < maxLines)) {
208                     String line = inReader.readLine();
209                     if (line == null) return; // end of stream.
210                     outLines.append(line);
211                     totalLines++;
212                     outLines.append(System.lineSeparator());
213                 }
214                 SystemClock.sleep(timeout / 10);
215             }
216         }
217     }
218 }
219