Create persistent logger for Satellite SOS

1. Create persistent logging classes with default DropBoxManager backend
2. Add resource config to enable usage of the DropBoxManager logger

Bug: 343478281
Test: make & manual tests
Change-Id: Ibb7d8cb48dee8a574eda625bdd82d3d6e6f51fa3
Merged-In: Ibb7d8cb48dee8a574eda625bdd82d3d6e6f51fa3
diff --git a/core/java/android/telephony/DropBoxManagerLoggerBackend.java b/core/java/android/telephony/DropBoxManagerLoggerBackend.java
new file mode 100644
index 0000000..41f81ac
--- /dev/null
+++ b/core/java/android/telephony/DropBoxManagerLoggerBackend.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2024 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 android.telephony;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.DropBoxManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+
+/**
+ * A persistent logger backend that stores logs in Android DropBoxManager
+ *
+ * @hide
+ */
+public class DropBoxManagerLoggerBackend implements PersistentLoggerBackend {
+
+    private static final String TAG = "DropBoxManagerLoggerBackend";
+    // Separate tag reference to be explicitly used for dropboxmanager instead of logcat logging
+    private static final String DROPBOX_TAG = "DropBoxManagerLoggerBackend";
+    private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER =
+            DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS");
+    private static final ZoneId LOCAL_ZONE_ID = ZoneId.systemDefault();
+    private static final int BUFFER_SIZE_BYTES = 500 * 1024; // 500 KB
+    private static final int MIN_BUFFER_BYTES_FOR_FLUSH = 5 * 1024; // 5 KB
+
+    private static DropBoxManagerLoggerBackend sInstance;
+
+    private final DropBoxManager mDropBoxManager;
+    private final Object mBufferLock = new Object();
+    @GuardedBy("mBufferLock")
+    private final StringBuilder mLogBuffer = new StringBuilder();
+    private long mBufferStartTime = -1L;
+    private final HandlerThread mHandlerThread = new HandlerThread(DROPBOX_TAG);
+    private final Handler mHandler;
+    // Flag for determining if logging is enabled as a general feature
+    private final boolean mDropBoxManagerLoggingEnabled;
+
+    /**
+     * Returns a singleton instance of {@code DropBoxManagerLoggerBackend} that will log to
+     * DropBoxManager if the config_dropboxmanager_persistent_logging_enabled resource config is
+     * enabled.
+     * @param context Android context
+     */
+    @Nullable
+    public static synchronized DropBoxManagerLoggerBackend getInstance(@NonNull Context context) {
+        if (sInstance == null) {
+            sInstance = new DropBoxManagerLoggerBackend(context);
+        }
+        return sInstance;
+    }
+
+    private DropBoxManagerLoggerBackend(@NonNull Context context) {
+        mDropBoxManager = context.getSystemService(DropBoxManager.class);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mDropBoxManagerLoggingEnabled = persistentLoggingEnabled(context);
+    }
+
+    private boolean persistentLoggingEnabled(@NonNull Context context) {
+        try {
+            return context.getResources().getBoolean(
+                    R.bool.config_dropboxmanager_persistent_logging_enabled);
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Persistent logging config not found");
+            return false;
+        }
+    }
+
+    /**
+     * Persist a DEBUG log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void debug(@NonNull String tag, @NonNull String msg) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("D", tag, msg, Optional.empty());
+    }
+
+    /**
+     * Persist a INFO log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void info(@NonNull String tag, @NonNull String msg) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("I", tag, msg, Optional.empty());
+    }
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void warn(@NonNull String tag, @NonNull String msg) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("W", tag, msg, Optional.empty());
+    }
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    public void warn(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("W", tag, msg, Optional.of(t));
+    }
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void error(@NonNull String tag, @NonNull String msg) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("E", tag, msg, Optional.empty());
+    }
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    public void error(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+        bufferLog("E", tag, msg, Optional.of(t));
+    }
+
+    private synchronized void bufferLog(
+            @NonNull String level,
+            @NonNull String tag,
+            @NonNull String msg,
+            Optional<Throwable> t) {
+        if (mBufferStartTime == -1L) {
+            mBufferStartTime = System.currentTimeMillis();
+        }
+
+        synchronized (mBufferLock) {
+            mLogBuffer
+                    .append(formatLog(level, tag, msg, t))
+                    .append("\n");
+
+            if (mLogBuffer.length() >= BUFFER_SIZE_BYTES) {
+                flushAsync();
+            }
+        }
+    }
+
+    private String formatLog(
+            @NonNull String level,
+            @NonNull String tag,
+            @NonNull String msg,
+            Optional<Throwable> t) {
+        // Expected format = "$Timestamp $Level $Tag: $Message"
+        return formatTimestamp(System.currentTimeMillis()) + " " + level + " " + tag + ": "
+                + t.map(throwable -> msg + ": " + Log.getStackTraceString(throwable)).orElse(msg);
+    }
+
+    private String formatTimestamp(long currentTimeMillis) {
+        return Instant.ofEpochMilli(currentTimeMillis)
+                .atZone(LOCAL_ZONE_ID)
+                .format(LOG_TIMESTAMP_FORMATTER);
+    }
+
+    /**
+     * Flushes all buffered logs into DropBoxManager as a single log record with a tag of
+     * {@link #DROPBOX_TAG} asynchronously. Should be invoked sparingly as DropBoxManager has
+     * device-level limitations on the number files that can be stored.
+     */
+    public void flushAsync() {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+
+        mHandler.post(this::flush);
+    };
+
+    /**
+     * Flushes all buffered logs into DropBoxManager as a single log record with a tag of
+     * {@link #DROPBOX_TAG}. Should be invoked sparingly as DropBoxManager has device-level
+     * limitations on the number files that can be stored.
+     */
+    public void flush() {
+        if (!mDropBoxManagerLoggingEnabled) {
+            return;
+        }
+
+        synchronized (mBufferLock) {
+            if (mLogBuffer.length() < MIN_BUFFER_BYTES_FOR_FLUSH) {
+                return;
+            }
+
+            Log.d(DROPBOX_TAG, "Flushing logs from "
+                    + formatTimestamp(mBufferStartTime) + " to "
+                    + formatTimestamp(System.currentTimeMillis()));
+
+            try {
+                mDropBoxManager.addText(DROPBOX_TAG, mLogBuffer.toString());
+            } catch (Exception e) {
+                Log.w(DROPBOX_TAG, "Failed to flush logs of length "
+                        + mLogBuffer.length() + " to DropBoxManager", e);
+            }
+            mLogBuffer.setLength(0);
+        }
+        mBufferStartTime = -1L;
+    }
+}
diff --git a/core/java/android/telephony/PersistentLogger.java b/core/java/android/telephony/PersistentLogger.java
new file mode 100644
index 0000000..47d15e5
--- /dev/null
+++ b/core/java/android/telephony/PersistentLogger.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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 android.telephony;
+
+import android.annotation.NonNull;
+
+/**
+ * A persistent logging client. Intended for persisting SOS/emergency related critical debug logs
+ * in situations where standard Android logcat logs may not be retained long enough or available
+ * for access.
+ *
+ * @hide
+ */
+public class PersistentLogger {
+    private final PersistentLoggerBackend mPersistentLoggerBackend;
+
+    public PersistentLogger(@NonNull PersistentLoggerBackend persistentLoggerBackend) {
+        mPersistentLoggerBackend = persistentLoggerBackend;
+    }
+
+    /**
+     * Persist a DEBUG log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void debug(@NonNull String tag, @NonNull String msg) {
+        mPersistentLoggerBackend.debug(tag, msg);
+    }
+
+    /**
+     * Persist a INFO log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void info(@NonNull String tag, @NonNull String msg) {
+        mPersistentLoggerBackend.info(tag, msg);
+    }
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void warn(@NonNull String tag, @NonNull String msg) {
+        mPersistentLoggerBackend.warn(tag, msg);
+    }
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    public void warn(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) {
+        mPersistentLoggerBackend.warn(tag, msg, t);
+    }
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    public void error(@NonNull String tag, @NonNull String msg) {
+        mPersistentLoggerBackend.error(tag, msg);
+    }
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    public void error(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) {
+        mPersistentLoggerBackend.error(tag, msg, t);
+    }
+}
diff --git a/core/java/android/telephony/PersistentLoggerBackend.java b/core/java/android/telephony/PersistentLoggerBackend.java
new file mode 100644
index 0000000..e3e72e1
--- /dev/null
+++ b/core/java/android/telephony/PersistentLoggerBackend.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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 android.telephony;
+
+import android.annotation.NonNull;
+
+/**
+ * Interface for logging backends to provide persistent log storage.
+ *
+ * @hide
+ */
+public interface PersistentLoggerBackend {
+
+    /**
+     * Persist a DEBUG log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    void debug(@NonNull String tag, @NonNull String msg);
+
+    /**
+     * Persist a INFO log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    void info(@NonNull String tag, @NonNull String msg);
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    void warn(@NonNull String tag, @NonNull String msg);
+
+    /**
+     * Persist a WARN log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    void warn(@NonNull String tag, @NonNull String msg, @NonNull Throwable t);
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     */
+    void error(@NonNull String tag, @NonNull String msg);
+
+    /**
+     * Persist a ERROR log message.
+     * @param tag Used to identify the source of a log message.
+     * @param msg The message you would like logged.
+     * @param t An exception to log.
+     */
+    void error(@NonNull String tag, @NonNull String msg, @NonNull Throwable t);
+}
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 16f0f32..192643b 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -392,4 +392,9 @@
     <integer name="config_wait_for_datagram_sending_response_for_last_message_timeout_millis">60000</integer>
     <java-symbol type="integer" name="config_wait_for_datagram_sending_response_for_last_message_timeout_millis" />
 
+    <!-- Boolean indicating whether to enable persistent logging via DropBoxManager.
+     Used in persisting SOS/emergency related log messages.
+     -->
+    <bool name="config_dropboxmanager_persistent_logging_enabled">false</bool>
+    <java-symbol type="bool" name="config_dropboxmanager_persistent_logging_enabled" />
 </resources>