diff options
4 files changed, 412 insertions, 0 deletions
diff --git a/core/java/android/telephony/DropBoxManagerLoggerBackend.java b/core/java/android/telephony/DropBoxManagerLoggerBackend.java new file mode 100644 index 000000000000..41f81acd1af3 --- /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 000000000000..47d15e57f073 --- /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 000000000000..e3e72e19e418 --- /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 16f0f3221939..192643b00b59 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> |