From b5bf93b7ec6c55e209ba912ec11303bf529cef35 Mon Sep 17 00:00:00 2001 From: pynicolas <6371790+pynicolas@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:03:38 +0100 Subject: [PATCH] Allow other apps to call FairScan to scan a document to PDF (#81) * Intent for external calls * External calls: start on camera screen * Isolate captured images for each scan session * Remove access to settings when called externally --- README.md | 23 ++++ app/src/main/AndroidManifest.xml | 7 +- .../main/java/org/fairscan/app/FairScanApp.kt | 34 ++++-- .../main/java/org/fairscan/app/LaunchMode.kt | 20 +++ .../java/org/fairscan/app/MainActivity.kt | 115 ++++++++++++++---- .../java/org/fairscan/app/MainViewModel.kt | 7 +- .../org/fairscan/app/ScanSessionContainer.kt | 34 ++++++ .../org/fairscan/app/data/ImageRepository.kt | 6 +- .../java/org/fairscan/app/ui/Navigation.kt | 18 ++- .../fairscan/app/ui/components/Scaffold.kt | 18 +-- .../fairscan/app/ui/screens/DocumentScreen.kt | 8 +- .../app/ui/screens/export/ExportScreen.kt | 1 - .../app/ui/screens/export/ExportUiState.kt | 1 - .../app/ui/screens/export/ExportViewModel.kt | 8 +- .../org/fairscan/app/ui/NavigationTest.kt | 14 ++- 15 files changed, 254 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/org/fairscan/app/LaunchMode.kt create mode 100644 app/src/main/java/org/fairscan/app/ScanSessionContainer.kt diff --git a/README.md b/README.md index c8db9f1..f1f3120 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,29 @@ FairScan works on any device that: --- +## Experimental: Scan to PDF via intent + +FairScan can be invoked by other Android applications to perform a document scan and return a generated PDF. + +This feature is **experimental** and intended for developers who want to rely on FairScan as a +simple, privacy-respecting scanning tool. +The intent contract and behavior may change between versions, and backward compatibility +is not guaranteed at this stage. + +Intent action: `org.fairscan.app.action.SCAN_TO_PDF` + +This is an **implicit intent** that launches FairScan in a dedicated external mode. + +When started via this intent: + +- FairScan opens directly in scan mode +- the user scans one or more pages +- FairScan generates a single PDF +- the resulting PDF is returned to the calling application as a URI with a limited lifetime +- the calling application should immediately copy the content of the URI as FairScan deletes it later + +--- + ## Technical details FairScan uses: diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d58c80..18a63f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,9 +27,12 @@ android:theme="@style/Theme.FairScan"> - + + + + - \ No newline at end of file + diff --git a/app/src/main/java/org/fairscan/app/FairScanApp.kt b/app/src/main/java/org/fairscan/app/FairScanApp.kt index e065002..7348b7f 100644 --- a/app/src/main/java/org/fairscan/app/FairScanApp.kt +++ b/app/src/main/java/org/fairscan/app/FairScanApp.kt @@ -21,16 +21,13 @@ 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.FileManager +import org.fairscan.app.data.LogRepository 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.about.AboutViewModel 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 org.fairscan.app.ui.screens.settings.SettingsRepository import org.fairscan.app.ui.screens.settings.SettingsViewModel @@ -42,15 +39,14 @@ class FairScanApp : Application() { override fun onCreate() { super.onCreate() appContainer = AppContainer(this) + appContainer.cleanOrphanSessions() } } const val THUMBNAIL_SIZE_DP = 120 class AppContainer(context: Context) { - private val density = context.resources.displayMetrics.density - private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt() - val imageRepository = ImageRepository(context.filesDir, OpenCvTransformations(), thumbnailSizePx) + private val cacheDir = context.cacheDir val preparationDir = File(context.cacheDir, "pdfs") val fileManager = FileManager( preparationDir, @@ -72,10 +68,30 @@ class AppContainer(context: Context) { } } - val mainViewModelFactory = viewModelFactory { MainViewModel(it) } val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) } val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) } - val exportViewModelFactory = viewModelFactory { ExportViewModel(it) } val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) } val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) } + + fun cleanOrphanSessions() { + val sessionsRoot = sessionsRoot() + if (!sessionsRoot.exists()) return + + val now = System.currentTimeMillis() + + sessionsRoot.listFiles() + ?.filter { it.isDirectory } + ?.forEach { dir -> + if (isOldSession(dir, now)) { + dir.deleteRecursively() + } + } + } + + fun sessionsRoot(): File = File(cacheDir, "sessions") + + private fun isOldSession(dir: File, now: Long): Boolean { + val lastModified = dir.lastModified() + return now - lastModified > 24 * 60 * 60 * 1000 // 24h + } } diff --git a/app/src/main/java/org/fairscan/app/LaunchMode.kt b/app/src/main/java/org/fairscan/app/LaunchMode.kt new file mode 100644 index 0000000..0000334 --- /dev/null +++ b/app/src/main/java/org/fairscan/app/LaunchMode.kt @@ -0,0 +1,20 @@ +/* + * 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 . + */ +package org.fairscan.app + +enum class LaunchMode { + NORMAL, + EXTERNAL_SCAN_TO_PDF +} diff --git a/app/src/main/java/org/fairscan/app/MainActivity.kt b/app/src/main/java/org/fairscan/app/MainActivity.kt index 1c90299..0ae44af 100644 --- a/app/src/main/java/org/fairscan/app/MainActivity.kt +++ b/app/src/main/java/org/fairscan/app/MainActivity.kt @@ -72,17 +72,36 @@ import org.fairscan.app.ui.screens.settings.SettingsScreen import org.fairscan.app.ui.screens.settings.SettingsViewModel import org.fairscan.app.ui.theme.FairScanTheme import org.opencv.android.OpenCVLoader +import java.io.File +import java.util.UUID class MainActivity : ComponentActivity() { + private lateinit var sessionDir: File + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initLibraries() val appContainer = (application as FairScanApp).appContainer - val viewModel: MainViewModel by viewModels { appContainer.mainViewModelFactory } + val launchMode = resolveLaunchMode(intent) + sessionDir = when (launchMode) { + LaunchMode.NORMAL -> filesDir + LaunchMode.EXTERNAL_SCAN_TO_PDF -> + File(appContainer.sessionsRoot(), UUID.randomUUID().toString()).apply { mkdirs() } + } + val sessionContainer = ScanSessionContainer(this, sessionDir) + val viewModel: MainViewModel by viewModels { + appContainer.viewModelFactory { + MainViewModel(sessionContainer.imageRepository, launchMode) + } + } + val exportViewModel: ExportViewModel by viewModels { + appContainer.viewModelFactory { + ExportViewModel(appContainer, sessionContainer.imageRepository) + } + } val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory } val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory } - val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory } val aboutViewModel: AboutViewModel by viewModels { appContainer.aboutViewModelFactory } val settingsViewModel: SettingsViewModel by viewModels { appContainer.settingsViewModelFactory } @@ -102,7 +121,7 @@ class MainActivity : ComponentActivity() { CollectAboutEvents(context, aboutViewModel) FairScanTheme { - val navigation = navigation(viewModel) + val navigation = navigation(viewModel, launchMode) when (val screen = currentScreen) { is Screen.Main.Home -> { val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle() @@ -131,6 +150,19 @@ class MainActivity : ComponentActivity() { document = document, initialPage = screen.initialPage, navigation = navigation, + onExportClick = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) { + { + lifecycleScope.launch { + val result = exportViewModel.generatePdfForExternalCall() + sendActivityResult(result) + viewModel.startNewDocument() + finish() + } + Unit + } + } else { + navigation.toExportScreen + }, onDeleteImage = { id -> viewModel.deletePage(id) }, onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) }, onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) }, @@ -145,12 +177,12 @@ class MainActivity : ComponentActivity() { setFilename = exportViewModel::setFilename, share = { share(exportViewModel.applyRenaming(), exportViewModel) }, save = { exportViewModel.onSaveClicked() }, - open = { item -> openUri(item.uri, item.format.mimeType) } + open = { item -> openUri(item.uri, item.format.mimeType) }, ), onCloseScan = { viewModel.startNewDocument() viewModel.navigateTo(Screen.Main.Home) - }, + } ) } is Screen.Overlay.About -> { @@ -170,6 +202,20 @@ class MainActivity : ComponentActivity() { } } + override fun onDestroy() { + super.onDestroy() + if (resolveLaunchMode(intent) == LaunchMode.EXTERNAL_SCAN_TO_PDF) { + sessionDir.deleteRecursively() + } + } + + private fun resolveLaunchMode(intent: Intent?): LaunchMode { + return when (intent?.action) { + "org.fairscan.app.action.SCAN_TO_PDF" -> LaunchMode.EXTERNAL_SCAN_TO_PDF + else -> LaunchMode.NORMAL + } + } + @Composable private fun SettingsScreenWrapper(settingsViewModel: SettingsViewModel, nav: Navigation) { val launcher = rememberLauncherForActivityResult( @@ -265,10 +311,7 @@ class MainActivity : ComponentActivity() { viewModel.setAsShared() - val authority = "${applicationContext.packageName}.fileprovider" - val uris = result.files.map { file -> - FileProvider.getUriForFile(this, authority, file) - } + val uris = result.files.map(::uriForFile) val intent = Intent().apply { action = if (uris.size == 1) Intent.ACTION_SEND else Intent.ACTION_SEND_MULTIPLE type = result.format.mimeType @@ -293,6 +336,24 @@ class MainActivity : ComponentActivity() { startActivity(chooser) } + private fun sendActivityResult(result: ExportResult?) { + val pdf = result as? ExportResult.Pdf ?: return + + val uri = uriForFile(pdf.file) + val resultIntent = Intent().apply { + data = uri + clipData = ClipData.newRawUri(null, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + setResult(RESULT_OK, resultIntent) + } + + private fun uriForFile(file: File): Uri { + val authority = "${applicationContext.packageName}.fileprovider" + return FileProvider.getUriForFile(this, authority, file) + } + private fun checkPermissionThen( requestPermissionLauncher: ManagedActivityResultLauncher, action: () -> Unit @@ -312,8 +373,7 @@ class MainActivity : ComponentActivity() { if (fileUri.scheme == ContentResolver.SCHEME_CONTENT) { fileUri } else { - val authority = "${applicationContext.packageName}.fileprovider" - FileProvider.getUriForFile(this, authority, fileUri.toFile()) + uriForFile(fileUri.toFile()) } val openIntent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uriToOpen, mimeType) @@ -335,15 +395,28 @@ class MainActivity : ComponentActivity() { Log.d("OpenCV", "Initialization successful") } } + + private fun navigation(viewModel: MainViewModel, launchMode: LaunchMode): Navigation = Navigation( + toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) }, + toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) }, + toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) }, + toExportScreen = { viewModel.navigateTo(Screen.Main.Export) }, + toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) }, + toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) }, + toSettingsScreen = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) null else { + { + viewModel.navigateTo(Screen.Overlay.Settings) + } + }, + back = { + val origin = viewModel.currentScreen.value + viewModel.navigateBack() + val destination = viewModel.currentScreen.value + if (destination == origin && launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) { + setResult(RESULT_CANCELED) + finish() + } + } + ) } -private fun navigation(viewModel: MainViewModel): Navigation = Navigation( - toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) }, - toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) }, - toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) }, - toExportScreen = { viewModel.navigateTo(Screen.Main.Export) }, - toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) }, - toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) }, - toSettingsScreen = { viewModel.navigateTo(Screen.Overlay.Settings) }, - back = { viewModel.navigateBack() } -) diff --git a/app/src/main/java/org/fairscan/app/MainViewModel.kt b/app/src/main/java/org/fairscan/app/MainViewModel.kt index e432807..750fac7 100644 --- a/app/src/main/java/org/fairscan/app/MainViewModel.kt +++ b/app/src/main/java/org/fairscan/app/MainViewModel.kt @@ -26,15 +26,14 @@ 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(appContainer: AppContainer): ViewModel() { +class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() { - private val imageRepository = appContainer.imageRepository - - private val _navigationState = MutableStateFlow(NavigationState.initial()) + private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode)) val currentScreen: StateFlow = _navigationState.map { it.current } .stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current) diff --git a/app/src/main/java/org/fairscan/app/ScanSessionContainer.kt b/app/src/main/java/org/fairscan/app/ScanSessionContainer.kt new file mode 100644 index 0000000..8cc4ac2 --- /dev/null +++ b/app/src/main/java/org/fairscan/app/ScanSessionContainer.kt @@ -0,0 +1,34 @@ +/* + * 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 . + */ +package org.fairscan.app + +import android.content.Context +import org.fairscan.app.data.ImageRepository +import org.fairscan.app.platform.OpenCvTransformations +import java.io.File + +class ScanSessionContainer( + context: Context, + scanRootDir: File +) { + private val density = context.resources.displayMetrics.density + private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt() + + val imageRepository = ImageRepository( + scanRootDir, + OpenCvTransformations(), + thumbnailSizePx + ) +} diff --git a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt index 5eb1469..101c0ff 100644 --- a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt +++ b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt @@ -23,16 +23,16 @@ const val SCAN_DIR_NAME = "scanned_pages" const val THUMBNAIL_DIR_NAME = "thumbnails" class ImageRepository( - appFilesDir: File, + scanRootDir: File, val transformations: ImageTransformations, private val thumbnailSizePx: Int, ) { - private val scanDir: File = File(appFilesDir, SCAN_DIR_NAME).apply { + private val scanDir: File = File(scanRootDir, SCAN_DIR_NAME).apply { if (!exists()) mkdirs() } - private val thumbnailDir: File = File(appFilesDir, THUMBNAIL_DIR_NAME).apply { + private val thumbnailDir: File = File(scanRootDir, THUMBNAIL_DIR_NAME).apply { if (!exists()) mkdirs() } diff --git a/app/src/main/java/org/fairscan/app/ui/Navigation.kt b/app/src/main/java/org/fairscan/app/ui/Navigation.kt index 9ceb6a4..c4feb06 100644 --- a/app/src/main/java/org/fairscan/app/ui/Navigation.kt +++ b/app/src/main/java/org/fairscan/app/ui/Navigation.kt @@ -14,6 +14,8 @@ */ package org.fairscan.app.ui +import org.fairscan.app.LaunchMode + sealed class Screen { sealed class Main : Screen() { object Home : Main() @@ -35,15 +37,24 @@ data class Navigation( val toExportScreen: () -> Unit, val toAboutScreen: () -> Unit, val toLibrariesScreen: () -> Unit, - val toSettingsScreen: () -> Unit, + val toSettingsScreen: (() -> Unit)?, val back: () -> Unit, ) +fun startScreenFor(mode: LaunchMode): Screen.Main = + when (mode) { + LaunchMode.NORMAL -> Screen.Main.Home + LaunchMode.EXTERNAL_SCAN_TO_PDF -> Screen.Main.Camera + } + @ConsistentCopyVisibility -data class NavigationState private constructor(val stack: List) { +data class NavigationState private constructor(val stack: List, val root: Screen.Main) { companion object { - fun initial() = NavigationState(listOf(Screen.Main.Home)) + fun initial(mode: LaunchMode): NavigationState { + val root = startScreenFor(mode) + return NavigationState(listOf(root), root) + } } val current: Screen get() = stack.last() @@ -58,6 +69,7 @@ data class NavigationState private constructor(val stack: List) { fun navigateBack(): NavigationState { return when (current) { + root -> this // Back handled by system is Screen.Main.Home -> this // Back handled by system is Screen.Main.Camera -> copy(stack = listOf(Screen.Main.Home)) is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera)) diff --git a/app/src/main/java/org/fairscan/app/ui/components/Scaffold.kt b/app/src/main/java/org/fairscan/app/ui/components/Scaffold.kt index b593aee..63f9dcc 100644 --- a/app/src/main/java/org/fairscan/app/ui/components/Scaffold.kt +++ b/app/src/main/java/org/fairscan/app/ui/components/Scaffold.kt @@ -181,14 +181,16 @@ fun AppOverflowMenu( .background(MaterialTheme.colorScheme.surface) ) { - DropdownMenuItem( - leadingIcon = { Icon(Icons.Default.Settings, contentDescription = null) }, - text = { Text(stringResource(R.string.settings)) }, - onClick = { - expanded = false - navigation.toSettingsScreen() - } - ) + navigation.toSettingsScreen?.let { toSettings -> + DropdownMenuItem( + leadingIcon = { Icon(Icons.Default.Settings, contentDescription = null) }, + text = { Text(stringResource(R.string.settings)) }, + onClick = { + expanded = false + toSettings() + } + ) + } DropdownMenuItem( leadingIcon = { Icon(Icons.Default.Info, contentDescription = null) }, diff --git a/app/src/main/java/org/fairscan/app/ui/screens/DocumentScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/DocumentScreen.kt index 7bc1285..7fd2cad 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/DocumentScreen.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/DocumentScreen.kt @@ -73,6 +73,7 @@ fun DocumentScreen( document: DocumentUiModel, initialPage: Int, navigation: Navigation, + onExportClick: () -> Unit, onDeleteImage: (String) -> Unit, onRotateImage: (String, Boolean) -> Unit, onPageReorder: (String, Int) -> Unit, @@ -105,7 +106,7 @@ fun DocumentScreen( ), onBack = navigation.back, bottomBar = { - BottomBar(navigation) + BottomBar(onExportClick) }, pageListButton = { SecondaryActionButton( @@ -217,7 +218,7 @@ fun RotationButtons( @Composable private fun BottomBar( - navigation: Navigation, + onExportClick: () -> Unit, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -225,7 +226,7 @@ private fun BottomBar( horizontalArrangement = Arrangement.End ) { MainActionButton( - onClick = navigation.toExportScreen, + onClick = onExportClick, icon = Icons.Default.Description, text = stringResource(R.string.export), ) @@ -243,6 +244,7 @@ fun DocumentScreenPreview() { ), initialPage = 1, navigation = dummyNavigation(), + onExportClick = {}, onDeleteImage = { _ -> }, onRotateImage = { _,_ -> }, onPageReorder = { _,_ -> }, diff --git a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportScreen.kt index 1ce858a..c1eb900 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportScreen.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportScreen.kt @@ -75,7 +75,6 @@ import org.fairscan.app.ui.components.NewDocumentDialog import org.fairscan.app.ui.components.isLandscape import org.fairscan.app.ui.components.pageCountText import org.fairscan.app.ui.dummyNavigation -import org.fairscan.app.ui.screens.settings.ExportFormat import org.fairscan.app.ui.screens.settings.ExportFormat.PDF import org.fairscan.app.ui.theme.FairScanTheme import java.io.File diff --git a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportUiState.kt b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportUiState.kt index bb6e422..4958848 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportUiState.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportUiState.kt @@ -21,7 +21,6 @@ data class ExportUiState( val format: ExportFormat = ExportFormat.PDF, val isGenerating: Boolean = false, val result: ExportResult? = null, - val desiredFilename: String = "", val savedBundle: SavedBundle? = null, val hasShared: Boolean = false, val errorMessage: String? = null, diff --git a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt index 5ca040b..e8d7435 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.withContext import org.fairscan.app.AppContainer import org.fairscan.app.RecentDocument import org.fairscan.app.data.FileManager +import org.fairscan.app.data.ImageRepository import org.fairscan.app.ui.screens.settings.ExportFormat import java.io.File import java.io.FileInputStream @@ -46,11 +47,10 @@ sealed interface ExportEvent { data object SaveError : ExportEvent } -class ExportViewModel(container: AppContainer): ViewModel() { +class ExportViewModel(container: AppContainer, val imageRepository: ImageRepository): ViewModel() { private val preparationDir = container.preparationDir private val fileManager = container.fileManager - private val imageRepository = container.imageRepository private val settingsRepository = container.settingsRepository private val recentDocumentsDataStore = container.recentDocumentsDataStore private val logger = container.logger @@ -66,6 +66,10 @@ class ExportViewModel(container: AppContainer): ViewModel() { return@withContext ExportResult.Pdf(pdf.file, pdf.sizeInBytes, pdf.pageCount) } + suspend fun generatePdfForExternalCall(): ExportResult.Pdf { + return generatePdf() + } + private val _uiState = MutableStateFlow(ExportUiState()) val uiState: StateFlow = _uiState.asStateFlow() diff --git a/app/src/test/java/org/fairscan/app/ui/NavigationTest.kt b/app/src/test/java/org/fairscan/app/ui/NavigationTest.kt index 54f34d5..12d9953 100644 --- a/app/src/test/java/org/fairscan/app/ui/NavigationTest.kt +++ b/app/src/test/java/org/fairscan/app/ui/NavigationTest.kt @@ -15,6 +15,7 @@ package org.fairscan.app.ui import org.assertj.core.api.Assertions.assertThat +import org.fairscan.app.LaunchMode import org.fairscan.app.ui.Screen.Main.Camera import org.fairscan.app.ui.Screen.Main.Document import org.fairscan.app.ui.Screen.Main.Export @@ -27,14 +28,14 @@ class NavigationTest { @Test fun empty_ScreenStack() { - val empty = NavigationState.initial() + val empty = NavigationState.initial(LaunchMode.NORMAL) assertThat(empty.current).isEqualTo(Home) assertThat(empty.navigateBack()).isEqualTo(empty) } @Test fun navigate_between_fixed_screens() { - val atHome = NavigationState.initial() + val atHome = NavigationState.initial(LaunchMode.NORMAL) val atCamera = atHome.navigateTo(Camera) val atDocument = atHome.navigateTo(Document()) val atExport = atHome.navigateTo(Export) @@ -56,7 +57,7 @@ class NavigationTest { @Test fun navigate_to_secondary_screens() { - val atHome = NavigationState.initial() + val atHome = NavigationState.initial(LaunchMode.NORMAL) val atCamera = atHome.navigateTo(Camera) val atAboutAfterHome = atHome.navigateTo(About) @@ -71,4 +72,11 @@ class NavigationTest { assertThat(atLibrariesAfterCameraAbout.current).isEqualTo(Libraries) assertThat(atLibrariesAfterCameraAbout.navigateBack()).isEqualTo(atAboutAfterCamera) } + + @Test + fun external_call() { + val initial = NavigationState.initial(LaunchMode.EXTERNAL_SCAN_TO_PDF) + assertThat(initial.current).isEqualTo(Camera) + assertThat(initial.navigateBack().current).isEqualTo(Camera) + } } \ No newline at end of file