diff --git a/utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt index 2b1cd2637a1..3cd6f8707e1 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt @@ -11,6 +11,8 @@ import org.oppia.android.app.model.EventLog.ConsoleLoggerContext import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.threading.BlockingDispatcher import java.io.File +import java.io.FileWriter +import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton @@ -33,6 +35,8 @@ class ConsoleLogger @Inject constructor( */ val logErrorMessagesFlow: SharedFlow = _logErrorMessagesFlow + private var printWriter: PrintWriter? = null + /** Logs a verbose message with the specified tag. */ fun v(tag: String, msg: String) { writeLog(LogLevel.VERBOSE, tag, msg) @@ -73,12 +77,12 @@ class ConsoleLogger @Inject constructor( writeError(LogLevel.WARNING, tag, msg, tr) } - /** Logs a error message with the specified tag. */ + /** Logs an error message with the specified tag. */ fun e(tag: String, msg: String) { writeLog(LogLevel.ERROR, tag, msg) } - /** Logs a error message with the specified tag, message and exception. */ + /** Logs an error message with the specified tag, message and exception. */ fun e(tag: String, msg: String, tr: Throwable?) { writeError(LogLevel.ERROR, tag, msg, tr) } @@ -109,7 +113,7 @@ class ConsoleLogger @Inject constructor( } // Add the log to the error message flow so it can be logged to firebase. - CoroutineScope(blockingDispatcher).launch { + blockingScope.launch { // Only log error messages to firebase. if (logLevel == LogLevel.ERROR) { _logErrorMessagesFlow.emit( @@ -124,10 +128,17 @@ class ConsoleLogger @Inject constructor( } /** - * Writes the specified text line to file in a background thread to ensure that saving messages don't block the main - * thread. A blocking dispatcher is used to ensure messages are written in order. + * Writes the specified text line to file in a background thread to ensure that saving messages + * doesn't block the main thread. A blocking dispatcher is used to ensure messages are written + * in order. */ private fun logToFileInBackground(text: String) { - blockingScope.launch { logDirectory.printWriter().use { out -> out.println(text) } } + blockingScope.launch { + if (printWriter == null) { + printWriter = PrintWriter(FileWriter(logDirectory, true)) // Open in append mode. + } + printWriter?.println(text) + printWriter?.flush() + } } } diff --git a/utility/src/test/java/org/oppia/android/util/logging/ConsoleLoggerTest.kt b/utility/src/test/java/org/oppia/android/util/logging/ConsoleLoggerTest.kt index 6a314de75fe..05df6b8c3c7 100644 --- a/utility/src/test/java/org/oppia/android/util/logging/ConsoleLoggerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/logging/ConsoleLoggerTest.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,6 +32,8 @@ import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.testing.LocaleTestModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.io.File +import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton @@ -54,9 +57,31 @@ class ConsoleLoggerTest { @field:[Inject BackgroundTestDispatcher] lateinit var backgroundTestDispatcher: TestCoroutineDispatcher + private lateinit var logFile: File + @Before fun setUp() { setUpTestApplicationComponent() + logFile = File(context.filesDir, "oppia_app.log") + logFile.delete() + } + + @After + fun tearDown() { + logFile.delete() + } + + @Test + fun testConsoleLogger_multipleLogCalls_appendsToFile() { + consoleLogger.e(testTag, testMessage) + consoleLogger.e(testTag, "$testMessage 2") + testCoroutineDispatchers.advanceUntilIdle() + + val logContent = logFile.readText() + assertThat(logContent).contains(testMessage) + assertThat(logContent).contains("$testMessage 2") + assertThat(logContent.indexOf(testMessage)) + .isLessThan(logContent.indexOf("$testMessage 2")) } @Test @@ -76,6 +101,26 @@ class ConsoleLoggerTest { assertThat(firstErrorContext.fullErrorLog).isEqualTo(testMessage) } + @Test + fun testConsoleLogger_closeAndReopen_continuesToAppend() { + consoleLogger.e(testTag, "first $testMessage") + testCoroutineDispatchers.advanceUntilIdle() + + // Force close the PrintWriter to simulate app restart + val printWriterField = ConsoleLogger::class.java.getDeclaredField("printWriter") + printWriterField.isAccessible = true + (printWriterField.get(consoleLogger) as? PrintWriter)?.close() + printWriterField.set(consoleLogger, null) + + consoleLogger.e(testTag, "first $testMessage") + consoleLogger.e(testTag, "second $testMessage") + testCoroutineDispatchers.advanceUntilIdle() + + val logContent = logFile.readText() + assertThat(logContent).contains("first $testMessage") + assertThat(logContent).contains("second $testMessage") + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } @@ -84,9 +129,7 @@ class ConsoleLoggerTest { class TestModule { @Provides @Singleton - fun provideContext(application: Application): Context { - return application - } + fun provideContext(application: Application): Context = application @Provides @Singleton @@ -96,7 +139,7 @@ class ConsoleLoggerTest { @Provides @Singleton @EnableFileLog - fun provideEnableFileLog(): Boolean = false + fun provideEnableFileLog(): Boolean = true @Provides @Singleton @@ -113,7 +156,6 @@ class ConsoleLoggerTest { FakeOppiaClockModule::class, ] ) - interface TestApplicationComponent : DataProvidersInjector { @Component.Builder interface Builder {