1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-23 08:43:15 +02:00

Enhanced LogManager with Markdown formatting and lifecycle awareness

This commit is contained in:
oliexdev
2025-08-15 06:40:01 +02:00
parent 841dcf04fc
commit ac181603e1
2 changed files with 170 additions and 77 deletions

View File

@@ -107,7 +107,8 @@ class MainActivity : ComponentActivity() {
// --- LogManager initializing --- // --- LogManager initializing ---
lifecycleScope.launch { lifecycleScope.launch {
val isFileLoggingEnabled = userSettingsRepository.isFileLoggingEnabled.first() val isFileLoggingEnabled = runCatching { userSettingsRepository.isFileLoggingEnabled.first() }
.getOrElse { false }
LogManager.init(applicationContext, isFileLoggingEnabled) LogManager.init(applicationContext, isFileLoggingEnabled)
LogManager.d(TAG, "LogManager initialized. File logging enabled: $isFileLoggingEnabled") LogManager.d(TAG, "LogManager initialized. File logging enabled: $isFileLoggingEnabled")
} }

View File

@@ -17,8 +17,11 @@
*/ */
package com.health.openscale.core.utils package com.health.openscale.core.utils
import android.app.Activity
import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle
import android.util.Log import android.util.Log
import com.health.openscale.BuildConfig import com.health.openscale.BuildConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -26,11 +29,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
/** /**
* Manages logging for the application, providing methods to log messages * Manages logging for the application, providing methods to log messages
@@ -38,7 +45,7 @@ import java.util.Locale
*/ */
object LogManager { object LogManager {
private const val DEFAULT_TAG = "openScaleLog" private const val TAG = "openScaleLog"
private const val LOG_SUB_DIRECTORY = "logs" private const val LOG_SUB_DIRECTORY = "logs"
private const val CURRENT_LOG_FILE_NAME_BASE = "openScale_current_log" private const val CURRENT_LOG_FILE_NAME_BASE = "openScale_current_log"
private const val LOG_FILE_EXTENSION = ".txt" private const val LOG_FILE_EXTENSION = ".txt"
@@ -50,6 +57,11 @@ object LogManager {
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
// Tracks foreground Activities to detect when the app goes to background.
private val startedActivityCount = AtomicInteger(0)
@Volatile private var lifecycleCallbacksRegistered = false
@Volatile private var isMarkdownBlockOpen = false
/** /**
* Initializes the LogManager. Must be called once, typically in Application.onCreate(). * Initializes the LogManager. Must be called once, typically in Application.onCreate().
* @param context The application context. * @param context The application context.
@@ -58,7 +70,7 @@ object LogManager {
fun init(context: Context, enableLoggingToFile: Boolean) { fun init(context: Context, enableLoggingToFile: Boolean) {
if (isInitialized) { if (isInitialized) {
// Log a warning if already initialized, but don't re-initialize. // Log a warning if already initialized, but don't re-initialize.
Log.w(DEFAULT_TAG, "LogManager already initialized. Ignoring subsequent init call.") Log.w(TAG, "LogManager already initialized. Ignoring subsequent init call.")
return return
} }
@@ -66,14 +78,40 @@ object LogManager {
logToFileEnabled = enableLoggingToFile logToFileEnabled = enableLoggingToFile
isInitialized = true isInitialized = true
// Register ActivityLifecycleCallbacks to close the markdown block when app goes background.
if (!lifecycleCallbacksRegistered && appContext is Application) {
(appContext as Application).registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
val count = startedActivityCount.incrementAndGet()
if (count == 1) {
// App went to foreground -> open the diff block to keep Markdown well-formed.
openMarkdownBlockIfNeeded()
}
}
override fun onActivityStopped(activity: Activity) {
if (startedActivityCount.decrementAndGet() == 0) {
// App went to background -> close the diff block to keep Markdown well-formed.
closeMarkdownBlock()
}
}
// Unused callbacks keep empty
override fun onActivityCreated(a: Activity, b: Bundle?) {}
override fun onActivityResumed(a: Activity) {}
override fun onActivityPaused(a: Activity) {}
override fun onActivitySaveInstanceState(a: Activity, outState: Bundle) {}
override fun onActivityDestroyed(a: Activity) {}
})
lifecycleCallbacksRegistered = true
}
// Log initialization status. // Log initialization status.
if (logToFileEnabled) { if (logToFileEnabled) {
coroutineScope.launch { coroutineScope.launch {
resetLogFileOnStartup() resetLogFileOnStartup()
i(DEFAULT_TAG, "LogManager initialized. Logging to file: enabled. Log directory: ${getLogDirectory().absolutePath}") i(TAG, "LogManager initialized. Logging to file: enabled. Log directory: ${getLogDirectory().absolutePath}")
} }
} else { } else {
i(DEFAULT_TAG, "LogManager initialized. Logging to file: disabled.") i(TAG, "LogManager initialized. Logging to file: disabled.")
} }
} }
@@ -81,15 +119,15 @@ object LogManager {
* Deletes the current log file if it exists and writes initial headers. * Deletes the current log file if it exists and writes initial headers.
* This is called on startup if file logging is enabled. * This is called on startup if file logging is enabled.
*/ */
private suspend fun resetLogFileOnStartup() { private fun resetLogFileOnStartup() {
val logDir = getLogDirectory() val logDir = getLogDirectory()
val currentLogFile = File(logDir, "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION") val currentLogFile = File(logDir, "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION")
if (currentLogFile.exists()) { if (currentLogFile.exists()) {
if (currentLogFile.delete()) { if (currentLogFile.delete()) {
d(DEFAULT_TAG, "Previous log file deleted on startup: ${currentLogFile.name}") d(TAG, "Previous log file deleted on startup: ${currentLogFile.name}")
} else { } else {
w(DEFAULT_TAG, "Failed to delete previous log file on startup: ${currentLogFile.name}") w(TAG, "Failed to delete previous log file on startup: ${currentLogFile.name}")
} }
} }
// Always attempt to write headers, ensures file is created if it didn't exist. // Always attempt to write headers, ensures file is created if it didn't exist.
@@ -104,16 +142,19 @@ object LogManager {
fun updateLoggingPreference(enabled: Boolean) { fun updateLoggingPreference(enabled: Boolean) {
val oldState = logToFileEnabled val oldState = logToFileEnabled
if (oldState == enabled) { if (oldState == enabled) {
d(DEFAULT_TAG, "File logging preference is already set to: $enabled. No change.") d(TAG, "File logging preference is already set to: $enabled. No change.")
return return
} }
logToFileEnabled = enabled logToFileEnabled = enabled
i(DEFAULT_TAG, "File logging preference updated to: $logToFileEnabled (was: $oldState)") if (!logToFileEnabled) {
closeMarkdownBlock()
}
i(TAG, "File logging preference updated to: $logToFileEnabled (was: $oldState)")
if (logToFileEnabled) { // Only act if newly enabled if (logToFileEnabled) { // Only act if newly enabled
coroutineScope.launch { coroutineScope.launch {
val currentLogFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION") val currentLogFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION")
if (!currentLogFile.exists() || currentLogFile.length() == 0L) { if (!currentLogFile.exists() || currentLogFile.length() == 0L) {
d(DEFAULT_TAG, "Log file missing or empty after enabling file logging. Writing headers.") d(TAG, "Log file missing or empty after enabling file logging. Writing headers.")
writeInitialLogHeaders(currentLogFile) writeInitialLogHeaders(currentLogFile)
} }
} }
@@ -131,10 +172,10 @@ object LogManager {
if (externalLogDir != null) { if (externalLogDir != null) {
if (!externalLogDir.exists()) { if (!externalLogDir.exists()) {
if (!externalLogDir.mkdirs()) { if (!externalLogDir.mkdirs()) {
w(DEFAULT_TAG, "Failed to create external log directory: ${externalLogDir.absolutePath}. Attempting internal storage.") w(TAG, "Failed to create external log directory: ${externalLogDir.absolutePath}. Attempting internal storage.")
// Fall through to internal storage if mkdirs fails // Fall through to internal storage if mkdirs fails
} else { } else {
d(DEFAULT_TAG, "External log directory created: ${externalLogDir.absolutePath}") d(TAG, "External log directory created: ${externalLogDir.absolutePath}")
return externalLogDir return externalLogDir
} }
} }
@@ -146,14 +187,14 @@ object LogManager {
if (!internalLogDir.exists()) { if (!internalLogDir.exists()) {
if (!internalLogDir.mkdirs()) { if (!internalLogDir.mkdirs()) {
// If internal storage also fails, this is a more serious issue. // If internal storage also fails, this is a more serious issue.
e(DEFAULT_TAG, "Failed to create internal log directory: ${internalLogDir.absolutePath}. Logging to file may not work.") e(TAG, "Failed to create internal log directory: ${internalLogDir.absolutePath}. Logging to file may not work.")
} else { } else {
d(DEFAULT_TAG, "Internal log directory created: ${internalLogDir.absolutePath}") d(TAG, "Internal log directory created: ${internalLogDir.absolutePath}")
} }
} }
// Log this fallback case if externalLogDir was null initially // Log this fallback case if externalLogDir was null initially
if (externalLogDir == null) { if (externalLogDir == null) {
w(DEFAULT_TAG, "External storage not available. Using internal storage for logs: ${internalLogDir.absolutePath}") w(TAG, "External storage not available. Using internal storage for logs: ${internalLogDir.absolutePath}")
} }
return internalLogDir return internalLogDir
} }
@@ -180,14 +221,14 @@ object LogManager {
if (!isInitialized) { if (!isInitialized) {
// Use Android's Log directly if LogManager isn't initialized. // Use Android's Log directly if LogManager isn't initialized.
// This ensures critical early errors or misconfigurations are visible. // This ensures critical early errors or misconfigurations are visible.
val initErrorMsg = "LogManager not initialized! Attempted to log: [${tag ?: DEFAULT_TAG}] $message" val initErrorMsg = "LogManager not initialized! Attempted to log: [${tag ?: TAG}] $message"
Log.e("LogManager_NotInit", initErrorMsg, throwable) Log.e("LogManager_NotInit", initErrorMsg, throwable)
// Optionally, print to System.err as a last resort if Logcat is also problematic // Optionally, print to System.err as a last resort if Logcat is also problematic
// System.err.println("$initErrorMsg ${throwable?.let { Log.getStackTraceString(it) }}") // System.err.println("$initErrorMsg ${throwable?.let { Log.getStackTraceString(it) }}")
return return
} }
val currentTag = tag ?: DEFAULT_TAG val currentTag = tag ?: TAG
// Log to Android's Logcat // Log to Android's Logcat
when (priority) { when (priority) {
@@ -214,20 +255,24 @@ object LogManager {
// Check if file is missing or empty, then write headers. // Check if file is missing or empty, then write headers.
// This can happen if the file was cleared or logging was just enabled. // This can happen if the file was cleared or logging was just enabled.
if (!currentLogFile.exists() || currentLogFile.length() == 0L) { if (!currentLogFile.exists() || currentLogFile.length() == 0L) {
d(DEFAULT_TAG, "Log file missing or empty, writing headers: ${currentLogFile.absolutePath}") d(TAG, "Log file missing or empty, writing headers: ${currentLogFile.absolutePath}")
writeInitialLogHeaders(currentLogFile) // This will create/overwrite with headers writeInitialLogHeaders(currentLogFile) // This will create/overwrite with headers
isMarkdownBlockOpen = true
} }
checkAndRotateLog(currentLogFile) // Rotate log if it exceeds max size checkAndRotateLog(currentLogFile) // Rotate log if it exceeds max size
// Append the log message // Append the log message
FileWriter(currentLogFile, true).use { writer -> OutputStreamWriter(
FileOutputStream(currentLogFile, true),
StandardCharsets.UTF_8
).use { writer ->
writer.append(formattedMessageForFile) writer.append(formattedMessageForFile)
writer.append("\n") writer.append("\n")
} }
} catch (e: IOException) { } catch (e: IOException) {
// Log error related to file writing to Logcat only, to avoid recursive file logging issues. // Log error related to file writing to Logcat only, to avoid recursive file logging issues.
Log.e(DEFAULT_TAG, "Error writing to log file: ${e.message}", e) Log.e(TAG, "Error writing to log file: ${e.message}", e)
} }
} }
} }
@@ -241,41 +286,29 @@ object LogManager {
*/ */
private fun writeInitialLogHeaders(logFile: File) { private fun writeInitialLogHeaders(logFile: File) {
try { try {
// Ensure the directory exists before attempting to write. // Ensure directory exists.
logFile.parentFile?.mkdirs() logFile.parentFile?.mkdirs()
OutputStreamWriter(FileOutputStream(logFile, false), StandardCharsets.UTF_8).use { writer ->
FileWriter(logFile, false).use { writer -> // false for append means overwrite
val separator = "============================================================"
val sessionStartTime = dateFormat.format(Date()) val sessionStartTime = dateFormat.format(Date())
// GitHub-friendly Markdown header; copy-pasteable into issues/PRs.
writer.append("$separator\n") writer.append("| Field | Value |\n")
writer.append(" LOG SESSION STARTED\n") writer.append("|---|---|\n")
writer.append(" -------------------\n") writer.append("| Time | $sessionStartTime |\n")
writer.append(" Time : $sessionStartTime\n") writer.append("| App | openScale |\n")
writer.append("| Version | ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) |\n")
writer.append("| Package | ${BuildConfig.APPLICATION_ID} |\n")
writer.append("| Build Type | ${BuildConfig.BUILD_TYPE} |\n")
writer.append("| Device | ${Build.MANUFACTURER} ${Build.MODEL} |\n")
writer.append("| Android | ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) |\n")
writer.append("| Build ID | ${Build.DISPLAY} |\n")
writer.append("\n") writer.append("\n")
// Open a ```diff block; subsequent log lines use +/-/!/… prefixes.
writer.append(" APPLICATION INFO\n") writer.append("```diff\n")
writer.append(" ----------------\n")
writer.append(" App Name : openScale\n") // Consider making this dynamic if needed
writer.append(" Version : ${BuildConfig.VERSION_NAME}\n")
writer.append(" Version Code : ${BuildConfig.VERSION_CODE}\n")
writer.append(" Package ID : ${BuildConfig.APPLICATION_ID}\n")
writer.append(" Build Type : ${BuildConfig.BUILD_TYPE}\n")
writer.append("\n")
writer.append(" DEVICE INFO\n")
writer.append(" -----------\n")
writer.append(" Manufacturer : ${Build.MANUFACTURER}\n")
writer.append(" Model : ${Build.MODEL}\n")
writer.append(" Android Version : ${Build.VERSION.RELEASE}\n")
writer.append(" API Level : ${Build.VERSION.SDK_INT}\n")
writer.append(" System Build ID : ${Build.DISPLAY}\n")
writer.append("$separator\n\n")
} }
d(DEFAULT_TAG, "Initial log headers written to: ${logFile.absolutePath}") isMarkdownBlockOpen = true
d(TAG, "Initial markdown-diff headers written to: ${logFile.absolutePath}")
} catch (e: IOException) { } catch (e: IOException) {
Log.e(DEFAULT_TAG, "Error writing initial log headers to ${logFile.absolutePath}", e) Log.e(TAG, "Error writing initial log headers to ${logFile.absolutePath}", e)
} }
} }
@@ -289,16 +322,27 @@ object LogManager {
* @return The formatted log string. * @return The formatted log string.
*/ */
private fun formatMessageForFile(priority: Int, tag: String, message: String, throwable: Throwable?): String { private fun formatMessageForFile(priority: Int, tag: String, message: String, throwable: Throwable?): String {
val priorityChar = when (priority) { // GitHub diff mapping: + INFO, ! WARN, - ERROR, ' ' DEBUG, ? VERBOSE
Log.VERBOSE -> "V"; Log.DEBUG -> "D"; Log.INFO -> "I"; Log.WARN -> "W"; Log.ERROR -> "E"; else -> "?" val levelChar = when (priority) {
Log.VERBOSE -> 'V'
Log.DEBUG -> 'D'
Log.INFO -> 'I'
Log.WARN -> 'W'
Log.ERROR -> 'E'
else -> '?'
} }
val timestamp = dateFormat.format(Date()) // Generate timestamp at the moment of formatting val prefix = when (priority) {
val builder = StringBuilder() Log.ERROR -> "- "
builder.append("$timestamp $priorityChar/$tag: $message") Log.WARN -> "! "
throwable?.let { Log.INFO -> "+ "
builder.append("\n").append(Log.getStackTraceString(it)) // Append stack trace if present Log.DEBUG -> "? "
Log.VERBOSE -> ". "
else -> " "
} }
return builder.toString() val timestamp = dateFormat.format(Date())
val base = "$timestamp $levelChar/$tag: $message"
val withThrowable = if (throwable != null) "$base\n${Log.getStackTraceString(throwable)}" else base
return prefix + withThrowable
} }
/** /**
@@ -310,19 +354,23 @@ object LogManager {
private fun checkAndRotateLog(currentLogFile: File) { private fun checkAndRotateLog(currentLogFile: File) {
if (currentLogFile.exists() && currentLogFile.length() > MAX_LOG_SIZE_BYTES) { if (currentLogFile.exists() && currentLogFile.length() > MAX_LOG_SIZE_BYTES) {
val oldFileSize = currentLogFile.length() val oldFileSize = currentLogFile.length()
i(DEFAULT_TAG, "Log file '${currentLogFile.name}' (size: $oldFileSize bytes) exceeds limit ($MAX_LOG_SIZE_BYTES bytes). Rotating.") i(TAG, "Log file '${currentLogFile.name}' (size: $oldFileSize bytes) exceeds limit ($MAX_LOG_SIZE_BYTES bytes). Rotating.")
try {
OutputStreamWriter(FileOutputStream(currentLogFile, true), StandardCharsets.UTF_8).use {
it.append("\n```").append("\n")
}
isMarkdownBlockOpen = false
} catch (_: IOException) {}
// Delete the oversized log file
if (currentLogFile.delete()) { if (currentLogFile.delete()) {
i(DEFAULT_TAG, "Oversized log file deleted: ${currentLogFile.name}. A new log file will be started with headers.") i(TAG, "Oversized log file deleted: ${currentLogFile.name}. A new log file will be started with headers.")
// The writeInitialLogHeaders will be called to prepare the new (now non-existent or empty) file. // Immediately open a new header and start a fresh ```diff block.
// It is important that this happens *before* the next log message is written.
// The main log() function's check for existence/emptiness and subsequent call to writeInitialLogHeaders
// will handle this. Alternatively, we can explicitly call it here.
// For clarity and ensuring headers are written immediately after rotation:
writeInitialLogHeaders(currentLogFile) writeInitialLogHeaders(currentLogFile)
isMarkdownBlockOpen = true
} else { } else {
e(DEFAULT_TAG, "Failed to delete oversized log file '${currentLogFile.name}' for rotation. Current log may continue to grow or writes may fail.") e(TAG, "Failed to delete oversized log file '${currentLogFile.name}' for rotation. Current log may continue to grow or writes may fail.")
} }
} }
} }
@@ -334,7 +382,7 @@ object LogManager {
*/ */
fun getLogFile(): File? { fun getLogFile(): File? {
if (!isInitialized) { if (!isInitialized) {
w(DEFAULT_TAG, "getLogFile() called before LogManager was initialized.") w(TAG, "getLogFile() called before LogManager was initialized.")
return null return null
} }
val logFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION") val logFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION")
@@ -342,7 +390,7 @@ object LogManager {
logFile logFile
} else { } else {
// Log file might not exist if logging to file is disabled or no logs written yet. // Log file might not exist if logging to file is disabled or no logs written yet.
d(DEFAULT_TAG, "Queried log file does not currently exist at path: ${logFile.absolutePath}") d(TAG, "Queried log file does not currently exist at path: ${logFile.absolutePath}")
null null
} }
} }
@@ -353,7 +401,7 @@ object LogManager {
*/ */
fun clearLogFiles() { fun clearLogFiles() {
if (!isInitialized) { if (!isInitialized) {
w(DEFAULT_TAG, "clearLogFiles() called before LogManager was initialized.") w(TAG, "clearLogFiles() called before LogManager was initialized.")
return return
} }
coroutineScope.launch { coroutineScope.launch {
@@ -362,26 +410,70 @@ object LogManager {
try { try {
if (currentLogFile.exists()) { if (currentLogFile.exists()) {
try {
OutputStreamWriter(FileOutputStream(currentLogFile, true), StandardCharsets.UTF_8).use {
it.append("\n```").append("\n")
}
isMarkdownBlockOpen = false
} catch (_: Exception) {}
if (currentLogFile.delete()) { if (currentLogFile.delete()) {
i(DEFAULT_TAG, "Log file cleared: ${currentLogFile.absolutePath}") i(TAG, "Log file cleared: ${currentLogFile.absolutePath}")
} else { } else {
e(DEFAULT_TAG, "Failed to clear log file: ${currentLogFile.absolutePath}") e(TAG, "Failed to clear log file: ${currentLogFile.absolutePath}")
// If deletion fails, do not proceed to write headers to a potentially problematic file. // If deletion fails, do not proceed to write headers to a potentially problematic file.
return@launch return@launch
} }
} else { } else {
i(DEFAULT_TAG, "Log file already cleared or did not exist: ${currentLogFile.absolutePath}") i(TAG, "Log file already cleared or did not exist: ${currentLogFile.absolutePath}")
} }
// If file logging is enabled, a new log session effectively starts, so write headers. // If file logging is enabled, a new log session effectively starts, so write headers.
if (logToFileEnabled) { if (logToFileEnabled) {
d(DEFAULT_TAG, "File logging is enabled, writing initial headers after clearing.") d(TAG, "File logging is enabled, writing initial headers after clearing.")
writeInitialLogHeaders(currentLogFile) writeInitialLogHeaders(currentLogFile) // starts new ```diff block
isMarkdownBlockOpen = true
} }
} catch (e: Exception) { } catch (e: Exception) {
// Catch any unexpected exception during file operations. // Catch any unexpected exception during file operations.
Log.e(DEFAULT_TAG, "Error during clearLogFiles operation for ${currentLogFile.absolutePath}", e) Log.e(TAG, "Error during clearLogFiles operation for ${currentLogFile.absolutePath}", e)
} }
} }
} }
/**
* Closes the ```diff code block at the end of the current log file (if any).
* Safe to call multiple times; appends a fence only if the file exists.
*/
@JvmStatic
fun closeMarkdownBlock() {
if (!isInitialized) return
val logFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION")
if (!logFile.exists() || !isMarkdownBlockOpen) return
try {
OutputStreamWriter(FileOutputStream(logFile, true), StandardCharsets.UTF_8).use {
it.append("\n```").append("\n")
}
isMarkdownBlockOpen = false
d(TAG, "Markdown diff block closed.")
} catch (e: IOException) {
Log.e(TAG, "Error closing markdown diff block", e)
}
}
private fun openMarkdownBlockIfNeeded() {
if (!isInitialized || !logToFileEnabled || isMarkdownBlockOpen) return
val logFile = File(getLogDirectory(), "$CURRENT_LOG_FILE_NAME_BASE$LOG_FILE_EXTENSION")
try {
// Falls Datei frisch oder leer ist, wird ohnehin beim nächsten Write der Header erzeugt.
if (!logFile.exists() || logFile.length() == 0L) return
OutputStreamWriter(FileOutputStream(logFile, true), StandardCharsets.UTF_8).use {
it.append("```diff\n")
}
isMarkdownBlockOpen = true
d(TAG, "Markdown diff block reopened after app foreground.")
} catch (e: IOException) {
Log.e(TAG, "Error reopening markdown diff block", e)
}
}
} }