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)
|
assertEquals("org.fairscan.app", appContext.packageName)
|
||||||
|
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
val segmentationService = ImageSegmentationService(context)
|
val segmentationService = ImageSegmentationService(context) { _, _, _ -> }
|
||||||
segmentationService.initialize()
|
segmentationService.initialize()
|
||||||
OpenCVLoader.initLocal()
|
OpenCVLoader.initLocal()
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,20 @@ package org.fairscan.app
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
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.ImageRepository
|
||||||
|
import org.fairscan.app.data.LogRepository
|
||||||
import org.fairscan.app.data.PdfFileManager
|
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.AndroidPdfWriter
|
||||||
import org.fairscan.app.platform.OpenCvTransformations
|
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
|
import java.io.File
|
||||||
|
|
||||||
class FairScanApp : Application() {
|
class FairScanApp : Application() {
|
||||||
@@ -43,4 +53,21 @@ class AppContainer(context: Context) {
|
|||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
AndroidPdfWriter()
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
initLibraries()
|
initLibraries()
|
||||||
val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) }
|
val appContainer = (application as FairScanApp).appContainer
|
||||||
val homeViewModel: HomeViewModel by viewModels { HomeViewModel.getFactory(this) }
|
val viewModel: MainViewModel by viewModels { appContainer.mainViewModelFactory }
|
||||||
val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) }
|
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
|
||||||
val exportViewModel: ExportViewModel by viewModels { ExportViewModel.getFactory(this) }
|
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
|
||||||
|
val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory }
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
exportViewModel.cleanUpOldPdfs(1000 * 3600)
|
exportViewModel.cleanUpOldPdfs(1000 * 3600)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app
|
package org.fairscan.app
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -29,24 +26,13 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fairscan.app.data.ImageRepository
|
|
||||||
import org.fairscan.app.ui.NavigationState
|
import org.fairscan.app.ui.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(appContainer: AppContainer): ViewModel() {
|
||||||
private val imageRepository: ImageRepository
|
|
||||||
): ViewModel() {
|
|
||||||
|
|
||||||
companion object {
|
private val imageRepository = appContainer.imageRepository
|
||||||
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 _navigationState = MutableStateFlow(NavigationState.initial())
|
private val _navigationState = MutableStateFlow(NavigationState.initial())
|
||||||
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.fairscan.app.data.Logger
|
||||||
import org.opencv.core.CvType
|
import org.opencv.core.CvType
|
||||||
import org.opencv.core.Mat
|
import org.opencv.core.Mat
|
||||||
import org.tensorflow.lite.DataType
|
import org.tensorflow.lite.DataType
|
||||||
@@ -41,7 +42,7 @@ import org.tensorflow.lite.support.image.ops.Rot90Op
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
class ImageSegmentationService(private val context: Context) {
|
class ImageSegmentationService(private val context: Context, private val logger: Logger) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ImageSegmentation"
|
private const val TAG = "ImageSegmentation"
|
||||||
@@ -63,7 +64,7 @@ class ImageSegmentationService(private val context: Context) {
|
|||||||
Interpreter(litertBuffer, options)
|
Interpreter(litertBuffer, options)
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
// That should not happen: let the app crash so that we know about it
|
// 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)
|
throw IllegalStateException("Failed to load LiteRT model", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,7 @@ class ImageSegmentationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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
|
package org.fairscan.app.ui.screens.camera
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -32,7 +29,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.detectDocumentQuad
|
||||||
import org.fairscan.app.domain.extractDocument
|
import org.fairscan.app.domain.extractDocument
|
||||||
import org.fairscan.app.domain.scaledTo
|
import org.fairscan.app.domain.scaledTo
|
||||||
@@ -43,18 +40,9 @@ sealed interface CameraEvent {
|
|||||||
data class ImageCaptured(val jpegBytes: ByteArray) : CameraEvent
|
data class ImageCaptured(val jpegBytes: ByteArray) : CameraEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
class CameraViewModel(
|
class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||||
private val imageSegmentationService: ImageSegmentationService
|
|
||||||
): ViewModel() {
|
|
||||||
|
|
||||||
companion object {
|
private val imageSegmentationService = appContainer.imageSegmentationService
|
||||||
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 _events = MutableSharedFlow<CameraEvent>()
|
private val _events = MutableSharedFlow<CameraEvent>()
|
||||||
val events = _events.asSharedFlow()
|
val events = _events.asSharedFlow()
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ package org.fairscan.app.ui.screens.export
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -34,9 +31,8 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
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.GeneratedPdf
|
||||||
import org.fairscan.app.data.ImageRepository
|
|
||||||
import org.fairscan.app.data.PdfFileManager
|
import org.fairscan.app.data.PdfFileManager
|
||||||
import org.fairscan.app.ui.screens.home.HomeViewModel
|
import org.fairscan.app.ui.screens.home.HomeViewModel
|
||||||
import org.fairscan.app.ui.state.PdfGenerationUiState
|
import org.fairscan.app.ui.state.PdfGenerationUiState
|
||||||
@@ -50,22 +46,11 @@ sealed interface ExportEvent {
|
|||||||
data object PdfSaved : ExportEvent
|
data object PdfSaved : ExportEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExportViewModel(
|
class ExportViewModel(container: AppContainer): ViewModel() {
|
||||||
private val pdfFileManager: PdfFileManager,
|
|
||||||
private val imageRepository: ImageRepository,
|
|
||||||
): ViewModel() {
|
|
||||||
|
|
||||||
companion object {
|
private val pdfFileManager = container.pdfFileManager
|
||||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
private val imageRepository = container.imageRepository
|
||||||
@Suppress("UNCHECKED_CAST")
|
private val logger = container.logger
|
||||||
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 _events = MutableSharedFlow<ExportEvent>()
|
private val _events = MutableSharedFlow<ExportEvent>()
|
||||||
val events = _events.asSharedFlow()
|
val events = _events.asSharedFlow()
|
||||||
@@ -100,7 +85,7 @@ class ExportViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("FairScan", "PDF generation failed", e)
|
logger.e("FairScan", "PDF generation failed", e)
|
||||||
_pdfUiState.update {
|
_pdfUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isGenerating = false,
|
isGenerating = false,
|
||||||
@@ -172,7 +157,7 @@ class ExportViewModel(
|
|||||||
_events.emit(ExportEvent.PdfSaved)
|
_events.emit(ExportEvent.PdfSaved)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} 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"))
|
_events.emit(ExportEvent.ShowToast("Error while saving PDF"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,33 +14,21 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.ui.screens.home
|
package org.fairscan.app.ui.screens.home
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.fairscan.app.AppContainer
|
||||||
import org.fairscan.app.RecentDocument
|
import org.fairscan.app.RecentDocument
|
||||||
import org.fairscan.app.RecentDocuments
|
|
||||||
import org.fairscan.app.data.recentDocumentsDataStore
|
|
||||||
import org.fairscan.app.ui.state.RecentDocumentUiState
|
import org.fairscan.app.ui.state.RecentDocumentUiState
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class HomeViewModel(private val recentDocumentsDataStore: DataStore<RecentDocuments>): ViewModel() {
|
class HomeViewModel(appContainer: AppContainer): ViewModel() {
|
||||||
|
|
||||||
companion object {
|
private val recentDocumentsDataStore = appContainer.recentDocumentsDataStore
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
|
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
|
||||||
recentDocumentsDataStore.data.map {
|
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