Merge changes from topic "ecall_diagnostics" into udc-dev

* changes:
  Impl to persist dumpsys/logcat in Telephony
  Telephony method for diagnostics data
diff --git a/src/com/android/phone/DataCollectorConfig.java b/src/com/android/phone/DataCollectorConfig.java
new file mode 100644
index 0000000..00f2fce
--- /dev/null
+++ b/src/com/android/phone/DataCollectorConfig.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 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.phone;
+
+import android.provider.DeviceConfig;
+
+public final class DataCollectorConfig {
+    public static final long LOGCAT_READ_TIMEOUT_MILLIS_VALUE = 500L;
+    public static final long DUMPSYS_READ_TIMEOUT_MILLIS_VALUE = 100L;
+    public static final long LOGCAT_PROC_TIMEOUT_MILLIS_VALUE = 500L;
+    public static final long DUMPSYS_PROC_TIMEOUT_MILLIS_VALUE = 100L;
+    public static final int MAX_LOGCAT_LINES_LOW_MEM_DEVICE_VALUE = 2000;
+    public static final int MAX_LOGCAT_LINES_VALUE = 8000;
+    private static String LOGCAT_READ_TIMEOUT_MILLIS = "logcat_read_timeout_millis";
+    private static String DUMPSYS_READ_TIMEOUT_MILLIS = "dumpsys_read_timeout_millis";
+    private static String LOGCAT_PROC_TIMEOUT_MILLIS = "logcat_proc_timeout_millis";
+    private static String DUMPSYS_PROC_TIMEOUT_MILLIS = "dumpsys_proc_timeout_millis";
+    private static String MAX_LOGCAT_LINES_LOW_MEM = "max_logcat_lines_low_mem";
+    private static String MAX_LOGCAT_LINES = "max_logcat_lines";
+
+    public static int getMaxLogcatLinesForLowMemDevice() {
+        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+                MAX_LOGCAT_LINES_LOW_MEM, MAX_LOGCAT_LINES_LOW_MEM_DEVICE_VALUE);
+    }
+
+    public static int getMaxLogcatLines() {
+        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+                MAX_LOGCAT_LINES, MAX_LOGCAT_LINES_VALUE);
+    }
+
+    public static long getLogcatReadTimeoutMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                LOGCAT_READ_TIMEOUT_MILLIS, LOGCAT_READ_TIMEOUT_MILLIS_VALUE);
+    }
+
+    public static long getDumpsysReadTimeoutMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                DUMPSYS_READ_TIMEOUT_MILLIS, DUMPSYS_READ_TIMEOUT_MILLIS_VALUE);
+    }
+
+    public static long getLogcatProcTimeoutMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                LOGCAT_PROC_TIMEOUT_MILLIS, LOGCAT_PROC_TIMEOUT_MILLIS_VALUE);
+    }
+
+    public static long getDumpsysProcTimeoutMillis() {
+        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+                DUMPSYS_PROC_TIMEOUT_MILLIS, DUMPSYS_PROC_TIMEOUT_MILLIS_VALUE);
+    }
+
+    public static class Adapter {
+        public Adapter() {
+        }
+
+        public int getMaxLogcatLinesForLowMemDevice() {
+            return DataCollectorConfig.getMaxLogcatLinesForLowMemDevice();
+        }
+
+        public int getMaxLogcatLines() {
+            return DataCollectorConfig.getMaxLogcatLines();
+        }
+
+        public long getLogcatReadTimeoutMillis() {
+            return DataCollectorConfig.getLogcatReadTimeoutMillis();
+        }
+
+        public long getDumpsysReadTimeoutMillis() {
+            return DataCollectorConfig.getDumpsysReadTimeoutMillis();
+        }
+
+        public long getLogcatProcTimeoutMillis() {
+            return DataCollectorConfig.getLogcatProcTimeoutMillis();
+        }
+
+        public long getDumpsysProcTimeoutMillis() {
+            return DataCollectorConfig.getDumpsysProcTimeoutMillis();
+        }
+    }
+
+
+}
diff --git a/src/com/android/phone/DiagnosticDataCollector.java b/src/com/android/phone/DiagnosticDataCollector.java
new file mode 100644
index 0000000..d7ebe3d
--- /dev/null
+++ b/src/com/android/phone/DiagnosticDataCollector.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 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.phone;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.os.DropBoxManager;
+import android.os.SystemClock;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class to help collect dumpsys/logcat and persist it to the
+ * on-device dropbox service. It is purely a utility and does
+ * not make decisions on if/when to collect.
+ */
+public class DiagnosticDataCollector {
+
+    //error msg that is appended to output if cmd execution results in error
+    public static final String ERROR_MSG = "DiagnosticDataCollector error executing cmd";
+    private static final String TAG = "DDC";
+    private static final String[] TELECOM_DUMPSYS_COMMAND =
+            {"/system/bin/dumpsys", "telecom", "EmergencyDiagnostics"};
+    private static final String[] TELEPHONY_DUMPSYS_COMMAND =
+            {"/system/bin/dumpsys", "telephony.registry", "EmergencyDiagnostics"};
+    private static final String LOGCAT_BINARY =
+            "/system/bin/logcat";
+    private static final String LOGCAT_BUFFERS = "system,radio";
+    private static final long LOG_TIME_OFFSET_MILLIS = 75L;
+    private static final String DUMPSYS_BINARY = "/system/bin/dumpsys";
+    private final Runtime mJavaRuntime;
+    private final Executor mAsyncTaskExecutor;
+    private final DropBoxManager mDropBoxManager;
+    private final SimpleDateFormat mDateFormat = new SimpleDateFormat("MM-dd HH:mm:ss.mmm",
+            Locale.US);
+    private final boolean mIsLowRamDevice;
+
+    public DiagnosticDataCollector(Runtime javaRuntime, Executor asyncTaskExecutor,
+            DropBoxManager dropBoxManager, boolean isLowRamDevice) {
+        mJavaRuntime = javaRuntime;
+        mAsyncTaskExecutor = asyncTaskExecutor;
+        mDropBoxManager = dropBoxManager;
+        mIsLowRamDevice = isLowRamDevice;
+    }
+
+    public void persistEmergencyDianosticData(@NonNull DataCollectorConfig.Adapter dc,
+            @NonNull TelephonyManager.EmergencyCallDiagnosticParams edp, @NonNull String tag) {
+
+        if (edp.isTelephonyDumpSysCollectionEnabled()) {
+            persistTelephonyState(dc, tag);
+        }
+        if (edp.isTelecomDumpSysCollectionEnabled()) {
+            persistTelecomState(dc, tag);
+        }
+        if (edp.isLogcatCollectionEnabled()) {
+            persistLogcat(dc, tag, edp.getLogcatStartTime());
+        }
+    }
+
+
+    @SuppressWarnings("JavaUtilDate") //just used for DateFormatter.format (required by logcat)
+    private void persistLogcat(DataCollectorConfig.Adapter dc, String tag, long logcatStartTime) {
+        String startTime = mDateFormat.format(new Date(logcatStartTime - LOG_TIME_OFFSET_MILLIS));
+        Log.d(TAG, "Persisting Logcat");
+        int maxLines;
+        if (mIsLowRamDevice) {
+            maxLines = dc.getMaxLogcatLinesForLowMemDevice();
+        } else {
+            maxLines = dc.getMaxLogcatLines();
+        }
+        DiagnosticRunnable dr = new DiagnosticRunnable(
+                new String[]{LOGCAT_BINARY, "-t", startTime, "-b", LOGCAT_BUFFERS},
+                dc.getLogcatReadTimeoutMillis(), dc.getLogcatProcTimeoutMillis(),
+                tag, dc.getMaxLogcatLinesForLowMemDevice());
+        mAsyncTaskExecutor.execute(dr);
+    }
+
+    private void persistTelecomState(DataCollectorConfig.Adapter dc, String tag) {
+        Log.d(TAG, "Persisting Telecom state");
+        DiagnosticRunnable dr = new DiagnosticRunnable(TELECOM_DUMPSYS_COMMAND,
+                dc.getDumpsysReadTimeoutMillis(), dc.getDumpsysProcTimeoutMillis(),
+                tag, dc.getMaxLogcatLines());
+        mAsyncTaskExecutor.execute(dr);
+    }
+
+    private void persistTelephonyState(DataCollectorConfig.Adapter dc, String tag) {
+        Log.d(TAG, "Persisting Telephony state");
+        DiagnosticRunnable dr = new DiagnosticRunnable(TELEPHONY_DUMPSYS_COMMAND,
+                dc.getDumpsysReadTimeoutMillis(),
+                dc.getDumpsysProcTimeoutMillis(),
+                tag, dc.getMaxLogcatLines());
+        mAsyncTaskExecutor.execute(dr);
+    }
+
+    private class DiagnosticRunnable implements Runnable {
+
+        private static final String TAG = "DDC-DiagnosticRunnable";
+        private final String[] mCmd;
+        private final String mDropBoxTag;
+        private final int mMaxLogcatLines;
+        private long mStreamTimeout;
+        private long mProcTimeout;
+
+        DiagnosticRunnable(String[] cmd, long streamTimeout, long procTimeout, String dropboxTag,
+                int maxLogcatLines) {
+            mCmd = cmd;
+            mStreamTimeout = streamTimeout;
+            mProcTimeout = procTimeout;
+            mDropBoxTag = dropboxTag;
+            mMaxLogcatLines = maxLogcatLines;
+            Log.d(TAG, "Runnable created with cmd: " + Arrays.toString(cmd));
+        }
+
+        @Override
+        @WorkerThread
+        public void run() {
+            Log.d(TAG, "Running async persist for tag" + mDropBoxTag);
+            getProcOutputAndPersist(mCmd,
+                    mStreamTimeout, mProcTimeout, mDropBoxTag, mMaxLogcatLines);
+        }
+
+        @WorkerThread
+        private void getProcOutputAndPersist(String[] cmd, long streamTimeout, long procTimeout,
+                String dropboxTag, int maxLogcatLines) {
+            Process process = null;
+            StringBuilder output = new StringBuilder();
+            long startProcTime = SystemClock.elapsedRealtime();
+            int outputSizeFromErrorStream = 0;
+            try {
+                process = mJavaRuntime.exec(cmd);
+                readStreamLinesWithTimeout(
+                        new BufferedReader(new InputStreamReader(process.getInputStream())), output,
+                        streamTimeout, maxLogcatLines);
+                int outputSizeFromInputStream = output.length();
+                readStreamLinesWithTimeout(
+                        new BufferedReader(new InputStreamReader(process.getErrorStream())), output,
+                        streamTimeout, maxLogcatLines);
+                Log.d(TAG, "[" + cmd[0] + "]" + "streams read in " + (SystemClock.elapsedRealtime()
+                        - startProcTime) + " milliseconds");
+                process.waitFor(procTimeout, TimeUnit.MILLISECONDS);
+                outputSizeFromErrorStream = output.length() - outputSizeFromInputStream;
+            } catch (InterruptedException e) {
+                output.append(ERROR_MSG + e.toString() + System.lineSeparator());
+            } catch (IOException e) {
+                output.append(ERROR_MSG + e.toString() + System.lineSeparator());
+            } finally {
+                if (process != null) {
+                    process.destroy();
+                }
+            }
+            Log.d(TAG, "[" + cmd[0] + "]" + "output collected in " + (SystemClock.elapsedRealtime()
+                    - startProcTime) + " milliseconds. Size:" + output.toString().length());
+            if (outputSizeFromErrorStream > 0) {
+                Log.w(TAG, "Cmd ran with errors");
+                output.append(ERROR_MSG + System.lineSeparator());
+            }
+            mDropBoxManager.addText(dropboxTag, output.toString());
+        }
+
+        @WorkerThread
+        private void readStreamLinesWithTimeout(
+                BufferedReader inReader, StringBuilder outLines, long timeout, int maxLines)
+                throws IOException {
+            long startTimeMs = SystemClock.elapsedRealtime();
+            int totalLines = 0;
+            while (SystemClock.elapsedRealtime() < startTimeMs + timeout) {
+                // If there is a burst of data, continue reading without checking for timeout.
+                while (inReader.ready() && (totalLines < maxLines)) {
+                    String line = inReader.readLine();
+                    if (line == null) return; // end of stream.
+                    outLines.append(line);
+                    totalLines++;
+                    outLines.append(System.lineSeparator());
+                }
+                SystemClock.sleep(timeout / 10);
+            }
+        }
+    }
+}
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 642250b..4d4abb5 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -51,6 +51,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.DropBoxManager;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
@@ -12028,6 +12029,49 @@
         return simState.ordinal();
     }
 
+    private void persistEmergencyCallDiagnosticDataInternal(@NonNull String dropboxTag,
+            boolean enableLogcat,
+            long logcatStartTimestampMillis, boolean enableTelecomDump,
+            boolean enableTelephonyDump) {
+        DropBoxManager db = mApp.getSystemService(DropBoxManager.class);
+        TelephonyManager.EmergencyCallDiagnosticParams edp =
+                new TelephonyManager.EmergencyCallDiagnosticParams();
+        edp.setLogcatCollection(enableLogcat, logcatStartTimestampMillis);
+        edp.setTelephonyDumpSysCollection(enableTelephonyDump);
+        edp.setTelecomDumpSysCollection(enableTelecomDump);
+        Log.d(LOG_TAG, "persisting with Params " + edp.toString());
+        DiagnosticDataCollector ddc = new DiagnosticDataCollector(Runtime.getRuntime(),
+                Executors.newCachedThreadPool(), db,
+                mApp.getSystemService(ActivityManager.class).isLowRamDevice());
+        ddc.persistEmergencyDianosticData(new DataCollectorConfig.Adapter(), edp, dropboxTag);
+    }
+
+    /**
+     * Request telephony to persist state for debugging emergency call failures.
+     *
+     * @param dropBoxTag                 Tag to use when persisting data to dropbox service.
+     * @param enableLogcat               whether to collect logcat output
+     * @param logcatStartTimestampMillis timestamp from when logcat buffers would be persisted
+     * @param enableTelecomDump          whether to collect telecom dumpsys
+     * @param enableTelephonyDump        whether to collect telephony dumpsys
+     */
+    @Override
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    public void persistEmergencyCallDiagnosticData(@NonNull String dropboxTag, boolean enableLogcat,
+            long logcatStartTimestampMillis, boolean enableTelecomDump,
+            boolean enableTelephonyDump) {
+        mApp.enforceCallingPermission(android.Manifest.permission.DUMP,
+                "persistEmergencyCallDiagnosticData");
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            persistEmergencyCallDiagnosticDataInternal(dropboxTag, enableLogcat,
+                    logcatStartTimestampMillis, enableTelecomDump, enableTelephonyDump);
+
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
     /**
      * Get current cell broadcast ranges.
      */
diff --git a/tests/src/com/android/phone/DiagnosticDataCollectorTest.java b/tests/src/com/android/phone/DiagnosticDataCollectorTest.java
new file mode 100644
index 0000000..e0d89bc
--- /dev/null
+++ b/tests/src/com/android/phone/DiagnosticDataCollectorTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 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.phone;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.DropBoxManager;
+import android.telephony.TelephonyManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Unit Tests for DiagnosticDataCollector.
+ */
+@RunWith(JUnit4.class)
+public class DiagnosticDataCollectorTest {
+
+    private static final String[] TELECOM_DUMPSYS_COMMAND =
+            {"/system/bin/dumpsys", "telecom", "EmergencyDiagnostics"};
+    private static final String[] TELEPHONY_DUMPSYS_COMMAND =
+            {"/system/bin/dumpsys", "telephony.registry", "EmergencyDiagnostics"};
+    private static final String[] LOGCAT_BINARY = {"/system/bin/logcat"};
+
+
+    @Mock
+    DataCollectorConfig.Adapter mConfig;
+    private Runtime mRuntime;
+
+    @Mock
+    private DropBoxManager mDropBoxManager;
+
+    private DiagnosticDataCollector mDiagnosticDataCollector;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mRuntime = spy(Runtime.getRuntime());
+        mDiagnosticDataCollector = new DiagnosticDataCollector(mRuntime, Runnable::run,
+                mDropBoxManager, false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    private void verifyCmdAndDropboxTag(String[] cmd, String tag, boolean startsWithMatch)
+            throws InterruptedException, IOException {
+        ArgumentCaptor<String[]> textArrayCaptor = ArgumentCaptor.forClass(String[].class);
+
+        //verify cmd passed to runtime
+        verify(mRuntime).exec(textArrayCaptor.capture());
+        String[] argList = textArrayCaptor.getValue();
+        if (startsWithMatch) {
+            assertEquals(cmd[0], argList[0]);
+        } else {
+            assertEquals(Arrays.toString(cmd), Arrays.toString(argList));
+        }
+        ArgumentCaptor<String> textCaptor = ArgumentCaptor.forClass(String.class);
+
+        //make sure logcat output does not have errors
+        verify(mDropBoxManager, times(1)).addText(eq(tag), textCaptor.capture());
+        assertFalse(textCaptor.getValue().contains(DiagnosticDataCollector.ERROR_MSG));
+    }
+
+    @Test
+    public void testPersistForTelecomDumpsys() throws IOException, InterruptedException {
+        TelephonyManager.EmergencyCallDiagnosticParams dp =
+                new TelephonyManager.EmergencyCallDiagnosticParams();
+        dp.setTelecomDumpSysCollection(true);
+        mDiagnosticDataCollector.persistEmergencyDianosticData(mConfig, dp, "test_tag_telecom");
+
+        verifyCmdAndDropboxTag(TELECOM_DUMPSYS_COMMAND, "test_tag_telecom", false);
+    }
+
+    @Test
+    public void testPersistForTelephonyDumpsys() throws IOException, InterruptedException {
+        TelephonyManager.EmergencyCallDiagnosticParams dp =
+                new TelephonyManager.EmergencyCallDiagnosticParams();
+        dp.setTelephonyDumpSysCollection(true);
+        mDiagnosticDataCollector.persistEmergencyDianosticData(mConfig, dp, "test_tag_telephony");
+
+        verifyCmdAndDropboxTag(TELEPHONY_DUMPSYS_COMMAND, "test_tag_telephony", false);
+    }
+
+    @Test
+    public void testPersistForLogcat() throws IOException, InterruptedException {
+        TelephonyManager.EmergencyCallDiagnosticParams dp =
+                new TelephonyManager.EmergencyCallDiagnosticParams();
+        dp.setLogcatCollection(true, System.currentTimeMillis());
+        mDiagnosticDataCollector.persistEmergencyDianosticData(mConfig, dp, "test_tag_logcat");
+
+        verifyCmdAndDropboxTag(LOGCAT_BINARY, "test_tag_logcat", true);
+    }
+
+}