MainActivity shouldn't do the job of a ViewModel

This commit is contained in:
Pierre-Yves Nicolas
2025-11-23 19:05:20 +01:00
parent d4a3c78c23
commit f805201768
2 changed files with 82 additions and 39 deletions

View File

@@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.Q
@@ -42,25 +41,23 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.fairscan.app.data.GeneratedPdf
import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.Screen
import org.fairscan.app.ui.components.rememberCameraPermissionState
import org.fairscan.app.ui.screens.AboutScreen
import org.fairscan.app.ui.screens.DocumentScreen
import org.fairscan.app.ui.screens.home.HomeScreen
import org.fairscan.app.ui.screens.LibrariesScreen
import org.fairscan.app.ui.screens.camera.CameraEvent
import org.fairscan.app.ui.screens.camera.CameraScreen
import org.fairscan.app.ui.screens.camera.CameraViewModel
import org.fairscan.app.ui.screens.export.ExportEvent
import org.fairscan.app.ui.screens.export.ExportScreenWrapper
import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.export.PdfGenerationActions
import org.fairscan.app.ui.screens.home.HomeScreen
import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.theme.FairScanTheme
import org.opencv.android.OpenCVLoader
@@ -93,17 +90,34 @@ class MainActivity : ComponentActivity() {
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
val cameraPermission = rememberCameraPermissionState()
val savePdf = { savePdf(exportViewModel.getFinalPdf(), homeViewModel, exportViewModel) }
val storagePermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
savePdf()
exportViewModel.onSavePdfClicked()
} else {
val message = getString(R.string.storage_permission_denied)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
LaunchedEffect(Unit) {
exportViewModel.events.collect { event ->
when (event) {
ExportEvent.RequestSavePdf -> {
checkPermissionThen(storagePermissionLauncher) {
exportViewModel.onRequestPdfSave(context, homeViewModel)
}
}
is ExportEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
ExportEvent.PdfSaved -> {
Toast.makeText(context, "PDF saved", Toast.LENGTH_SHORT).show()
}
}
}
}
FairScanTheme {
val navigation = Navigation(
toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
@@ -155,7 +169,7 @@ class MainActivity : ComponentActivity() {
setFilename = exportViewModel::setFilename,
uiStateFlow = exportViewModel.pdfUiState,
sharePdf = { sharePdf(exportViewModel.getFinalPdf(), exportViewModel) },
savePdf = { checkPermissionThen(storagePermissionLauncher, savePdf) },
savePdf = { exportViewModel.onSavePdfClicked() },
openPdf = { openPdf(exportViewModel.pdfUiState.value.savedFileUri) }
),
onCloseScan = {
@@ -210,37 +224,6 @@ class MainActivity : ComponentActivity() {
}
}
private fun savePdf(
generatedPdf: GeneratedPdf?,
homeViewModel: HomeViewModel,
exportViewModel: ExportViewModel
) {
if (generatedPdf == null)
return
val appScope = CoroutineScope(Dispatchers.IO)
val context = this
appScope.launch {
try {
val targetFile = exportViewModel.saveFile(generatedPdf.file)
homeViewModel.addRecentDocument(targetFile.absolutePath, generatedPdf.pageCount)
suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(targetFile.absolutePath),
arrayOf(PDF_MIME_TYPE)
) { _, _ -> continuation.resume(Unit) {} }
}
} catch (e: Exception) {
Log.e("FairScan", "Failed to save PDF", e)
withContext(Dispatchers.Main) {
Toast.makeText(context,
getString(R.string.error_save), Toast.LENGTH_SHORT).show()
}
}
}
}
private fun openPdf(fileUri: Uri?) {
if (fileUri == null) return
val uri = FileProvider.getUriForFile(

View File

@@ -15,6 +15,7 @@
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
@@ -22,20 +23,33 @@ 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
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
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.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
import java.io.File
private const val PDF_MIME_TYPE = "application/pdf"
sealed interface ExportEvent {
data object RequestSavePdf : ExportEvent
data class ShowToast(val message: String) : ExportEvent
data object PdfSaved : ExportEvent
}
class ExportViewModel(
private val pdfFileManager: PdfFileManager,
private val imageRepository: ImageRepository,
@@ -53,6 +67,9 @@ class ExportViewModel(
}
}
private val _events = MutableSharedFlow<ExportEvent>()
val events = _events.asSharedFlow()
private suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
val imageIds = imageRepository.imageIds()
val jpegs = imageIds.asSequence()
@@ -127,6 +144,49 @@ class ExportViewModel(
return copiedFile
}
fun onSavePdfClicked() {
viewModelScope.launch {
_events.emit(ExportEvent.RequestSavePdf)
}
}
fun onRequestPdfSave(context: Context, homeViewModel: HomeViewModel) {
viewModelScope.launch {
performPdfSave(context, homeViewModel)
}
}
private suspend fun performPdfSave(context: Context, homeViewModel: HomeViewModel) {
try {
val pdf = getFinalPdf() ?: return
val targetFile = saveFile(pdf.file)
mediaScan(context, targetFile)
// TODO remove that call: that should be handled through the ExportEvent
homeViewModel.addRecentDocument(
targetFile.absolutePath,
pdf.pageCount
)
_events.emit(ExportEvent.PdfSaved)
} catch (e: Exception) {
Log.e("FairScan", "Failed to save PDF", e)
_events.emit(ExportEvent.ShowToast("Error while saving PDF"))
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun mediaScan(context: Context, file: File) =
suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(file.absolutePath),
arrayOf(PDF_MIME_TYPE)
) { _, _ -> continuation.resume(Unit) {} }
}
fun cleanUpOldPdfs(thresholdInMillis: Int) {
pdfFileManager.cleanUpOldFiles(thresholdInMillis)
}