diff --git a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt index 6dd0d69dc0..40016922ae 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt @@ -10,11 +10,10 @@ package io.element.android.x.initializer import android.content.Context import android.system.Os import androidx.startup.Initializer -import io.element.android.features.rageshake.api.reporter.BugReporter +import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.libraries.architecture.bindings import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.tracing.TracingConfiguration -import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.x.di.AppBindings import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -34,7 +33,7 @@ class PlatformInitializer : Initializer { val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() } val tracingConfiguration = TracingConfiguration( writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) }, - writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter), + writesToFilesConfiguration = bugReporter.createWriteToFilesConfiguration(), logLevel = logLevel, extraTargets = listOf(ELEMENT_X_TARGET), traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() }, @@ -45,14 +44,5 @@ class PlatformInitializer : Initializer { Os.setenv("RUST_BACKTRACE", "1", true) } - private fun defaultWriteToDiskConfiguration(bugReporter: BugReporter): WriteToFilesConfiguration.Enabled { - return WriteToFilesConfiguration.Enabled( - directory = bugReporter.logDirectory().absolutePath, - filenamePrefix = "logs", - // Keep a maximum of 1 week of log files. - numberOfFiles = 7 * 24, - ) - } - override fun dependencies(): List>> = mutableListOf() } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index ae9f935061..db512ff2ea 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -36,6 +36,7 @@ import io.element.android.appnav.root.RootView import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.api.LoginParams import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -73,6 +74,7 @@ class RootFlowNode @AssistedInject constructor( private val signedOutEntryPoint: SignedOutEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, + private val bugReporter: BugReporter, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.SplashScreen, @@ -123,6 +125,7 @@ class RootFlowNode @AssistedInject constructor( private fun switchToNotLoggedInFlow(params: LoginParams?) { matrixSessionCache.removeAll() + bugReporter.setLogDirectorySubfolder(null) backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt index 302a12d99b..1e173474cc 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt @@ -42,12 +42,7 @@ class MatrixSessionCache @Inject constructor( init { authenticationService.listenToNewMatrixClients { matrixClient -> - val syncOrchestrator = syncOrchestratorFactory.create(matrixClient) - sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( - matrixClient = matrixClient, - syncOrchestrator = syncOrchestrator, - ) - syncOrchestrator.start() + onNewMatrixClient(matrixClient) } } @@ -105,17 +100,21 @@ class MatrixSessionCache @Inject constructor( Timber.d("Restore matrix session: $sessionId") return authenticationService.restoreSession(sessionId) .onSuccess { matrixClient -> - val syncOrchestrator = syncOrchestratorFactory.create(matrixClient) - sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( - matrixClient = matrixClient, - syncOrchestrator = syncOrchestrator, - ) - syncOrchestrator.start() + onNewMatrixClient(matrixClient) } .onFailure { Timber.e(it, "Fail to restore session") } } + + private fun onNewMatrixClient(matrixClient: MatrixClient) { + val syncOrchestrator = syncOrchestratorFactory.create(matrixClient) + sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( + matrixClient = matrixClient, + syncOrchestrator = syncOrchestrator, + ) + syncOrchestrator.start() + } } private data class InMemoryMatrixSession( diff --git a/features/rageshake/api/build.gradle.kts b/features/rageshake/api/build.gradle.kts index f47b748c3e..7fe1620613 100644 --- a/features/rageshake/api/build.gradle.kts +++ b/features/rageshake/api/build.gradle.kts @@ -16,5 +16,6 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) implementation(projects.libraries.androidutils) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.uiStrings) } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/WriteToFilesConfigurationFactory.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/WriteToFilesConfigurationFactory.kt new file mode 100644 index 0000000000..52298b5414 --- /dev/null +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/WriteToFilesConfigurationFactory.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.api.logs + +import io.element.android.features.rageshake.api.reporter.BugReporter +import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration + +fun BugReporter.createWriteToFilesConfiguration(): WriteToFilesConfiguration { + return WriteToFilesConfiguration.Enabled( + directory = logDirectory().absolutePath, + filenamePrefix = "logs", + // Keep a maximum of 1 week of log files. + numberOfFiles = 7 * 24, + ) +} diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt index 1cc3cc8908..bb625370cf 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt @@ -34,6 +34,14 @@ interface BugReporter { */ fun logDirectory(): File + /** + * Set the subfolder name for the log directory. + * This will create a subfolder in the log directory with the given name. + * It will also configure the Rust SDK to use this subfolder for its logs. + * If the name is null, the log files will be stored in the base folder for the logs. + */ + fun setLogDirectorySubfolder(subfolderName: String?) + /** * Set the current tracing log level. */ diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index b9ed1c7778..028b671eb6 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -13,6 +13,7 @@ import androidx.core.net.toFile import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.RageshakeConfig +import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener import io.element.android.features.rageshake.impl.crash.CrashDataStore @@ -28,11 +29,14 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.SdkMetadata +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -71,6 +75,8 @@ class DefaultBugReporter @Inject constructor( private val bugReporterUrlProvider: BugReporterUrlProvider, private val sdkMetadata: SdkMetadata, private val matrixClientProvider: MatrixClientProvider, + private val tracingService: TracingService, + matrixAuthenticationService: MatrixAuthenticationService, ) : BugReporter { companion object { // filenames @@ -81,7 +87,24 @@ class DefaultBugReporter @Inject constructor( private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") private var currentTracingLogLevel: String? = null - private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME) + private val logCatErrFile: File + get() = File(logDirectory(), LOG_CAT_FILENAME) + private val baseLogDirectory = File(context.cacheDir, LOG_DIRECTORY_NAME) + private var currentLogDirectory: File = baseLogDirectory + + init { + if (buildMeta.isEnterpriseBuild) { + val logSubfolder = runBlocking { + sessionStore.getLatestSession() + }?.userId?.substringAfter(":") + setCurrentLogDirectory(logSubfolder) + matrixAuthenticationService.listenToNewMatrixClients { + // When a new Matrix client is created, we update the tracing configuration to write + // the files in a dedicated subfolders. + setLogDirectorySubfolder(it.userIdServerName()) + } + } + } override suspend fun sendBugReport( withDevicesLogs: Boolean, @@ -286,16 +309,44 @@ class DefaultBugReporter @Inject constructor( } override fun logDirectory(): File { - return File(context.cacheDir, LOG_DIRECTORY_NAME).apply { + return currentLogDirectory.apply { mkdirs() } } + override fun setLogDirectorySubfolder(subfolderName: String?) { + if (buildMeta.isEnterpriseBuild) { + setCurrentLogDirectory(subfolderName) + tracingService.updateWriteToFilesConfiguration(createWriteToFilesConfiguration()) + } + } + + private fun setCurrentLogDirectory(subfolderName: String?) { + currentLogDirectory = if (subfolderName == null) { + baseLogDirectory + } else { + File(baseLogDirectory, subfolderName) + } + } + suspend fun deleteAllFiles(predicate: (File) -> Boolean) { withContext(coroutineDispatchers.io) { - getLogFiles() - .filter(predicate) - .forEach { it.safeDelete() } + deleteAllFilesRecursive(baseLogDirectory, predicate) + } + } + + private fun deleteAllFilesRecursive( + directory: File, + predicate: (File) -> Boolean, + ) { + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + deleteAllFilesRecursive(file, predicate) + } else { + if (predicate(file)) { + file.safeDelete() + } + } } } @@ -325,11 +376,12 @@ class DefaultBugReporter @Inject constructor( * @return the file if the operation succeeds */ override fun saveLogCat() { - if (logCatErrFile.exists()) { - logCatErrFile.safeDelete() + val file = logCatErrFile + if (file.exists()) { + file.safeDelete() } try { - logCatErrFile.writer().use { + file.writer().use { getLogCatError(it) } } catch (error: OutOfMemoryError) { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt index 1f10cd0ab1..ebaa524bd5 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt @@ -53,6 +53,10 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter { return File("fake") } + override fun setLogDirectorySubfolder(subfolderName: String?) { + // No op + } + override fun setCurrentTracingLogLevel(logLevel: String) { // No op } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index e73c7863b4..c7c424bfae 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -10,16 +10,26 @@ package io.element.android.features.rageshake.impl.reporter import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.RageshakeConfig import io.element.android.features.rageshake.api.reporter.BugReporterListener +import io.element.android.features.rageshake.impl.crash.CrashDataStore import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.tracing.TracingService +import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeSdkMetadata +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.tracing.FakeTracingService import io.element.android.libraries.network.useragent.DefaultUserAgentProvider +import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -45,7 +55,7 @@ class DefaultBugReporterTest { .setResponseCode(200) ) server.start() - val sut = createDefaultBugReporter(server) + val sut = createDefaultBugReporter(server = server) var onUploadCancelledCalled = false var onUploadFailedCalled = false val progressValues = mutableListOf() @@ -97,22 +107,14 @@ class DefaultBugReporterTest { storeData(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH")) } - val buildMeta = aBuildMeta() val fakeEncryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService) fakeEncryptionService.givenDeviceKeys("CURVECURVECURVE", "EDKEYEDKEYEDKY") - val sut = DefaultBugReporter( - context = RuntimeEnvironment.getApplication(), - screenshotHolder = FakeScreenshotHolder(), + val sut = createDefaultBugReporter( + server = server, crashDataStore = FakeCrashDataStore(), - coroutineDispatchers = testCoroutineDispatchers(), - okHttpClient = { OkHttpClient.Builder().build() }, - userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), sessionStore = mockSessionStore, - buildMeta = buildMeta, - bugReporterUrlProvider = { server.url("/") }, - sdkMetadata = FakeSdkMetadata("123456789"), matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) ) @@ -166,22 +168,13 @@ class DefaultBugReporterTest { storeData(aSessionData("@foo:example.com", "ABCDEFGH")) } - val buildMeta = aBuildMeta() val fakeEncryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService) fakeEncryptionService.givenDeviceKeys(null, null) - val sut = DefaultBugReporter( - context = RuntimeEnvironment.getApplication(), - screenshotHolder = FakeScreenshotHolder(), - crashDataStore = FakeCrashDataStore(), - coroutineDispatchers = testCoroutineDispatchers(), - okHttpClient = { OkHttpClient.Builder().build() }, - userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), + val sut = createDefaultBugReporter( + server = server, sessionStore = mockSessionStore, - buildMeta = buildMeta, - bugReporterUrlProvider = { server.url("/") }, - sdkMetadata = FakeSdkMetadata("123456789"), matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) ) @@ -209,21 +202,13 @@ class DefaultBugReporterTest { ) server.start() - val buildMeta = aBuildMeta() val fakeEncryptionService = FakeEncryptionService() fakeEncryptionService.givenDeviceKeys(null, null) - val sut = DefaultBugReporter( - context = RuntimeEnvironment.getApplication(), - screenshotHolder = FakeScreenshotHolder(), + val sut = createDefaultBugReporter( + server = server, crashDataStore = FakeCrashDataStore("I did crash", true), - coroutineDispatchers = testCoroutineDispatchers(), - okHttpClient = { OkHttpClient.Builder().build() }, - userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), sessionStore = InMemorySessionStore(), - buildMeta = buildMeta, - bugReporterUrlProvider = { server.url("/") }, - sdkMetadata = FakeSdkMetadata("123456789"), matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(Exception("Mock no client")) }) ) @@ -276,7 +261,7 @@ class DefaultBugReporterTest { .setBody("""{"error": "An error body"}""") ) server.start() - val sut = createDefaultBugReporter(server) + val sut = createDefaultBugReporter(server = server) var onUploadCancelledCalled = false var onUploadFailedCalled = false var onUploadFailedReason: String? = null @@ -318,22 +303,172 @@ class DefaultBugReporterTest { assertThat(onUploadSucceedCalled).isFalse() } + @Test + fun `the log directory is initialized using the last session store data`() = runTest { + val sut = createDefaultBugReporter( + buildMeta = aBuildMeta(isEnterpriseBuild = true), + sessionStore = InMemorySessionStore().apply { + storeData(aSessionData(sessionId = "@alice:domain.com")) + } + ) + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs/domain.com") + } + + @Test + fun `foss build - the log directory is initialized to the root log directory`() = runTest { + val sut = createDefaultBugReporter( + sessionStore = InMemorySessionStore().apply { + storeData(aSessionData(sessionId = "@alice:domain.com")) + } + ) + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") + } + + @Test + fun `when the log directory is updated, the tracing service is invoked`() = runTest { + var param: WriteToFilesConfiguration? = null + val updateWriteToFilesConfigurationResult = lambdaRecorder { + param = it + } + val sut = createDefaultBugReporter( + buildMeta = aBuildMeta(isEnterpriseBuild = true), + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ), + ) + sut.setLogDirectorySubfolder("my.sub.folder") + updateWriteToFilesConfigurationResult.assertions().isCalledOnce() + assertThat(param).isNotNull() + assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) + assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/my.sub.folder") + assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs") + assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) + assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") + } + + @Test + fun `foss build - when the log directory is updated, the tracing service is not invoked`() = runTest { + val updateWriteToFilesConfigurationResult = lambdaRecorder {} + val sut = createDefaultBugReporter( + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ) + ) + sut.setLogDirectorySubfolder("my.sub.folder") + updateWriteToFilesConfigurationResult.assertions().isNeverCalled() + } + + @Test + fun `when the log directory is reset, the tracing service is invoked`() = runTest { + var param: WriteToFilesConfiguration? = null + val updateWriteToFilesConfigurationResult = lambdaRecorder { + param = it + } + val sut = createDefaultBugReporter( + buildMeta = aBuildMeta(isEnterpriseBuild = true), + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ), + ) + sut.setLogDirectorySubfolder(null) + updateWriteToFilesConfigurationResult.assertions().isCalledOnce() + assertThat(param).isNotNull() + assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) + assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs") + assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs") + assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) + assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") + } + + @Test + fun `foss build - when the log directory is reset, the tracing service is not invoked`() = runTest { + val updateWriteToFilesConfigurationResult = lambdaRecorder {} + val sut = createDefaultBugReporter( + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ) + ) + sut.setLogDirectorySubfolder(null) + updateWriteToFilesConfigurationResult.assertions().isNeverCalled() + } + + @Test + fun `when a new MatrixClient is created the logs folder is updated`() = runTest { + var param: WriteToFilesConfiguration? = null + val updateWriteToFilesConfigurationResult = lambdaRecorder { + param = it + } + val matrixAuthenticationService = FakeMatrixAuthenticationService().apply { + givenMatrixClient( + FakeMatrixClient( + userIdServerNameLambda = { "domain.foo.org" }, + ) + ) + } + val sut = createDefaultBugReporter( + buildMeta = aBuildMeta(isEnterpriseBuild = true), + matrixAuthenticationService = matrixAuthenticationService, + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ) + ) + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") + matrixAuthenticationService.login("alice", "password") + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs/domain.foo.org") + updateWriteToFilesConfigurationResult.assertions().isCalledOnce() + assertThat(param).isNotNull() + assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) + assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/domain.foo.org") + assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs") + assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) + assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") + } + + @Test + fun `foss build - when a new MatrixClient is created the logs folder is not updated`() = runTest { + val updateWriteToFilesConfigurationResult = lambdaRecorder {} + val matrixAuthenticationService = FakeMatrixAuthenticationService().apply { + givenMatrixClient( + FakeMatrixClient( + userIdServerNameLambda = { "domain.foo.org" }, + ) + ) + } + val sut = createDefaultBugReporter( + matrixAuthenticationService = matrixAuthenticationService, + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ) + ) + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") + matrixAuthenticationService.login("alice", "password") + assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") + updateWriteToFilesConfigurationResult.assertions().isNeverCalled() + } + private fun TestScope.createDefaultBugReporter( - server: MockWebServer + buildMeta: BuildMeta = aBuildMeta(), + sessionStore: SessionStore = InMemorySessionStore(), + matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), + crashDataStore: CrashDataStore = FakeCrashDataStore(), + server: MockWebServer = MockWebServer(), + tracingService: TracingService = FakeTracingService(), + matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), ): DefaultBugReporter { - val buildMeta = aBuildMeta() return DefaultBugReporter( context = RuntimeEnvironment.getApplication(), screenshotHolder = FakeScreenshotHolder(), - crashDataStore = FakeCrashDataStore(), + crashDataStore = crashDataStore, coroutineDispatchers = testCoroutineDispatchers(), okHttpClient = { OkHttpClient.Builder().build() }, userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), - sessionStore = InMemorySessionStore(), + sessionStore = sessionStore, buildMeta = buildMeta, bugReporterUrlProvider = { server.url("/") }, sdkMetadata = FakeSdkMetadata("123456789"), - matrixClientProvider = FakeMatrixClientProvider() + matrixClientProvider = matrixClientProvider, + tracingService = tracingService, + matrixAuthenticationService = matrixAuthenticationService, ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt index 5cef6cde02..8c183c80b4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt @@ -11,4 +11,6 @@ import timber.log.Timber interface TracingService { fun createTimberTree(target: String): Timber.Tree + + fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index a1ffbc5da9..1a044a9f4a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -71,9 +71,9 @@ class RustMatrixAuthenticationService @Inject constructor( private var currentClient: Client? = null private var currentHomeserver = MutableStateFlow(null) - private var newMatrixClientObserver: ((MatrixClient) -> Unit)? = null + private val newMatrixClientObservers = mutableListOf<(MatrixClient) -> Unit>() override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) { - newMatrixClientObserver = lambda + newMatrixClientObservers.add(lambda) } private fun rotateSessionPath(): SessionPaths { @@ -155,7 +155,8 @@ class RustMatrixAuthenticationService @Inject constructor( passphrase = pendingPassphrase, sessionPaths = currentSessionPaths, ) - newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) + val matrixClient = rustMatrixClientFactory.create(client) + newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.storeData(sessionData) // Clean up the strong reference held here since it's no longer necessary @@ -246,7 +247,8 @@ class RustMatrixAuthenticationService @Inject constructor( pendingOAuthAuthorizationData?.close() pendingOAuthAuthorizationData = null - newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) + val matrixClient = rustMatrixClientFactory.create(client) + newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.storeData(sessionData) // Clean up the strong reference held here since it's no longer necessary @@ -290,7 +292,8 @@ class RustMatrixAuthenticationService @Inject constructor( passphrase = pendingPassphrase, sessionPaths = emptySessionPaths, ) - newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) + val matrixClient = rustMatrixClientFactory.create(client) + newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.storeData(sessionData) // Clean up the strong reference held here since it's no longer necessary diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt index 912561d843..728bf136b8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import org.matrix.rustcomponents.sdk.TracingFileConfiguration +import org.matrix.rustcomponents.sdk.reloadTracingFileWriter import timber.log.Timber import javax.inject.Inject @@ -23,6 +24,12 @@ class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : override fun createTimberTree(target: String): Timber.Tree { return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable) } + + override fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration) { + config.toTracingFileConfiguration()?.let { + reloadTracingFileWriter(it) + } + } } private fun LogLevel.toRustLogLevel(): org.matrix.rustcomponents.sdk.LogLevel { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/tracing/FakeTracingService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/tracing/FakeTracingService.kt new file mode 100644 index 0000000000..52ffe8f32d --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/tracing/FakeTracingService.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.tracing + +import io.element.android.libraries.matrix.api.tracing.TracingService +import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration +import io.element.android.tests.testutils.lambda.lambdaError +import timber.log.Timber + +class FakeTracingService( + private val createTimberTreeResult: (String) -> Timber.Tree = { lambdaError() }, + private val updateWriteToFilesConfigurationResult: (WriteToFilesConfiguration) -> Unit = { lambdaError() } +) : TracingService { + override fun createTimberTree(target: String): Timber.Tree { + return createTimberTreeResult(target) + } + + override fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration) { + updateWriteToFilesConfigurationResult(config) + } +}