Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8677821
Abstract discovery backend
Chrisimx May 17, 2026
af91ce9
Refactor HTTP timeout configuration and improve scanner discovery (ch…
Chrisimx May 17, 2026
a2d587c
Show fast scanners quickly and slow scanners later if they are reachable
Chrisimx May 17, 2026
b27b6e3
More caching of the scanner reachabilities
Chrisimx May 17, 2026
925e449
Implement incremental upgrading of scanner reachability state and sav…
Chrisimx May 17, 2026
24ecb27
Partially use scan handles in scanning screen
Chrisimx May 17, 2026
767a97c
Remove protocol dependency from scanning screen (now only impl in Sca…
Chrisimx May 22, 2026
cbebc00
Introduce database schema version 5 and remove protocol dependency fr…
Chrisimx May 27, 2026
18aa851
Update to new eSCLKt version
Chrisimx May 27, 2026
0cbdf16
Partially refactor scanning functionality to use unified abstractions
Chrisimx Jul 11, 2026
322a922
Dynamic scan settings
Chrisimx Jul 11, 2026
15b64e5
Fix (most pressing) serialization issues
Chrisimx Jul 13, 2026
22e1251
Use sealed value type hierarchy instead of Any to ease serialization
Chrisimx Jul 15, 2026
d54c83a
Introduce WSD scanning protocol
Chrisimx Jul 18, 2026
b7e51bd
Add localization of choice values
Chrisimx Jul 18, 2026
fd9f9cb
Add protocol selection for custom scanner creation
Chrisimx Jul 18, 2026
b4bc864
Finish WSD protocol implementation
Chrisimx Jul 19, 2026
3fa0124
Add German translation scan intent strings
Chrisimx Jul 19, 2026
ab8fbf8
Add TODO note for fixing file name handling in ZIP export
Chrisimx Jul 19, 2026
e0640a1
Re-add action button to snackbar in ScanningScreen
Chrisimx Jul 19, 2026
dbaf0ea
Fix pdf export functionality
Chrisimx Jul 19, 2026
3469339
Make prerelease
Chrisimx Jul 19, 2026
6a0a264
Introduce initial scan settings provider and use it
Chrisimx Jul 19, 2026
b063e72
Implement scan region parameter display
Chrisimx Jul 20, 2026
a2260cf
Faster WSD discovery by running flow emission and better lifecycle ma…
Chrisimx Jul 25, 2026
e44f9ba
Tolerance for scan region selection
Chrisimx Jul 25, 2026
1b60ac2
Add migration for Room database to handle type changes
Chrisimx Jul 29, 2026
b67ad39
Upgrade AnyScanKt
Chrisimx Jul 30, 2026
9bb8dca
Fix indexing error in Room migration (4 to 5)
Chrisimx Jul 30, 2026
8656de0
Extract scan initiation to use case, fix B&W format issue and move sc…
Chrisimx Jul 31, 2026
59243b3
Apply format
Chrisimx Aug 1, 2026
644b34a
Fix play flavor of ScanBridge
Chrisimx Aug 1, 2026
7791816
Fix input source switch back when you press the ADF button while dupl…
Chrisimx Aug 1, 2026
fa20b36
Update AnyScanKt
Chrisimx Aug 1, 2026
d6aa1c6
Fix crash when rotating too many times
Chrisimx Aug 1, 2026
d4d0d7e
Fix NavBackStackEntry.toTypedRoute() which prevented route saving
Chrisimx Aug 1, 2026
823113b
Fix crash that happend for specific invalid URLS
Chrisimx Aug 2, 2026
1f603ad
Apply format
Chrisimx Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_e
ktlint_ignore_back_ticked_identifier = false
ktlint_standard_no-unused-imports = enabled
max_line_length = 140
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_no-wildcard-imports = disabled

[**/generated/**/*.kt]
ktlint = disabled

[**/build/kspCaches/**/*.kt]
ktlint = disabled
2 changes: 1 addition & 1 deletion androidApp/adb+.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
# Script adb+
# Usage
# You can run any command adb provide on all your current devices
# You can run any command adb provides on all your current devices
# ./adb+ <command> is the equivalent of ./adb -s <serial number> <command>
#
# Examples
Expand Down
11 changes: 6 additions & 5 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ android {
applicationId = "io.github.chrisimx.scanbridge"
minSdk = 28
targetSdk = 36
versionCode = 2_001_004 // format is MAJ_MIN_PAT with always 3 digits
versionName = "2.1.4"
versionCode = 2_001_005 // we just count up for each build
versionName = "2.2.0-alpha1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["escl_server_url"] =
Expand Down Expand Up @@ -76,8 +76,8 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
compose = true
Expand All @@ -87,7 +87,7 @@ android {

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
jvmTarget = JvmTarget.JVM_21
optIn.add("kotlin.uuid.ExperimentalUuidApi")
freeCompilerArgs.add("-Xnon-local-break-continue")
}
Expand All @@ -113,6 +113,7 @@ dependencies {
implementation(project(":core"))
implementation(project(":composeUI"))
implementation(libs.androidx.concurrent.futures)
implementation(libs.coil.fetcher.ktor)

ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.runtime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package io.github.chrisimx.scanbridge

import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ val STARTUP_TABS = listOf(
Icons.Filled.Home,
Icons.Outlined.Home,
true,
{ innerPadding, navController, showCustomDialog, setShowCustomDialog, statefulScannerMap, statefulScannerMapSecure ->
{ innerPadding, navController, showCustomDialog, setShowCustomDialog ->
ScannerBrowser(
innerPadding,
navController,
showCustomDialog,
setShowCustomDialog,
statefulScannerMap,
statefulScannerMapSecure
setShowCustomDialog
)
}
),
Expand All @@ -32,7 +30,7 @@ val STARTUP_TABS = listOf(
Icons.Filled.Settings,
Icons.Outlined.Settings,
false,
{ innerPadding, _, _, _, _, _ ->
{ innerPadding, _, _, _ ->
AppSettingsScreen(innerPadding)
}
)
Expand Down
3 changes: 3 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- For finding WSD scanners via WS-Discovery, we need multicast capability -->
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />


<application
android:name=".ScanBridgeApplication"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.chrisimx.scanbridge

import io.github.chrisimx.anyscan.CommonScanSettingsEditor
import io.github.chrisimx.anyscan.CommonScannerCapabilities
import io.github.chrisimx.anyscan.FileFormat
import io.github.chrisimx.enumorrawcodegen.AnyScanEnumOrRaw
import io.github.chrisimx.scanbridge.ports.InitialScanSettingsProvider

class DefaultInitialScanSettingsProvider : InitialScanSettingsProvider {
override fun applyDefaults(editor: CommonScanSettingsEditor, capabilities: CommonScannerCapabilities) {
editor.setInputSource(capabilities.inputSources.first().inputSourceType)
editor.setFormat(AnyScanEnumOrRaw.Known(FileFormat.JPEG))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand All @@ -55,7 +54,6 @@ fun ScanBridgeApp() {
ScanBridgeTheme {
val navController = rememberNavController()
var startDestination: Any? by remember { mutableStateOf(null) }
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()

val currentBackStackEntry by navController.currentBackStackEntryAsState()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package io.github.chrisimx.scanbridge

import AndroidHttpClientFactory
import AndroidMdnsDiscoverService
import AndroidMulticastLockHandler
import AndroidScanBridgeDbBuilderFactory
import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import io.github.chrisimx.scanbridge.data.ui.CustomScannerViewModel
import coil3.ImageLoader
import coil3.network.ktor3.KtorNetworkFetcherFactory
import io.github.chrisimx.scanbridge.adapters.KoinBasedScanningProtocolManager
import io.github.chrisimx.scanbridge.adapters.RoomBackedCustomScannerRepository
import io.github.chrisimx.scanbridge.data.ui.ScanSettingsComposableStateHolder
import io.github.chrisimx.scanbridge.data.ui.ScanningScreenViewModel
import io.github.chrisimx.scanbridge.datastore.appSettingsStore
Expand All @@ -18,18 +23,29 @@ import io.github.chrisimx.scanbridge.infrastructure.KmLogScanBridgeLoggerFactory
import io.github.chrisimx.scanbridge.migrations.MigrationExecutor
import io.github.chrisimx.scanbridge.migrations.RoomBackedMigrationExecutor
import io.github.chrisimx.scanbridge.migrations.migrationsModule
import io.github.chrisimx.scanbridge.model.HttpClientConfig
import io.github.chrisimx.scanbridge.ports.CustomScannerRepository
import io.github.chrisimx.scanbridge.ports.HttpClientFactory
import io.github.chrisimx.scanbridge.ports.InitialScanSettingsProvider
import io.github.chrisimx.scanbridge.ports.LocaleProvider
import io.github.chrisimx.scanbridge.ports.MdnsDiscoverService
import io.github.chrisimx.scanbridge.ports.ScanBridgeLoggerFactory
import io.github.chrisimx.scanbridge.ports.ScanningProtocolManager
import io.github.chrisimx.scanbridge.ports.multicast.MulticastLockHandler
import io.github.chrisimx.scanbridge.proto.ScanBridgeSettings
import io.github.chrisimx.scanbridge.proto.ShownMessages
import io.github.chrisimx.scanbridge.repositories.DatastoreLastRouteRepository
import io.github.chrisimx.scanbridge.repositories.DatastoreShownMessagesRepository
import io.github.chrisimx.scanbridge.repositories.RoomLastRouteRepository
import io.github.chrisimx.scanbridge.scan.AndroidStartScanUseCase
import io.github.chrisimx.scanbridge.scannerdiscovery.DiscoveryUsecase
import io.github.chrisimx.scanbridge.scannerdiscovery.ScannerDiscoveryScreenViewModel
import io.github.chrisimx.scanbridge.services.AndroidLocaleProvider
import io.github.chrisimx.scanbridge.services.DebugLogService
import io.github.chrisimx.scanbridge.services.FileDebugLogService
import io.github.chrisimx.scanbridge.services.ScanJobRepository
import io.github.chrisimx.scanbridge.usecases.StartScanUseCase
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -49,6 +65,27 @@ fun createAppSettingsDataStore(context: Context) = context.appSettingsStore
fun createShownMessagesDataStore(context: Context) = context.shownMessagesStore

fun createScanBridgeDb(factory: ScanBridgeDbFactory): ScanBridgeDb = factory.createInstance()

fun createScannerIconImageLoader(factory: HttpClientFactory, context: Context): ImageLoader {
val ktorClient: HttpClient = factory.create(
HttpClientConfig(
disableCertValidation = true,
debugLogging = false,
requestTimeoutInSeconds = 2u,
connectTimeoutInSeconds = 2u,
socketTimeoutInSeconds = 2u
)
)
return ImageLoader.Builder(context)
.components {
add(
KtorNetworkFetcherFactory(
httpClient = ktorClient
)
)
}
.build()
}
val appModule = module {
single<DataStore<ShownMessages>>(named<ShownMessages>()) {
create(::createShownMessagesDataStore)
Expand All @@ -71,14 +108,26 @@ val appModule = module {
single<ScanBridgeDb> {
create(::createScanBridgeDb)
}
single(named("scannerIconImageLoader")) {
create(::createScannerIconImageLoader)
}
factory<AndroidMdnsDiscoverService>() bind MdnsDiscoverService::class
single<DatastoreLastRouteRepository>()
single<RoomLastRouteRepository>() bind LastRouteRepository::class
single<DatastoreShownMessagesRepository> { (scope: CoroutineScope) ->
DatastoreShownMessagesRepository(get(named<ShownMessages>()), scope)
} bind ShownMessagesRepository::class
single<KoinBasedScanningProtocolManager>() bind ScanningProtocolManager::class
single<DefaultInitialScanSettingsProvider>() bind InitialScanSettingsProvider::class
single<DefaultPaperFormatProvider>() bind PaperFormatProvider::class
factory<ScanSettingsComposableStateHolder>()
viewModel<ScanningScreenViewModel>()
viewModel<CustomScannerViewModel>()
single<RoomBackedCustomScannerRepository>() bind CustomScannerRepository::class
single<DiscoveryUsecase>()
viewModel<ScannerDiscoveryScreenViewModel>()
single<AndroidMulticastLockHandler>() bind MulticastLockHandler::class
single<AndroidStartScanUseCase>() bind StartScanUseCase::class
includes(scanProtocols)
}

class ScanBridgeApplication : Application() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@

package io.github.chrisimx.scanbridge

import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
Expand All @@ -31,17 +29,20 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import io.github.chrisimx.scanbridge.datastore.appSettingsStore
import io.github.chrisimx.scanbridge.ports.ScanningProtocolManager
import io.github.chrisimx.scanbridge.proto.scanningResponseTimeoutOrNull
import io.github.chrisimx.scanbridge.uicomponents.FullScreenError
import io.github.chrisimx.scanbridge.uicomponents.TemporaryFileHandler
import io.github.chrisimx.scanbridge.util.doTempFilesExist
import io.ktor.http.Url
import kotlin.uuid.Uuid
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames
import org.koin.compose.koinInject
import timber.log.Timber

@Serializable
Expand All @@ -53,7 +54,14 @@ object StartUpScreenRoute : BaseRoute

@Serializable
@SerialName("ScannerRoute")
data class ScannerRoute(val scannerName: String, val scannerURL: String, val sessionID: String) : BaseRoute
@OptIn(ExperimentalSerializationApi::class)
data class ScannerRoute(
val scannerName: String,
@JsonNames("scannerHandle", "scannerURL")
val scannerHandleString: String,
val protocolId: String = "eSCL",
val sessionID: String
) : BaseRoute

@Serializable
@SerialName("CropImageRoute")
Expand All @@ -66,24 +74,18 @@ data class ErrorRoute(val error: String) : BaseRoute
fun NavBackStackEntry.toTypedRoute(): BaseRoute? {
Timber.d("Route changed to: ${destination.route}")
return when (destination.route) {
"StartUpScreenRoute" -> StartUpScreenRoute
"StartUpScreenRoute" -> this.toRoute<StartUpScreenRoute>()

"CropImageRoute/{scanId}/{pageIdx}/{returnRoute}" -> {
val scanId = arguments?.getString("scanId") ?: return null
val returnRouteString = arguments?.getString("returnRoute") ?: return null
CropImageRoute(scanId, returnRouteString)
this.toRoute<CropImageRoute>()
}

"ScannerRoute/{scannerName}/{scannerURL}/{sessionID}" -> {
val scannerName = arguments?.getString("scannerName") ?: return null
val scannerURL = arguments?.getString("scannerURL") ?: return null
val sessionID = arguments?.getString("sessionID") ?: return null
ScannerRoute(scannerName, scannerURL, sessionID)
"ScannerRoute/{scannerName}/{scannerHandleString}/{sessionID}?protocolId={protocolId}" -> {
this.toRoute<ScannerRoute>()
}

"ErrorRoute/{error}" -> {
val error = arguments?.getString("error") ?: return null
ErrorRoute(error)
this.toRoute<ErrorRoute>()
}

else -> null
Expand All @@ -94,6 +96,7 @@ fun NavBackStackEntry.toTypedRoute(): BaseRoute? {
fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
val context = LocalContext.current
val appSettings = context.appSettingsStore.data
val protocolManager = koinInject<ScanningProtocolManager>()

NavHost(
modifier = Modifier.testTag("root_node"),
Expand Down Expand Up @@ -130,19 +133,26 @@ fun ScanBridgeNavHost(navController: NavHostController, startDestination: Any) {
val debug = appSettingsCurrent.writeDebug
val certValidationDisabled = appSettingsCurrent.disableCertChecks
val timeout = appSettingsCurrent.scanningResponseTimeoutOrNull?.value?.toUInt() ?: 25u
val scannerHandle = protocolManager.getScannerHandle(
scannerRoute.protocolId,
scannerRoute.scannerHandleString
)
Timber.tag("ScanBridgeNavHost")
.d(
"Navigating to scanner ${scannerRoute.scannerName} at ${scannerRoute.scannerURL}. Timeout is $timeout seconds, Debug is $debug. Disabling of cert checks is $certValidationDisabled. Session id is ${scannerRoute.sessionID}"
"Navigating to scanner ${scannerRoute.scannerName} at ${scannerRoute.scannerHandleString}. Timeout is $timeout seconds, Debug is $debug. Disabling of cert checks is $certValidationDisabled. Session id is ${scannerRoute.sessionID}"
)

check(scannerHandle != null) {
"Scanner handle not found for protocol ${scannerRoute.protocolId} and handle ${scannerRoute.scannerHandleString}"
}
ScanningScreen(
scannerRoute.scannerName,
Url(scannerRoute.scannerURL),
scannerHandle,
navController,
timeout,
debug,
certValidationDisabled,
Uuid.parse(scannerRoute.sessionID),
context.applicationContext as Application
Uuid.parse(scannerRoute.sessionID)
)
}
}
Expand Down
Loading
Loading