From ba6db789c95cee7042a76bcff45f74ab2f4f3c38 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 18 Jun 2025 13:54:59 +0200 Subject: [PATCH 01/12] feat(logging): introduce thread-safe logging with new LogFileWriter implementations --- .../com/wire/android/WireApplication.kt | 87 +++- .../com/wire/android/di/LogWriterModule.kt | 7 +- .../android/navigation/OtherDestinations.kt | 2 +- .../notification/WireNotificationManager.kt | 2 +- .../com/wire/android/services/CallService.kt | 2 +- .../com/wire/android/ui/debug/DebugScreen.kt | 45 +- .../android/ui/debug/UserDebugViewModel.kt | 10 +- .../android/util/logging/LogFileWriter.kt | 58 +++ .../util/logging/LogFileWriterConfig.kt | 42 ++ .../LogFileWriterV1Impl.kt} | 17 +- .../util/logging/LogFileWriterV2Impl.kt | 410 ++++++++++++++++++ .../android/util/{ => logging}/LogUtil.kt | 2 +- .../kotlin/customization/FeatureConfigs.kt | 4 +- default.json | 9 +- 14 files changed, 661 insertions(+), 36 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt create mode 100644 app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt rename app/src/main/kotlin/com/wire/android/util/{LogFileWriter.kt => logging/LogFileWriterV1Impl.kt} (94%) create mode 100644 app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt rename app/src/main/kotlin/com/wire/android/util/{ => logging}/LogUtil.kt (96%) diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 01e38274662..c55407226f5 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -40,7 +40,7 @@ import com.wire.android.feature.analytics.model.AnalyticsSettings import com.wire.android.util.AppNameUtil import com.wire.android.util.CurrentScreenManager import com.wire.android.util.DataDogLogger -import com.wire.android.util.LogFileWriter +import com.wire.android.util.logging.LogFileWriter import com.wire.android.util.getGitBuildId import com.wire.android.util.lifecycle.SyncLifecycleManager import com.wire.android.workmanager.WireWorkerFactory @@ -59,6 +59,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import javax.inject.Inject @HiltAndroidApp @@ -107,6 +108,8 @@ class WireApplication : BaseApp() { enableStrictMode() + setupGlobalExceptionHandler() + startActivityLifecycleCallback() globalAppScope.launch { @@ -164,6 +167,82 @@ class WireApplication : BaseApp() { } } + private fun setupGlobalExceptionHandler() { + setupUncaughtExceptionHandler() + setupHistoricalExitMonitoring() + } + + @Suppress("TooGenericExceptionCaught") + private fun setupUncaughtExceptionHandler() { + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { thread, exception -> + flushLogsBeforeCrash() + defaultHandler?.uncaughtException(thread, exception) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun flushLogsBeforeCrash() { + // Use fire-and-forget approach to avoid blocking the crash handler + // which could lead to ANRs. We attempt a quick flush but don't wait for it. + try { + globalAppScope.launch(Dispatchers.IO) { + try { + // Use a very short timeout to avoid delaying the crash + withTimeout(CRASH_FLUSH_TIMEOUT_MS) { + logFileWriter.get().forceFlush() + } + appLogger.i("Logs flushed before crash") + } catch (e: Exception) { + // Log errors but don't block the crash handler + appLogger.e("Failed to flush logs before crash", e) + } + } + } catch (e: Exception) { + // Ignore any launch failures - we don't want to interfere with crash handling + } + } + + @Suppress("TooGenericExceptionCaught") + private fun setupHistoricalExitMonitoring() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val activityManager = getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager + activityManager.setProcessStateSummary(ByteArray(0)) + + // This will be called after the app exits, so we can't flush here, + // but we log it for diagnostics + globalAppScope.launch { + activityManager.getHistoricalProcessExitReasons(packageName, 0, MAX_HISTORICAL_EXIT_REASONS) + .forEach { info -> + logPreviousExitReason(info) + } + } + } catch (e: Exception) { + appLogger.e("Failed to setup app exit monitoring", e) + } + } + } + + private fun logPreviousExitReason(info: android.app.ApplicationExitInfo) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + when (info.reason) { + android.app.ApplicationExitInfo.REASON_ANR -> { + appLogger.w("Previous app exit was due to ANR at ${info.timestamp}") + } + android.app.ApplicationExitInfo.REASON_CRASH -> { + appLogger.w("Previous app exit was due to crash at ${info.timestamp}") + } + android.app.ApplicationExitInfo.REASON_LOW_MEMORY -> { + appLogger.w("Previous app exit was due to low memory at ${info.timestamp}") + } + else -> { + appLogger.i("Previous app exit reason: ${info.reason} at ${info.timestamp}") + } + } + } + } + @Suppress("EmptyFunctionBlock") private fun startActivityLifecycleCallback() { registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { @@ -293,7 +372,9 @@ class WireApplication : BaseApp() { override fun onLowMemory() { super.onLowMemory() appLogger.w("onLowMemory called - Stopping logging, buckling the seatbelt and hoping for the best!") - logFileWriter.get().stop() + globalAppScope.launch { + logFileWriter.get().stop() + } } private companion object { @@ -316,5 +397,7 @@ class WireApplication : BaseApp() { } private const val TAG = "WireApplication" + private const val CRASH_FLUSH_TIMEOUT_MS = 1000L + private const val MAX_HISTORICAL_EXIT_REASONS = 5 } } diff --git a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt index edf61bb8c37..5c04783517d 100644 --- a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt @@ -19,7 +19,8 @@ package com.wire.android.di import android.content.Context -import com.wire.android.util.LogFileWriter +import com.wire.android.util.LogFileWriterV1Impl + import com.wire.android.util.logging.LogFileWriter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,7 +35,7 @@ class LogWriterModule { @Singleton @Provides fun provideKaliumFileWriter(@ApplicationContext context: Context): LogFileWriter { - val logsDirectory = LogFileWriter.logsDirectory(context) - return LogFileWriter(logsDirectory) + val logsDirectory = LogFileWriterV1Impl.logsDirectory(context) + return LogFileWriterV1Impl(logsDirectory) } } diff --git a/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt b/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt index 1ed00dd158e..40b2abf6530 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt @@ -26,10 +26,10 @@ import com.ramcosta.composedestinations.spec.Direction import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.util.EmailComposer -import com.wire.android.util.LogFileWriter import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.getUrisOfFilesInDirectory +import com.wire.android.util.logging.LogFileWriter import com.wire.android.util.multipleFileSharingIntent import com.wire.android.util.sha256 diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index 066a2cd06e8..45fd308897d 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -28,7 +28,7 @@ import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.lifecycle.SyncLifecycleManager -import com.wire.android.util.logIfEmptyUserName +import com.wire.android.util.logging.logIfEmptyUserName import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.ConversationId diff --git a/app/src/main/kotlin/com/wire/android/services/CallService.kt b/app/src/main/kotlin/com/wire/android/services/CallService.kt index aa9594da9aa..71e48d0146d 100644 --- a/app/src/main/kotlin/com/wire/android/services/CallService.kt +++ b/app/src/main/kotlin/com/wire/android/services/CallService.kt @@ -33,7 +33,7 @@ import com.wire.android.notification.CallNotificationManager import com.wire.android.notification.NotificationIds import com.wire.android.services.CallService.Action import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.logIfEmptyUserName +import com.wire.android.util.logging.logIfEmptyUserName import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.CoreLogic diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index 792802b0492..ac371a1e047 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ClipboardManager @@ -60,6 +61,9 @@ import com.wire.android.util.AppNameUtil import com.wire.android.util.getMimeType import com.wire.android.util.getUrisOfFilesInDirectory import com.wire.android.util.multipleFileSharingIntent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.launch import java.io.File @RootNavGraph @@ -74,6 +78,7 @@ fun DebugScreen( state = userDebugViewModel.state, onLoggingEnabledChange = userDebugViewModel::setLoggingEnabledState, onDeleteLogs = userDebugViewModel::deleteLogs, + onFlushLogs = userDebugViewModel::flushLogs, onDatabaseLoggerEnabledChanged = userDebugViewModel::setDatabaseLoggerEnabledState, onEnableWireCellsFeature = userDebugViewModel::enableWireCellsFeature, ) @@ -86,6 +91,7 @@ internal fun UserDebugContent( onLoggingEnabledChange: (Boolean) -> Unit, onDatabaseLoggerEnabledChanged: (Boolean) -> Unit, onDeleteLogs: () -> Unit, + onFlushLogs: () -> Deferred, onEnableWireCellsFeature: (Boolean) -> Unit, ) { val debugContentState: DebugContentState = rememberDebugContentState(state.logPath) @@ -111,7 +117,7 @@ internal fun UserDebugContent( isLoggingEnabled = isLoggingEnabled, onLoggingEnabledChange = onLoggingEnabledChange, onDeleteLogs = onDeleteLogs, - onShareLogs = debugContentState::shareLogs, + onShareLogs = { debugContentState.shareLogs(onFlushLogs) }, isDBLoggerEnabled = state.isDBLoggingEnabled, onDBLoggerEnabledChange = onDatabaseLoggerEnabledChanged, isPrivateBuild = BuildConfig.PRIVATE_BUILD, @@ -182,13 +188,15 @@ fun rememberDebugContentState(logPath: String): DebugContentState { val context = LocalContext.current val clipboardManager = LocalClipboardManager.current val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() return remember { DebugContentState( context, clipboardManager, logPath, - scrollState + scrollState, + coroutineScope ) } } @@ -197,7 +205,8 @@ data class DebugContentState( val context: Context, val clipboardManager: ClipboardManager, val logPath: String, - val scrollState: ScrollState + val scrollState: ScrollState, + val coroutineScope: CoroutineScope ) { fun copyToClipboard(text: String) { clipboardManager.setText(AnnotatedString(text)) @@ -208,18 +217,23 @@ data class DebugContentState( ).show() } - fun shareLogs() { - val dir = File(logPath).parentFile - val fileUris = - if (dir != null && dir.exists()) context.getUrisOfFilesInDirectory(dir) else arrayListOf() - val intent = context.multipleFileSharingIntent(fileUris) - // The first log file is simply text, not compressed. Get its mime type separately - // and set it as the mime type for the intent. - intent.type = fileUris.firstOrNull()?.getMimeType(context) ?: "text/plain" - // Get all other mime types and add them - val mimeTypes = fileUris.drop(1).mapNotNull { it.getMimeType(context) } - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toSet().toTypedArray()) - context.startActivity(intent) + fun shareLogs(onFlushLogs: () -> Deferred) { + coroutineScope.launch { + // Flush any buffered logs before sharing to ensure completeness + onFlushLogs().await() + + val dir = File(logPath).parentFile + val fileUris = + if (dir != null && dir.exists()) context.getUrisOfFilesInDirectory(dir) else arrayListOf() + val intent = context.multipleFileSharingIntent(fileUris) + // The first log file is simply text, not compressed. Get its mime type separately + // and set it as the mime type for the intent. + intent.type = fileUris.firstOrNull()?.getMimeType(context) ?: "text/plain" + // Get all other mime types and add them + val mimeTypes = fileUris.drop(1).mapNotNull { it.getMimeType(context) } + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toSet().toTypedArray()) + context.startActivity(intent) + } } } @@ -234,6 +248,7 @@ internal fun PreviewUserDebugContent() = WireTheme { onNavigationPressed = {}, onLoggingEnabledChange = {}, onDeleteLogs = {}, + onFlushLogs = { kotlinx.coroutines.CompletableDeferred(Unit) }, onDatabaseLoggerEnabledChanged = {}, onEnableWireCellsFeature = {}, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt index f520e602527..cc484f2afbe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.CurrentAccount import com.wire.android.util.EMPTY -import com.wire.android.util.LogFileWriter +import com.wire.android.util.logging.LogFileWriter import com.wire.kalium.common.logger.CoreLogger import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logic.data.user.UserId @@ -34,6 +34,8 @@ import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import kotlinx.coroutines.launch import javax.inject.Inject @@ -94,6 +96,12 @@ class UserDebugViewModel logFileWriter.deleteAllLogFiles() } + fun flushLogs(): Deferred { + return viewModelScope.async { + logFileWriter.forceFlush() + } + } + fun setLoggingEnabledState(isEnabled: Boolean) { viewModelScope.launch { globalDataStore.setLoggingEnabled(isEnabled) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt new file mode 100644 index 00000000000..769e80aeffa --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt @@ -0,0 +1,58 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.util.logging + +import android.content.Context +import java.io.File + +/** + * Common interface for log file writers to enable easy substitution + * between different implementations. + */ +interface LogFileWriter { + + /** + * The active logging file where logs are currently being written + */ + val activeLoggingFile: File + + /** + * Starts the log collection system + */ + suspend fun start() + + /** + * Stops the log collection system + */ + suspend fun stop() + + /** + * Forces a flush of any pending logs to ensure they are written to file + */ + suspend fun forceFlush() + + /** + * Deletes all log files including active and compressed files + */ + fun deleteAllLogFiles() + + companion object { + fun logsDirectory(context: Context) = File(context.cacheDir, "logs") + } +} diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt new file mode 100644 index 00000000000..121feeaaa42 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.util.logging + +import com.wire.android.BuildConfig + +data class LogFileWriterConfig( + val flushIntervalMs: Long = DEFAULT_FLUSH_INTERVAL_MS, + val maxBufferSize: Int = DEFAULT_MAX_BUFFER_SIZE, + val bufferSizeBytes: Int = DEFAULT_BUFFER_SIZE_BYTES, + val maxFileSize: Long = DEFAULT_MAX_FILE_SIZE_BYTES, + val enableAsyncFlushing: Boolean = BuildConfig.USE_ASYNC_FLUSH_LOGGING, + val flushTimeoutMs: Long = DEFAULT_FLUSH_TIMEOUT_MS, + val bufferLockTimeoutMs: Long = DEFAULT_BUFFER_LOCK_TIMEOUT_MS +) { + companion object { + private const val DEFAULT_FLUSH_INTERVAL_MS = 5000L + private const val DEFAULT_MAX_BUFFER_SIZE = 100 + private const val DEFAULT_BUFFER_SIZE_BYTES = 64 * 1024 + private const val DEFAULT_MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024L // 25MB + private const val DEFAULT_FLUSH_TIMEOUT_MS = 5000L // 5 seconds + private const val DEFAULT_BUFFER_LOCK_TIMEOUT_MS = 3000L // 3 seconds + + fun default() = LogFileWriterConfig() + } +} diff --git a/app/src/main/kotlin/com/wire/android/util/LogFileWriter.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt similarity index 94% rename from app/src/main/kotlin/com/wire/android/util/LogFileWriter.kt rename to app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt index 5a591ca336f..547e55975ca 100644 --- a/app/src/main/kotlin/com/wire/android/util/LogFileWriter.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt @@ -20,6 +20,7 @@ package com.wire.android.util import android.content.Context import com.wire.android.appLogger +import com.wire.android.util.logging.LogFileWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -43,11 +44,11 @@ import java.util.Locale import java.util.zip.GZIPOutputStream @Suppress("TooGenericExceptionCaught") -class LogFileWriter(private val logsDirectory: File) { +class LogFileWriterV1Impl(private val logsDirectory: File): LogFileWriter { private val logFileTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) - val activeLoggingFile = File(logsDirectory, ACTIVE_LOGGING_FILE_NAME) + override val activeLoggingFile = File(logsDirectory, ACTIVE_LOGGING_FILE_NAME) private val fileWriterCoroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var writingJob: Job? = null @@ -59,7 +60,7 @@ class LogFileWriter(private val logsDirectory: File) { * logger.i("something") // Is guaranteed to be recorded in the log file * ``` */ - suspend fun start() { + override suspend fun start() { appLogger.i("KaliumFileWritter.start called") val isWriting = writingJob?.isActive ?: false if (isWriting) { @@ -112,12 +113,16 @@ class LogFileWriter(private val logsDirectory: File) { /** * Stops processing logs and writing to files */ - fun stop() { + override suspend fun stop() { appLogger.i("KaliumFileWritter.stop called; Stopping log collection.") writingJob?.cancel() clearActiveLoggingFileContent() } + override suspend fun forceFlush() { + /* no-op */ + } + private fun clearActiveLoggingFileContent() { if (activeLoggingFile.exists()) { val writer = PrintWriter(activeLoggingFile) @@ -151,7 +156,7 @@ class LogFileWriter(private val logsDirectory: File) { } } - fun deleteAllLogFiles() { + override fun deleteAllLogFiles() { clearActiveLoggingFileContent() logsDirectory.listFiles()?.filter { it.extension.lowercase(Locale.ROOT) == LOG_COMPRESSED_FILE_EXTENSION @@ -196,7 +201,5 @@ class LogFileWriter(private val logsDirectory: File) { private const val BYTE_ARRAY_SIZE = 1024 private const val LOG_COMPRESSED_FILES_MAX_COUNT = 10 private const val LOG_COMPRESSED_FILE_EXTENSION = "gz" - - fun logsDirectory(context: Context) = File(context.cacheDir, "logs") } } diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt new file mode 100644 index 00000000000..eacacebceed --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -0,0 +1,410 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.util.logging + +import android.app.ActivityManager +import android.content.Context +import com.wire.android.appLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.cancelAndJoin +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.PrintWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.zip.GZIPOutputStream + +@Suppress("TooGenericExceptionCaught", "TooManyFunctions") +class LogFileWriterV2Impl( + private val logsDirectory: File, + private val context: Context? = null, + private val config: LogFileWriterConfig = LogFileWriterConfig.default() +) : LogFileWriter { + + private val logFileTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) + + override val activeLoggingFile = File(logsDirectory, ACTIVE_LOGGING_FILE_NAME) + + private val fileWriterCoroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var writingJob: Job? = null + private var flushJob: Job? = null + + // Buffering system + private val logBuffer = mutableListOf() + private val bufferMutex = Mutex() + private var lastFlushTime = 0L + private var bufferedWriter: BufferedWriter? = null + + // Process management + private var logcatProcess: Process? = null + + /** + * Initializes logging, waiting until the logger is actually initialized before returning. + * ```kotlin + * logFileWriter.start() + * logger.i("something") // Is guaranteed to be recorded in the log file + * ``` + */ + override suspend fun start() { + appLogger.i("KaliumFileWritter.start called") + val isWriting = writingJob?.isActive ?: false + if (isWriting) { + appLogger.d("KaliumFileWriter.init called but job was already active. Ignoring call") + return + } + ensureLogDirectoryAndFileExistence() + val waitInitializationJob = Job() + + writingJob = fileWriterCoroutineScope.launch { + observeLogCatWritingToLoggingFile().catch { + appLogger.e("Write to file failed :$it", it) + }.onEach { + waitInitializationJob.complete() + }.filter { + it > config.maxFileSize + }.collect { + ensureActive() + // Flush buffer before compression + bufferMutex.withLock { + flushBuffer() + } + launch { compressAsync() } + clearActiveLoggingFileContent() + deleteOldCompressedFiles() + } + } + + // Start periodic flush job + if (config.enableAsyncFlushing) { + flushJob = fileWriterCoroutineScope.launch { + while (isActive) { + delay(config.flushIntervalMs) + try { + withTimeout(config.bufferLockTimeoutMs) { + bufferMutex.withLock { + if (logBuffer.isNotEmpty()) { + flushBuffer() + lastFlushTime = System.currentTimeMillis() + } + } + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Periodic flush timed out, buffer may be locked by another operation") + } catch (e: Exception) { + appLogger.e("Error during periodic flush", e) + } + } + } + } + + appLogger.i("KaliumFileWritter.start: Starting log collection.") + waitInitializationJob.join() + } + + /** + * Observes logcat text, writing to the [activeLoggingFile] as it reads. + * @return A Flow that tells the current length, in bytes, of the log file. + */ + private fun CoroutineScope.observeLogCatWritingToLoggingFile(): Flow = flow { + Runtime.getRuntime().exec("logcat -c") + logcatProcess = Runtime.getRuntime().exec("logcat") + + val reader = logcatProcess!!.inputStream.bufferedReader() + + appLogger.i("Starting to write log files, grabbing from logcat") + while (isActive) { + val text = reader.readLine() + if (!text.isNullOrBlank()) { + val fileSize = writeLineToFile(text) + emit(fileSize) + } + } + reader.close() + stopLogcatProcess() + }.flowOn(Dispatchers.IO) + + private fun stopLogcatProcess() { + logcatProcess?.let { process -> + try { + process.destroy() + if (process.isAlive && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + process.destroyForcibly() + } + } catch (e: Exception) { + appLogger.e("Error stopping logcat process", e) + } + } + logcatProcess = null + } + + /** + * Stops processing logs and writing to files + */ + override suspend fun stop() { + appLogger.i("KaliumFileWritter.stop called; Stopping log collection.") + try { + // Stop logcat process first to prevent new logs + stopLogcatProcess() + + // Cancel jobs with timeout to avoid hanging + writingJob?.let { job -> + try { + withTimeout(config.flushTimeoutMs) { + job.cancelAndJoin() + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Writing job cancellation timed out, forcing cancellation") + job.cancel() + } + } + + flushJob?.let { job -> + try { + withTimeout(config.flushTimeoutMs) { + job.cancelAndJoin() + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Flush job cancellation timed out, forcing cancellation") + job.cancel() + } + } + + // Flush any remaining buffered content with timeout + try { + withTimeout(config.flushTimeoutMs) { + bufferMutex.withLock { + flushBuffer() + } + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Final buffer flush timed out, some logs may be lost") + } catch (e: Exception) { + appLogger.e("Error during final buffer flush", e) + } + } finally { + // Ensure resources are cleaned up regardless of exceptions + closeResources() + + try { + clearActiveLoggingFileContent() + } catch (e: Exception) { + appLogger.e("Error clearing active logging file content", e) + } + } + } + + private fun closeResources() { + try { + bufferedWriter?.close() + } catch (e: Exception) { + appLogger.e("Error closing buffered writer", e) + } finally { + bufferedWriter = null + } + } + + /** + * Manually flushes any buffered log entries to the file. + * This is useful before sharing logs to ensure all recent entries are included. + */ + override suspend fun forceFlush() { + try { + withTimeout(config.flushTimeoutMs) { + bufferMutex.withLock { + flushBuffer() + } + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Force flush operation timed out after ${config.flushTimeoutMs}ms") + throw e + } catch (e: Exception) { + appLogger.e("Error during force flush", e) + throw e + } + } + + private fun clearActiveLoggingFileContent() { + if (activeLoggingFile.exists()) { + val writer = PrintWriter(activeLoggingFile) + writer.print("") + writer.close() + } + } + + /** + * Writes the new [text] and other log entries in logcat to the [activeLoggingFile]. + * @return The length, in bytes, of the log file. + */ + private suspend fun writeLineToFile(text: String): Long = withContext(Dispatchers.IO) { + try { + withTimeout(config.bufferLockTimeoutMs) { + bufferMutex.withLock { + logBuffer.add(text) + + // Check memory pressure periodically + if (logBuffer.size % MEMORY_PRESSURE_CHECK_INTERVAL == 0) { + handleMemoryPressure() + } + + val currentTime = System.currentTimeMillis() + val shouldFlush = logBuffer.size >= config.maxBufferSize || + (config.enableAsyncFlushing && (currentTime - lastFlushTime) >= config.flushIntervalMs) + + if (shouldFlush) { + flushBuffer() + lastFlushTime = currentTime + } + + return@withLock activeLoggingFile.length() + } + } + } catch (e: TimeoutCancellationException) { + appLogger.w("Buffer write operation timed out, log line may be lost: $text") + // Return current file length as fallback + return@withContext activeLoggingFile.length() + } catch (e: Exception) { + appLogger.e("Error writing to log buffer", e) + return@withContext activeLoggingFile.length() + } + } + + private fun ensureLogDirectoryAndFileExistence() { + if (!logsDirectory.exists() && !logsDirectory.mkdirs()) { + appLogger.e("Unable to create logs directory") + } + + if (!activeLoggingFile.exists() && !activeLoggingFile.createNewFile()) { + appLogger.e("KaliumFileWriter: Failure to create new file for logging", IOException("Unable to load log file")) + } + if (!activeLoggingFile.canWrite()) { + appLogger.e("KaliumFileWriter: Logging file is not writable", IOException("Log file not writable")) + } + } + + override fun deleteAllLogFiles() { + clearActiveLoggingFileContent() + logsDirectory.listFiles()?.filter { + it.extension.lowercase(Locale.ROOT) == LOG_COMPRESSED_FILE_EXTENSION + }?.forEach { it.delete() } + } + + private fun getCompressedFilesList() = (logsDirectory.listFiles() ?: emptyArray()).filter { it != activeLoggingFile } + + private fun compressedFileName(): String { + val currentDate = logFileTimeFormat.format(Date()) + return "${LOG_FILE_PREFIX}_$currentDate.$LOG_COMPRESSED_FILE_EXTENSION" + } + + private fun deleteOldCompressedFiles() = getCompressedFilesList() + .sortedBy { it.lastModified() } + .dropLast(LOG_COMPRESSED_FILES_MAX_COUNT) + .forEach { + it.delete() + } + + private suspend fun compressAsync() = withContext(Dispatchers.IO) { + try { + val compressedFile = File(logsDirectory, compressedFileName()) + + GZIPOutputStream(compressedFile.outputStream().buffered()).use { gzipOut -> + activeLoggingFile.inputStream().buffered().use { input -> + input.copyTo(gzipOut, config.bufferSizeBytes) + } + } + + appLogger.i("Log file compressed: ${activeLoggingFile.name} -> ${compressedFile.name}") + } catch (e: Exception) { + appLogger.e("Failed to compress log file: ${activeLoggingFile.name}", e) + } + } + + private fun flushBuffer() { + if (logBuffer.isEmpty()) return + val linesToWrite = logBuffer.toList() + logBuffer.clear() + + try { + // Use BufferedWriter for efficient writing + val writer = bufferedWriter ?: BufferedWriter( + FileWriter(activeLoggingFile, true), + config.bufferSizeBytes + ).also { bufferedWriter = it } + + linesToWrite.forEach { line -> + writer.appendLine(line) + } + writer.flush() + } catch (e: IOException) { + appLogger.e("Failed to flush log buffer", e) + // Re-add failed lines back to buffer for retry + logBuffer.addAll(0, linesToWrite) + } + } + + private fun handleMemoryPressure() { + context?.let { + val activityManager = it.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + activityManager?.let { am -> + val memoryInfo = ActivityManager.MemoryInfo() + am.getMemoryInfo(memoryInfo) + + if (memoryInfo.availMem < memoryInfo.threshold * MEMORY_PRESSURE_MULTIPLIER) { + appLogger.w("Low memory detected, forcing log buffer flush") + // Force flush in a separate coroutine to avoid blocking + fileWriterCoroutineScope.launch { + bufferMutex.withLock { + flushBuffer() + lastFlushTime = System.currentTimeMillis() + } + } + } + } + } + } + + companion object { + private const val LOG_FILE_PREFIX = "wire" + private const val ACTIVE_LOGGING_FILE_NAME = "${LOG_FILE_PREFIX}_logs.txt" + private const val LOG_COMPRESSED_FILES_MAX_COUNT = 10 + private const val LOG_COMPRESSED_FILE_EXTENSION = "gz" + private const val MEMORY_PRESSURE_CHECK_INTERVAL = 50 + private const val MEMORY_PRESSURE_MULTIPLIER = 2 + } +} diff --git a/app/src/main/kotlin/com/wire/android/util/LogUtil.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogUtil.kt similarity index 96% rename from app/src/main/kotlin/com/wire/android/util/LogUtil.kt rename to app/src/main/kotlin/com/wire/android/util/logging/LogUtil.kt index 1907488de09..86e8f9456b8 100644 --- a/app/src/main/kotlin/com/wire/android/util/LogUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogUtil.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.util +package com.wire.android.util.logging import com.wire.android.appLogger import com.wire.kalium.logic.data.user.SelfUser diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index 0ec7998a7c4..419659f4744 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -116,5 +116,7 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { */ ANALYTICS_ENABLED("analytics_enabled", ConfigType.BOOLEAN), ANALYTICS_APP_KEY("analytics_app_key", ConfigType.STRING), - ANALYTICS_SERVER_URL("analytics_server_url", ConfigType.STRING) + ANALYTICS_SERVER_URL("analytics_server_url", ConfigType.STRING), + + USE_ASYNC_FLUSH_LOGGING("use_async_flush_logging", ConfigType.BOOLEAN), } diff --git a/default.json b/default.json index 8e1b9866cf9..ff8693d1861 100644 --- a/default.json +++ b/default.json @@ -35,7 +35,8 @@ "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", "analytics_server_url": "https://countly.wire.com/", "enable_crossplatform_backup": true, - "enable_new_registration": true + "enable_new_registration": true, + "use_async_flush_logging" : true }, "staging": { "application_id": "com.waz.zclient.dev", @@ -83,7 +84,8 @@ "analytics_server_url": "https://countly.wire.com/", "paginated_conversation_list_enabled": true, "enable_crossplatform_backup": true, - "enable_new_registration": true + "enable_new_registration": true, + "use_async_flush_logging" : true }, "fdroid": { "application_id": "com.wire", @@ -147,5 +149,6 @@ "paginated_conversation_list_enabled": false, "should_display_release_notes": true, "public_channels_enabled": false, - "use_new_login_for_default_backend": true + "use_new_login_for_default_backend": true, + "use_async_flush_logging" : false } From 6132313a7be52ea54aa5688b4f3af37ad3885b31 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 23 Jun 2025 15:45:26 +0200 Subject: [PATCH 02/12] quick change --- .../com/wire/android/di/LogWriterModule.kt | 11 +++++-- ...iterConfig.kt => LogFileWriterV2Config.kt} | 4 +-- .../util/logging/LogFileWriterV2Impl.kt | 31 ++----------------- 3 files changed, 13 insertions(+), 33 deletions(-) rename app/src/main/kotlin/com/wire/android/util/logging/{LogFileWriterConfig.kt => LogFileWriterV2Config.kt} (95%) diff --git a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt index 5c04783517d..bdcf5174128 100644 --- a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt @@ -19,8 +19,10 @@ package com.wire.android.di import android.content.Context +import com.wire.android.BuildConfig import com.wire.android.util.LogFileWriterV1Impl import com.wire.android.util.logging.LogFileWriter +import com.wire.android.util.logging.LogFileWriterV2Impl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -35,7 +37,12 @@ class LogWriterModule { @Singleton @Provides fun provideKaliumFileWriter(@ApplicationContext context: Context): LogFileWriter { - val logsDirectory = LogFileWriterV1Impl.logsDirectory(context) - return LogFileWriterV1Impl(logsDirectory) + if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) { + val logsDirectory = LogFileWriter.logsDirectory(context) + return LogFileWriterV2Impl(logsDirectory, context) + } else { + val logsDirectory = LogFileWriter.logsDirectory(context) + return LogFileWriterV1Impl(logsDirectory) + } } } diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt similarity index 95% rename from app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt rename to app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt index 121feeaaa42..d77bfc10060 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterConfig.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt @@ -20,7 +20,7 @@ package com.wire.android.util.logging import com.wire.android.BuildConfig -data class LogFileWriterConfig( +data class LogFileWriterV2Config( val flushIntervalMs: Long = DEFAULT_FLUSH_INTERVAL_MS, val maxBufferSize: Int = DEFAULT_MAX_BUFFER_SIZE, val bufferSizeBytes: Int = DEFAULT_BUFFER_SIZE_BYTES, @@ -37,6 +37,6 @@ data class LogFileWriterConfig( private const val DEFAULT_FLUSH_TIMEOUT_MS = 5000L // 5 seconds private const val DEFAULT_BUFFER_LOCK_TIMEOUT_MS = 3000L // 3 seconds - fun default() = LogFileWriterConfig() + fun default() = LogFileWriterV2Config() } } diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index eacacebceed..58601989b28 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -54,8 +54,8 @@ import java.util.zip.GZIPOutputStream @Suppress("TooGenericExceptionCaught", "TooManyFunctions") class LogFileWriterV2Impl( private val logsDirectory: File, - private val context: Context? = null, - private val config: LogFileWriterConfig = LogFileWriterConfig.default() + private val context: Context?, + private val config: LogFileWriterV2Config = LogFileWriterV2Config.default() ) : LogFileWriter { private val logFileTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) @@ -278,11 +278,6 @@ class LogFileWriterV2Impl( bufferMutex.withLock { logBuffer.add(text) - // Check memory pressure periodically - if (logBuffer.size % MEMORY_PRESSURE_CHECK_INTERVAL == 0) { - handleMemoryPressure() - } - val currentTime = System.currentTimeMillis() val shouldFlush = logBuffer.size >= config.maxBufferSize || (config.enableAsyncFlushing && (currentTime - lastFlushTime) >= config.flushIntervalMs) @@ -378,33 +373,11 @@ class LogFileWriterV2Impl( } } - private fun handleMemoryPressure() { - context?.let { - val activityManager = it.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - activityManager?.let { am -> - val memoryInfo = ActivityManager.MemoryInfo() - am.getMemoryInfo(memoryInfo) - - if (memoryInfo.availMem < memoryInfo.threshold * MEMORY_PRESSURE_MULTIPLIER) { - appLogger.w("Low memory detected, forcing log buffer flush") - // Force flush in a separate coroutine to avoid blocking - fileWriterCoroutineScope.launch { - bufferMutex.withLock { - flushBuffer() - lastFlushTime = System.currentTimeMillis() - } - } - } - } - } - } - companion object { private const val LOG_FILE_PREFIX = "wire" private const val ACTIVE_LOGGING_FILE_NAME = "${LOG_FILE_PREFIX}_logs.txt" private const val LOG_COMPRESSED_FILES_MAX_COUNT = 10 private const val LOG_COMPRESSED_FILE_EXTENSION = "gz" - private const val MEMORY_PRESSURE_CHECK_INTERVAL = 50 private const val MEMORY_PRESSURE_MULTIPLIER = 2 } } From ac80293a56ccc307d34ed9b42f891d03ab94aaee Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 23 Jun 2025 18:01:35 +0200 Subject: [PATCH 03/12] refactor(logging): remove unused MEMORY_PRESSURE_MULTIPLIER constant --- .../kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 58601989b28..d6aaa8216e9 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -378,6 +378,5 @@ class LogFileWriterV2Impl( private const val ACTIVE_LOGGING_FILE_NAME = "${LOG_FILE_PREFIX}_logs.txt" private const val LOG_COMPRESSED_FILES_MAX_COUNT = 10 private const val LOG_COMPRESSED_FILE_EXTENSION = "gz" - private const val MEMORY_PRESSURE_MULTIPLIER = 2 } } From 948574f03827e2139aa9a3802d2bfb9e677a9ebc Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 23 Jun 2025 18:06:34 +0200 Subject: [PATCH 04/12] remove not used value --- .../com/wire/android/util/logging/LogFileWriterV2Config.kt | 3 --- .../com/wire/android/util/logging/LogFileWriterV2Impl.kt | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt index d77bfc10060..55cb5d21e09 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Config.kt @@ -18,14 +18,11 @@ package com.wire.android.util.logging -import com.wire.android.BuildConfig - data class LogFileWriterV2Config( val flushIntervalMs: Long = DEFAULT_FLUSH_INTERVAL_MS, val maxBufferSize: Int = DEFAULT_MAX_BUFFER_SIZE, val bufferSizeBytes: Int = DEFAULT_BUFFER_SIZE_BYTES, val maxFileSize: Long = DEFAULT_MAX_FILE_SIZE_BYTES, - val enableAsyncFlushing: Boolean = BuildConfig.USE_ASYNC_FLUSH_LOGGING, val flushTimeoutMs: Long = DEFAULT_FLUSH_TIMEOUT_MS, val bufferLockTimeoutMs: Long = DEFAULT_BUFFER_LOCK_TIMEOUT_MS ) { diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index d6aaa8216e9..6ba3d1e508b 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -112,7 +112,6 @@ class LogFileWriterV2Impl( } // Start periodic flush job - if (config.enableAsyncFlushing) { flushJob = fileWriterCoroutineScope.launch { while (isActive) { delay(config.flushIntervalMs) @@ -132,7 +131,6 @@ class LogFileWriterV2Impl( } } } - } appLogger.i("KaliumFileWritter.start: Starting log collection.") waitInitializationJob.join() @@ -280,7 +278,7 @@ class LogFileWriterV2Impl( val currentTime = System.currentTimeMillis() val shouldFlush = logBuffer.size >= config.maxBufferSize || - (config.enableAsyncFlushing && (currentTime - lastFlushTime) >= config.flushIntervalMs) + ((currentTime - lastFlushTime) >= config.flushIntervalMs) if (shouldFlush) { flushBuffer() From a63eaf01345646dd43e066ce162c1348a15c14bf Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 25 Jun 2025 09:49:42 +0200 Subject: [PATCH 05/12] detekt --- .../wire/android/ui/debug/DebugScreenComposeTest.kt | 2 ++ .../kotlin/com/wire/android/di/LogWriterModule.kt | 4 ++-- .../kotlin/com/wire/android/ui/debug/DebugScreen.kt | 4 ++-- .../com/wire/android/util/logging/LogFileWriter.kt | 11 ++++++----- .../wire/android/util/logging/LogFileWriterV1Impl.kt | 6 ++---- .../wire/android/util/logging/LogFileWriterV2Impl.kt | 10 ++-------- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt b/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt index 812a2876ed5..9d4c55b309e 100644 --- a/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt +++ b/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.debug import androidx.compose.ui.test.junit4.createComposeRule import com.wire.android.extensions.waitUntilExists import com.wire.android.ui.WireTestTheme +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -40,6 +41,7 @@ class DebugScreenComposeTest { onDeleteLogs = {}, onDatabaseLoggerEnabledChanged = {}, onEnableWireCellsFeature = {}, + onFlushLogs = { CompletableDeferred(Unit) }, ) } } diff --git a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt index bdcf5174128..f90c9093484 100644 --- a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt @@ -20,7 +20,7 @@ package com.wire.android.di import android.content.Context import com.wire.android.BuildConfig -import com.wire.android.util.LogFileWriterV1Impl +import com.wire.android.util.logging.LogFileWriterV1Impl import com.wire.android.util.logging.LogFileWriter import com.wire.android.util.logging.LogFileWriterV2Impl import dagger.Module @@ -39,7 +39,7 @@ class LogWriterModule { fun provideKaliumFileWriter(@ApplicationContext context: Context): LogFileWriter { if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) { val logsDirectory = LogFileWriter.logsDirectory(context) - return LogFileWriterV2Impl(logsDirectory, context) + return LogFileWriterV2Impl(logsDirectory) } else { val logsDirectory = LogFileWriter.logsDirectory(context) return LogFileWriterV1Impl(logsDirectory) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index ac371a1e047..b22bcfa09dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt @@ -61,6 +61,7 @@ import com.wire.android.util.AppNameUtil import com.wire.android.util.getMimeType import com.wire.android.util.getUrisOfFilesInDirectory import com.wire.android.util.multipleFileSharingIntent +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.launch @@ -221,7 +222,6 @@ data class DebugContentState( coroutineScope.launch { // Flush any buffered logs before sharing to ensure completeness onFlushLogs().await() - val dir = File(logPath).parentFile val fileUris = if (dir != null && dir.exists()) context.getUrisOfFilesInDirectory(dir) else arrayListOf() @@ -248,7 +248,7 @@ internal fun PreviewUserDebugContent() = WireTheme { onNavigationPressed = {}, onLoggingEnabledChange = {}, onDeleteLogs = {}, - onFlushLogs = { kotlinx.coroutines.CompletableDeferred(Unit) }, + onFlushLogs = { CompletableDeferred(Unit) }, onDatabaseLoggerEnabledChanged = {}, onEnableWireCellsFeature = {}, ) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt index 769e80aeffa..99af4188b8e 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriter.kt @@ -26,29 +26,30 @@ import java.io.File * between different implementations. */ interface LogFileWriter { - + /** * The active logging file where logs are currently being written */ val activeLoggingFile: File - + /** * Starts the log collection system */ suspend fun start() - + /** * Stops the log collection system */ suspend fun stop() - + /** * Forces a flush of any pending logs to ensure they are written to file */ suspend fun forceFlush() - + /** * Deletes all log files including active and compressed files + * */ fun deleteAllLogFiles() diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt index 547e55975ca..d0634a6a12b 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV1Impl.kt @@ -16,11 +16,9 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.util +package com.wire.android.util.logging -import android.content.Context import com.wire.android.appLogger -import com.wire.android.util.logging.LogFileWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -44,7 +42,7 @@ import java.util.Locale import java.util.zip.GZIPOutputStream @Suppress("TooGenericExceptionCaught") -class LogFileWriterV1Impl(private val logsDirectory: File): LogFileWriter { +class LogFileWriterV1Impl(private val logsDirectory: File) : LogFileWriter { private val logFileTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 6ba3d1e508b..2cd5be43660 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -18,8 +18,6 @@ package com.wire.android.util.logging -import android.app.ActivityManager -import android.content.Context import com.wire.android.appLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,7 +52,6 @@ import java.util.zip.GZIPOutputStream @Suppress("TooGenericExceptionCaught", "TooManyFunctions") class LogFileWriterV2Impl( private val logsDirectory: File, - private val context: Context?, private val config: LogFileWriterV2Config = LogFileWriterV2Config.default() ) : LogFileWriter { @@ -71,7 +68,7 @@ class LogFileWriterV2Impl( private val bufferMutex = Mutex() private var lastFlushTime = 0L private var bufferedWriter: BufferedWriter? = null - + // Process management private var logcatProcess: Process? = null @@ -180,7 +177,6 @@ class LogFileWriterV2Impl( try { // Stop logcat process first to prevent new logs stopLogcatProcess() - // Cancel jobs with timeout to avoid hanging writingJob?.let { job -> try { @@ -219,7 +215,6 @@ class LogFileWriterV2Impl( } finally { // Ensure resources are cleaned up regardless of exceptions closeResources() - try { clearActiveLoggingFileContent() } catch (e: Exception) { @@ -227,7 +222,7 @@ class LogFileWriterV2Impl( } } } - + private fun closeResources() { try { bufferedWriter?.close() @@ -352,7 +347,6 @@ class LogFileWriterV2Impl( if (logBuffer.isEmpty()) return val linesToWrite = logBuffer.toList() logBuffer.clear() - try { // Use BufferedWriter for efficient writing val writer = bufferedWriter ?: BufferedWriter( From 0c1cc7decf50a3702f24af59cba521dc3a2f12cf Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 25 Jun 2025 09:58:29 +0200 Subject: [PATCH 06/12] detekt --- app/src/main/kotlin/com/wire/android/WireApplication.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index c55407226f5..f4acaa1c405 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -62,6 +62,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import javax.inject.Inject +@Suppress("TooManyFunctions") @HiltAndroidApp class WireApplication : BaseApp() { From 8268a73b3b4a199765c16bc086f47ac8227f433e Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 20 Aug 2025 13:24:05 +0200 Subject: [PATCH 07/12] detekt --- app/src/main/kotlin/com/wire/android/services/CallService.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/services/CallService.kt b/app/src/main/kotlin/com/wire/android/services/CallService.kt index baf9ef81b3f..4fe78858383 100644 --- a/app/src/main/kotlin/com/wire/android/services/CallService.kt +++ b/app/src/main/kotlin/com/wire/android/services/CallService.kt @@ -31,8 +31,6 @@ import com.wire.android.notification.CallNotificationManager import com.wire.android.notification.NotificationIds import com.wire.android.services.CallService.Action import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.logging.logIfEmptyUserName -import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.id.ConversationId From abf7c440928c407af88966b169f2b2b205943433 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 27 Aug 2025 11:39:18 +0200 Subject: [PATCH 08/12] fix merge conflicts --- .../main/kotlin/com/wire/android/services/CallServiceManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/services/CallServiceManager.kt b/app/src/main/kotlin/com/wire/android/services/CallServiceManager.kt index 06c08fe4937..3df3cb219fc 100644 --- a/app/src/main/kotlin/com/wire/android/services/CallServiceManager.kt +++ b/app/src/main/kotlin/com/wire/android/services/CallServiceManager.kt @@ -21,7 +21,7 @@ import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic import com.wire.android.notification.CallNotificationData import com.wire.android.services.CallService.Action -import com.wire.android.util.logIfEmptyUserName +import com.wire.android.util.logging.logIfEmptyUserName import com.wire.kalium.common.functional.Either import com.wire.kalium.common.functional.fold import com.wire.kalium.common.functional.left From 12b072090630a505e68ece82c280db5a24f68a89 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 8 Oct 2025 10:45:58 +0200 Subject: [PATCH 09/12] Update app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt Co-authored-by: Yamil Medina --- .../util/logging/LogFileWriterV2Impl.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 2cd5be43660..06a6157aca9 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -345,6 +345,28 @@ class LogFileWriterV2Impl( private fun flushBuffer() { if (logBuffer.isEmpty()) return + + try { + // Use BufferedWriter for efficient writing + val writer = bufferedWriter ?: BufferedWriter( + FileWriter(activeLoggingFile, true), + config.bufferSizeBytes + ).also { bufferedWriter = it } + + // Like here one, Write directly from buffer without copying to the .toList() + logBuffer.forEach { line -> + writer.appendLine(line) + } + writer.flush() + + // and here two, Clear only after successful write + logBuffer.clear() + + } catch (e: IOException) { + appLogger.e("Failed to flush log buffer", e) + } + } + if (logBuffer.isEmpty()) return val linesToWrite = logBuffer.toList() logBuffer.clear() try { From 745718f4ea5b84ef524f401287b2b583b45ab247 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 8 Oct 2025 10:47:43 +0200 Subject: [PATCH 10/12] Update app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt Co-authored-by: Yamil Medina --- .../com/wire/android/util/logging/LogFileWriterV2Impl.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 06a6157aca9..253e24d1a43 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -255,9 +255,9 @@ class LogFileWriterV2Impl( private fun clearActiveLoggingFileContent() { if (activeLoggingFile.exists()) { - val writer = PrintWriter(activeLoggingFile) - writer.print("") - writer.close() + PrintWriter(activeLoggingFile).use { writer -> + writer.print("") + } } } From e6cc9de17d3c271fb9efd31fde9469bc7423e11c Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 8 Oct 2025 11:34:24 +0200 Subject: [PATCH 11/12] pr comments --- .../util/logging/LogFileWriterV2Impl.kt | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 253e24d1a43..862fbb072aa 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -366,26 +366,6 @@ class LogFileWriterV2Impl( appLogger.e("Failed to flush log buffer", e) } } - if (logBuffer.isEmpty()) return - val linesToWrite = logBuffer.toList() - logBuffer.clear() - try { - // Use BufferedWriter for efficient writing - val writer = bufferedWriter ?: BufferedWriter( - FileWriter(activeLoggingFile, true), - config.bufferSizeBytes - ).also { bufferedWriter = it } - - linesToWrite.forEach { line -> - writer.appendLine(line) - } - writer.flush() - } catch (e: IOException) { - appLogger.e("Failed to flush log buffer", e) - // Re-add failed lines back to buffer for retry - logBuffer.addAll(0, linesToWrite) - } - } companion object { private const val LOG_FILE_PREFIX = "wire" From 83c91d461806e085b023c0e262fd03997f8540f9 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Wed, 8 Oct 2025 12:15:09 +0200 Subject: [PATCH 12/12] enable the async logger for for beta --- .../util/logging/LogFileWriterV2Impl.kt | 35 +++++++++---------- default.json | 3 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt index 862fbb072aa..72322295a09 100644 --- a/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt +++ b/app/src/main/kotlin/com/wire/android/util/logging/LogFileWriterV2Impl.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow @@ -37,8 +39,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.cancelAndJoin import java.io.BufferedWriter import java.io.File import java.io.FileWriter @@ -109,25 +109,25 @@ class LogFileWriterV2Impl( } // Start periodic flush job - flushJob = fileWriterCoroutineScope.launch { - while (isActive) { - delay(config.flushIntervalMs) - try { - withTimeout(config.bufferLockTimeoutMs) { - bufferMutex.withLock { - if (logBuffer.isNotEmpty()) { - flushBuffer() - lastFlushTime = System.currentTimeMillis() - } + flushJob = fileWriterCoroutineScope.launch { + while (isActive) { + delay(config.flushIntervalMs) + try { + withTimeout(config.bufferLockTimeoutMs) { + bufferMutex.withLock { + if (logBuffer.isNotEmpty()) { + flushBuffer() + lastFlushTime = System.currentTimeMillis() } } - } catch (e: TimeoutCancellationException) { - appLogger.w("Periodic flush timed out, buffer may be locked by another operation") - } catch (e: Exception) { - appLogger.e("Error during periodic flush", e) } + } catch (e: TimeoutCancellationException) { + appLogger.w("Periodic flush timed out, buffer may be locked by another operation") + } catch (e: Exception) { + appLogger.e("Error during periodic flush", e) } } + } appLogger.i("KaliumFileWritter.start: Starting log collection.") waitInitializationJob.join() @@ -273,7 +273,7 @@ class LogFileWriterV2Impl( val currentTime = System.currentTimeMillis() val shouldFlush = logBuffer.size >= config.maxBufferSize || - ((currentTime - lastFlushTime) >= config.flushIntervalMs) + ((currentTime - lastFlushTime) >= config.flushIntervalMs) if (shouldFlush) { flushBuffer() @@ -361,7 +361,6 @@ class LogFileWriterV2Impl( // and here two, Clear only after successful write logBuffer.clear() - } catch (e: IOException) { appLogger.e("Failed to flush log buffer", e) } diff --git a/default.json b/default.json index 503a99a92b9..3346b564f7a 100644 --- a/default.json +++ b/default.json @@ -69,7 +69,8 @@ "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", "analytics_server_url": "https://wire.count.ly/", "enable_new_registration": true, - "emm_support_enabled": true + "emm_support_enabled": true, + "use_async_flush_logging" : true }, "internal": { "application_id": "com.wire.internal",