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