MainActivity shouldn't do the job of a ViewModel
This commit is contained in:
@@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.media.MediaScannerConnection
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Build.VERSION_CODES.Q
|
import android.os.Build.VERSION_CODES.Q
|
||||||
@@ -42,25 +41,23 @@ import androidx.core.net.toFile
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.fairscan.app.data.GeneratedPdf
|
import org.fairscan.app.data.GeneratedPdf
|
||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
||||||
import org.fairscan.app.ui.screens.AboutScreen
|
import org.fairscan.app.ui.screens.AboutScreen
|
||||||
import org.fairscan.app.ui.screens.DocumentScreen
|
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.LibrariesScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraEvent
|
import org.fairscan.app.ui.screens.camera.CameraEvent
|
||||||
import org.fairscan.app.ui.screens.camera.CameraScreen
|
import org.fairscan.app.ui.screens.camera.CameraScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
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.ExportScreenWrapper
|
||||||
import org.fairscan.app.ui.screens.export.ExportViewModel
|
import org.fairscan.app.ui.screens.export.ExportViewModel
|
||||||
import org.fairscan.app.ui.screens.export.PdfGenerationActions
|
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.screens.home.HomeViewModel
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
import org.opencv.android.OpenCVLoader
|
import org.opencv.android.OpenCVLoader
|
||||||
@@ -93,17 +90,34 @@ class MainActivity : ComponentActivity() {
|
|||||||
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
val cameraPermission = rememberCameraPermissionState()
|
val cameraPermission = rememberCameraPermissionState()
|
||||||
val savePdf = { savePdf(exportViewModel.getFinalPdf(), homeViewModel, exportViewModel) }
|
|
||||||
val storagePermissionLauncher = rememberLauncherForActivityResult(
|
val storagePermissionLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted ->
|
) { isGranted ->
|
||||||
if (isGranted) {
|
if (isGranted) {
|
||||||
savePdf()
|
exportViewModel.onSavePdfClicked()
|
||||||
} else {
|
} else {
|
||||||
val message = getString(R.string.storage_permission_denied)
|
val message = getString(R.string.storage_permission_denied)
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
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 {
|
FairScanTheme {
|
||||||
val navigation = Navigation(
|
val navigation = Navigation(
|
||||||
toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
|
toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
|
||||||
@@ -155,7 +169,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
setFilename = exportViewModel::setFilename,
|
setFilename = exportViewModel::setFilename,
|
||||||
uiStateFlow = exportViewModel.pdfUiState,
|
uiStateFlow = exportViewModel.pdfUiState,
|
||||||
sharePdf = { sharePdf(exportViewModel.getFinalPdf(), exportViewModel) },
|
sharePdf = { sharePdf(exportViewModel.getFinalPdf(), exportViewModel) },
|
||||||
savePdf = { checkPermissionThen(storagePermissionLauncher, savePdf) },
|
savePdf = { exportViewModel.onSavePdfClicked() },
|
||||||
openPdf = { openPdf(exportViewModel.pdfUiState.value.savedFileUri) }
|
openPdf = { openPdf(exportViewModel.pdfUiState.value.savedFileUri) }
|
||||||
),
|
),
|
||||||
onCloseScan = {
|
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?) {
|
private fun openPdf(fileUri: Uri?) {
|
||||||
if (fileUri == null) return
|
if (fileUri == null) return
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileProvider.getUriForFile(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package org.fairscan.app.ui.screens.export
|
package org.fairscan.app.ui.screens.export
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -22,20 +23,33 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.fairscan.app.FairScanApp
|
import org.fairscan.app.FairScanApp
|
||||||
import org.fairscan.app.data.GeneratedPdf
|
import org.fairscan.app.data.GeneratedPdf
|
||||||
import org.fairscan.app.data.ImageRepository
|
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.state.PdfGenerationUiState
|
import org.fairscan.app.ui.state.PdfGenerationUiState
|
||||||
import java.io.File
|
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(
|
class ExportViewModel(
|
||||||
private val pdfFileManager: PdfFileManager,
|
private val pdfFileManager: PdfFileManager,
|
||||||
private val imageRepository: ImageRepository,
|
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) {
|
private suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
|
||||||
val imageIds = imageRepository.imageIds()
|
val imageIds = imageRepository.imageIds()
|
||||||
val jpegs = imageIds.asSequence()
|
val jpegs = imageIds.asSequence()
|
||||||
@@ -127,6 +144,49 @@ class ExportViewModel(
|
|||||||
return copiedFile
|
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) {
|
fun cleanUpOldPdfs(thresholdInMillis: Int) {
|
||||||
pdfFileManager.cleanUpOldFiles(thresholdInMillis)
|
pdfFileManager.cleanUpOldFiles(thresholdInMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user