Log errors in a file
This commit is contained in:
committed by
pynicolas
parent
f805201768
commit
f4aad46cb6
@@ -38,7 +38,7 @@ class DocumentDetectionTest {
|
||||
assertEquals("org.fairscan.app", appContext.packageName)
|
||||
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val segmentationService = ImageSegmentationService(context)
|
||||
val segmentationService = ImageSegmentationService(context) { _, _, _ -> }
|
||||
segmentationService.initialize()
|
||||
OpenCVLoader.initLocal()
|
||||
|
||||
|
||||
@@ -17,10 +17,20 @@ package org.fairscan.app
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import org.fairscan.app.data.FileLogger
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.data.LogRepository
|
||||
import org.fairscan.app.data.PdfFileManager
|
||||
import org.fairscan.app.data.recentDocumentsDataStore
|
||||
import org.fairscan.app.domain.ImageSegmentationService
|
||||
import org.fairscan.app.platform.AndroidPdfWriter
|
||||
import org.fairscan.app.platform.OpenCvTransformations
|
||||
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
||||
import org.fairscan.app.ui.screens.export.ExportViewModel
|
||||
import org.fairscan.app.ui.screens.home.HomeViewModel
|
||||
import java.io.File
|
||||
|
||||
class FairScanApp : Application() {
|
||||
@@ -43,4 +53,21 @@ class AppContainer(context: Context) {
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
AndroidPdfWriter()
|
||||
)
|
||||
val logger = FileLogger(LogRepository(File(context.filesDir, "logs.txt")))
|
||||
val imageSegmentationService = ImageSegmentationService(context, logger)
|
||||
val recentDocumentsDataStore = context.recentDocumentsDataStore
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified VM : ViewModel> viewModelFactory(
|
||||
crossinline create: (AppContainer) -> VM
|
||||
) = object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
return create(this@AppContainer) as T
|
||||
}
|
||||
}
|
||||
|
||||
val mainViewModelFactory = viewModelFactory { MainViewModel(it) }
|
||||
val homeViewModelFactory = viewModelFactory { HomeViewModel(it) }
|
||||
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
|
||||
val exportViewModelFactory = viewModelFactory { ExportViewModel(it) }
|
||||
}
|
||||
|
||||
@@ -69,10 +69,11 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initLibraries()
|
||||
val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) }
|
||||
val homeViewModel: HomeViewModel by viewModels { HomeViewModel.getFactory(this) }
|
||||
val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) }
|
||||
val exportViewModel: ExportViewModel by viewModels { ExportViewModel.getFactory(this) }
|
||||
val appContainer = (application as FairScanApp).appContainer
|
||||
val viewModel: MainViewModel by viewModels { appContainer.mainViewModelFactory }
|
||||
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
|
||||
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
|
||||
val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory }
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
exportViewModel.cleanUpOldPdfs(1000 * 3600)
|
||||
}
|
||||
|
||||
@@ -14,13 +14,10 @@
|
||||
*/
|
||||
package org.fairscan.app
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -29,24 +26,13 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.ui.NavigationState
|
||||
import org.fairscan.app.ui.Screen
|
||||
import org.fairscan.app.ui.state.DocumentUiModel
|
||||
|
||||
class MainViewModel(
|
||||
private val imageRepository: ImageRepository
|
||||
): ViewModel() {
|
||||
class MainViewModel(appContainer: AppContainer): ViewModel() {
|
||||
|
||||
companion object {
|
||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val app = context.applicationContext as FairScanApp
|
||||
return MainViewModel(app.appContainer.imageRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
private val imageRepository = appContainer.imageRepository
|
||||
|
||||
private val _navigationState = MutableStateFlow(NavigationState.initial())
|
||||
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
||||
|
||||
42
app/src/main/java/org/fairscan/app/data/LogRepository.kt
Normal file
42
app/src/main/java/org/fairscan/app/data/LogRepository.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 Pierre-Yves Nicolas
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.fairscan.app.data
|
||||
|
||||
import java.io.File
|
||||
|
||||
class LogRepository(private val file: File) {
|
||||
|
||||
fun getLogs(): String = file.readText()
|
||||
|
||||
fun log(tag: String, message: String, throwable: Throwable) {
|
||||
val line = buildString {
|
||||
append("${System.currentTimeMillis()} [$tag] $message")
|
||||
append("\n${throwable.stackTraceToString()}")
|
||||
}
|
||||
|
||||
try {
|
||||
ensureFileSizeIsReasonable()
|
||||
file.appendText(line + "\n\n")
|
||||
} catch (_: Exception) {
|
||||
// Avoid throwing another exception: do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureFileSizeIsReasonable() {
|
||||
if (file.length() > 128 * 1024) {
|
||||
file.writeText("")
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/org/fairscan/app/data/Logger.kt
Normal file
30
app/src/main/java/org/fairscan/app/data/Logger.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 Pierre-Yves Nicolas
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.fairscan.app.data
|
||||
|
||||
import android.util.Log
|
||||
|
||||
fun interface Logger {
|
||||
fun e(tag: String, message: String, throwable: Throwable)
|
||||
}
|
||||
|
||||
class FileLogger(
|
||||
private val logRepository: LogRepository
|
||||
) : Logger {
|
||||
override fun e(tag: String, message: String, throwable: Throwable) {
|
||||
Log.e(tag, message, throwable)
|
||||
logRepository.log(tag, message, throwable)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fairscan.app.data.Logger
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.tensorflow.lite.DataType
|
||||
@@ -41,7 +42,7 @@ import org.tensorflow.lite.support.image.ops.Rot90Op
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class ImageSegmentationService(private val context: Context) {
|
||||
class ImageSegmentationService(private val context: Context, private val logger: Logger) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ImageSegmentation"
|
||||
@@ -63,7 +64,7 @@ class ImageSegmentationService(private val context: Context) {
|
||||
Interpreter(litertBuffer, options)
|
||||
} catch (e: Error) {
|
||||
// That should not happen: let the app crash so that we know about it
|
||||
Log.e(TAG, "Failed to load LiteRT model: ${e.message}")
|
||||
logger.e(TAG, "Failed to load LiteRT model", e)
|
||||
throw IllegalStateException("Failed to load LiteRT model", e)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +108,7 @@ class ImageSegmentationService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error occurred in image segmentation: ${e.message}")
|
||||
logger.e(TAG, "Error occurred in image segmentation", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
*/
|
||||
package org.fairscan.app.ui.screens.camera
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -32,7 +29,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fairscan.app.domain.ImageSegmentationService
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.domain.detectDocumentQuad
|
||||
import org.fairscan.app.domain.extractDocument
|
||||
import org.fairscan.app.domain.scaledTo
|
||||
@@ -43,18 +40,9 @@ sealed interface CameraEvent {
|
||||
data class ImageCaptured(val jpegBytes: ByteArray) : CameraEvent
|
||||
}
|
||||
|
||||
class CameraViewModel(
|
||||
private val imageSegmentationService: ImageSegmentationService
|
||||
): ViewModel() {
|
||||
class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||
|
||||
companion object {
|
||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
return CameraViewModel(ImageSegmentationService(context)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
private val imageSegmentationService = appContainer.imageSegmentationService
|
||||
|
||||
private val _events = MutableSharedFlow<CameraEvent>()
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
@@ -16,12 +16,9 @@ package org.fairscan.app.ui.screens.export
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaScannerConnection
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -34,9 +31,8 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fairscan.app.FairScanApp
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.data.GeneratedPdf
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.data.PdfFileManager
|
||||
import org.fairscan.app.ui.screens.home.HomeViewModel
|
||||
import org.fairscan.app.ui.state.PdfGenerationUiState
|
||||
@@ -50,22 +46,11 @@ sealed interface ExportEvent {
|
||||
data object PdfSaved : ExportEvent
|
||||
}
|
||||
|
||||
class ExportViewModel(
|
||||
private val pdfFileManager: PdfFileManager,
|
||||
private val imageRepository: ImageRepository,
|
||||
): ViewModel() {
|
||||
class ExportViewModel(container: AppContainer): ViewModel() {
|
||||
|
||||
companion object {
|
||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val app = context.applicationContext as FairScanApp
|
||||
val pdfFileManager = app.appContainer.pdfFileManager
|
||||
val imageRepository = app.appContainer.imageRepository
|
||||
return ExportViewModel(pdfFileManager, imageRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
private val pdfFileManager = container.pdfFileManager
|
||||
private val imageRepository = container.imageRepository
|
||||
private val logger = container.logger
|
||||
|
||||
private val _events = MutableSharedFlow<ExportEvent>()
|
||||
val events = _events.asSharedFlow()
|
||||
@@ -100,7 +85,7 @@ class ExportViewModel(
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("FairScan", "PDF generation failed", e)
|
||||
logger.e("FairScan", "PDF generation failed", e)
|
||||
_pdfUiState.update {
|
||||
it.copy(
|
||||
isGenerating = false,
|
||||
@@ -172,7 +157,7 @@ class ExportViewModel(
|
||||
_events.emit(ExportEvent.PdfSaved)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("FairScan", "Failed to save PDF", e)
|
||||
logger.e("FairScan", "Failed to save PDF", e)
|
||||
_events.emit(ExportEvent.ShowToast("Error while saving PDF"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,33 +14,21 @@
|
||||
*/
|
||||
package org.fairscan.app.ui.screens.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.RecentDocument
|
||||
import org.fairscan.app.RecentDocuments
|
||||
import org.fairscan.app.data.recentDocumentsDataStore
|
||||
import org.fairscan.app.ui.state.RecentDocumentUiState
|
||||
import java.io.File
|
||||
|
||||
class HomeViewModel(private val recentDocumentsDataStore: DataStore<RecentDocuments>): ViewModel() {
|
||||
class HomeViewModel(appContainer: AppContainer): ViewModel() {
|
||||
|
||||
companion object {
|
||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
return HomeViewModel(context.recentDocumentsDataStore) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
private val recentDocumentsDataStore = appContainer.recentDocumentsDataStore
|
||||
|
||||
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
|
||||
recentDocumentsDataStore.data.map {
|
||||
|
||||
36
app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt
Normal file
36
app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 Pierre-Yves Nicolas
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.fairscan.app.data
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
class LogRepositoryTest {
|
||||
|
||||
@get:Rule
|
||||
var folder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun log_with_exception() {
|
||||
val repo = LogRepository(folder.newFile())
|
||||
assertThat(repo.getLogs()).isEmpty()
|
||||
repo.log("tag1", "message1", IllegalArgumentException("my exception"))
|
||||
assertThat(repo.getLogs()).contains("[tag1] message1")
|
||||
assertThat(repo.getLogs()).contains("my exception")
|
||||
print(repo.getLogs())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user