From a0d3eae4b4e5ff6ae62f9ffc81c971be5822387b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 10:35:57 +0100 Subject: [PATCH 1/6] feat: [QRSCANNER] qr-scanner module to connect by scanning the desktop QR code --- FloconAndroid/gradle/libs.versions.toml | 2 + .../qrscanner-no-op/build.gradle.kts | 31 +++++++++++ .../flocon/qrscanner/ScanQrCodeAndConnect.kt | 9 ++++ .../flocon/qrscanner/ConnectWithQrCode.kt | 8 +++ FloconAndroid/qrscanner/build.gradle.kts | 36 +++++++++++++ .../src/androidMain/AndroidManifest.xml | 11 ++++ .../flocon/qrscanner/ScanQrCodeAndConnect.kt | 51 +++++++++++++++++++ .../flocon/qrscanner/ConnectWithQrCode.kt | 19 +++++++ .../flocon/qrscanner/QrCodeAddress.kt | 25 +++++++++ .../flocon/qrscanner/QrCodeAddressTest.kt | 30 +++++++++++ FloconAndroid/settings.gradle.kts | 2 + docs/setup.md | 18 ++++++- 12 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 FloconAndroid/qrscanner-no-op/build.gradle.kts create mode 100644 FloconAndroid/qrscanner-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt create mode 100644 FloconAndroid/qrscanner-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt create mode 100644 FloconAndroid/qrscanner/build.gradle.kts create mode 100644 FloconAndroid/qrscanner/src/androidMain/AndroidManifest.xml create mode 100644 FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt create mode 100644 FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt create mode 100644 FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddress.kt create mode 100644 FloconAndroid/qrscanner/src/jvmTest/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddressTest.kt diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index bf22e36a9..b0fa9c516 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -25,6 +25,7 @@ maven-publish = "0.37.0" multiplatform-settings = "1.3.0" navigation3 = "1.2.0-rc01" okhttp-bom = "4.12.0" +play-services-code-scanner = "16.1.0" process-phoenix = "3.0.0" protobuf = "4.36.2" protobuf-plugin = "0.10.0" @@ -124,6 +125,7 @@ sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } # Third Party brotli-dec = { module = "org.brotli:dec", version.ref = "brotli" } +google-play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "play-services-code-scanner" } jakewharton-process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version.ref = "process-phoenix" } multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } vanniktech-mavenPublish-gradlePlugin = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version.ref = "maven-publish" } diff --git a/FloconAndroid/qrscanner-no-op/build.gradle.kts b/FloconAndroid/qrscanner-no-op/build.gradle.kts new file mode 100644 index 000000000..e255bf3b3 --- /dev/null +++ b/FloconAndroid/qrscanner-no-op/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("flocon.kotlin.library") + id("flocon.android.library") + id("flocon.jvm.library") + id("flocon.publish") +} + +kotlin { + android { + namespace = "io.github.openflocon.flocon.qrscanner.noop" + } + + sourceSets { + commonMain.dependencies { + api(projects.floconNoOp) + } + } +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-qr-scanner-no-op", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + + pom { + name = "Flocon QR Scanner No-Op" + } +} diff --git a/FloconAndroid/qrscanner-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt b/FloconAndroid/qrscanner-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt new file mode 100644 index 000000000..79718aca6 --- /dev/null +++ b/FloconAndroid/qrscanner-no-op/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocon.qrscanner + +import android.content.Context +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConnectResult + +/** Does nothing, no scanner is opened: always `null`. */ +@Suppress("UNUSED_PARAMETER") +suspend fun Flocon.Companion.scanQrCodeAndConnect(context: Context): FloconConnectResult? = null diff --git a/FloconAndroid/qrscanner-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt b/FloconAndroid/qrscanner-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt new file mode 100644 index 000000000..76a3029dc --- /dev/null +++ b/FloconAndroid/qrscanner-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocon.qrscanner + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConnectResult + +/** Does nothing, always [FloconConnectResult.NotStarted]. */ +@Suppress("UNUSED_PARAMETER") +suspend fun Flocon.Companion.connectWithQrCode(content: String): FloconConnectResult = FloconConnectResult.NotStarted diff --git a/FloconAndroid/qrscanner/build.gradle.kts b/FloconAndroid/qrscanner/build.gradle.kts new file mode 100644 index 000000000..3f2c63484 --- /dev/null +++ b/FloconAndroid/qrscanner/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("flocon.kotlin.library") + id("flocon.android.library") + id("flocon.jvm.library") + id("flocon.publish") +} + +kotlin { + android { + namespace = "io.github.openflocon.flocon.qrscanner" + } + + sourceSets { + commonMain.dependencies { + implementation(projects.flocon) + implementation(libs.kotlinx.coroutines.core) + } + + androidMain.dependencies { + implementation(libs.google.play.services.code.scanner) + } + } +} + +mavenPublishing { + coordinates( + groupId = project.property("floconGroupId") as String, + artifactId = "flocon-qr-scanner", + version = System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String + ) + + + pom { + name = "Flocon QR Scanner" + } +} diff --git a/FloconAndroid/qrscanner/src/androidMain/AndroidManifest.xml b/FloconAndroid/qrscanner/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..10805ff3b --- /dev/null +++ b/FloconAndroid/qrscanner/src/androidMain/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt new file mode 100644 index 000000000..19ea8c3b0 --- /dev/null +++ b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt @@ -0,0 +1,51 @@ +package io.github.openflocon.flocon.qrscanner + +import android.content.Context +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConnectResult +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Opens the Google code scanner, then connects to the desktop app at the address held by the QR code + * it reads, see [connectWithQrCode]. + * + * The scanner is a screen of Google Play Services: the app needs no camera permission. It requires + * Google Play Services on the device, otherwise the result is [FloconConnectResult.Failed]. + * + * ``` + * val result = Flocon.scanQrCodeAndConnect(context) + * ``` + * + * @param context preferably an `Activity`. + * @return the result of the connection, or `null` if the user closed the scanner without scanning. + */ +suspend fun Flocon.Companion.scanQrCodeAndConnect(context: Context): FloconConnectResult? = + when (val scan = scanQrCode(context)) { + Scan.Cancelled -> null + is Scan.Failed -> FloconConnectResult.Failed(scan.reason) + is Scan.Code -> connectWithQrCode(scan.content) + } + +private sealed interface Scan { + data class Code(val content: String) : Scan + data object Cancelled : Scan + data class Failed(val reason: String) : Scan +} + +private suspend fun scanQrCode(context: Context): Scan = suspendCancellableCoroutine { continuation -> + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + GmsBarcodeScanning.getClient(context, options) + .startScan() + .addOnSuccessListener { barcode -> + val content = barcode.rawValue + continuation.resume(if (content == null) Scan.Failed("The QR code is empty") else Scan.Code(content)) + } + .addOnCanceledListener { continuation.resume(Scan.Cancelled) } + .addOnFailureListener { error -> continuation.resume(Scan.Failed(error.message ?: "The scanner failed")) } +} diff --git a/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt b/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt new file mode 100644 index 000000000..0f0cc3aaf --- /dev/null +++ b/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/ConnectWithQrCode.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.qrscanner + +import io.github.openflocon.flocon.Flocon +import io.github.openflocon.flocon.FloconConnectResult +import io.github.openflocon.flocon.connect + +/** + * Connects to the desktop app at the address held by a scanned QR code. + * + * Same as `Flocon.connect(address)`, for the raw content of a QR code. The content is either the IP address + * (or host) of the computer, or a URL whose host is that address, such as `flocon://192.168.1.20`: the scheme, + * the port and the path are ignored. On Android, `Flocon.scanQrCodeAndConnect` scans the code and calls this. + * + * @param content the text read from the QR code. + * @return [FloconConnectResult.Connected], or why it could not connect. The messages are kept + * in every case, call it again to retry. + */ +suspend fun Flocon.Companion.connectWithQrCode(content: String): FloconConnectResult = + connect(addressFromQrCode(content)) diff --git a/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddress.kt b/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddress.kt new file mode 100644 index 000000000..acb92e776 --- /dev/null +++ b/FloconAndroid/qrscanner/src/commonMain/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddress.kt @@ -0,0 +1,25 @@ +package io.github.openflocon.flocon.qrscanner + +/** + * The address to connect to for the content of a QR code. + * + * The content is either the bare address of the computer (`192.168.1.20`), or a URL whose host is that + * address (`flocon://192.168.1.20`, `wss://my-mac.local:9023`): the scheme, the port and the path are + * ignored, as the port is always 9023. + * + * Anything that is not a URL is returned trimmed and unchanged, so that `Flocon.connect` reports it + * as an invalid address when it cannot be read. + */ +internal fun addressFromQrCode(content: String): String { + val text = content.trim() + val schemeEnd = text.indexOf("://") + if (schemeEnd <= 0) return text + val host = text + .substring(schemeEnd + 3) + .substringBefore('/') + .substringBefore('?') + .substringBefore('#') + .substringAfterLast('@') + .substringBefore(':') + return host.ifEmpty { text } +} diff --git a/FloconAndroid/qrscanner/src/jvmTest/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddressTest.kt b/FloconAndroid/qrscanner/src/jvmTest/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddressTest.kt new file mode 100644 index 000000000..7d5fc6f94 --- /dev/null +++ b/FloconAndroid/qrscanner/src/jvmTest/kotlin/io/github/openflocon/flocon/qrscanner/QrCodeAddressTest.kt @@ -0,0 +1,30 @@ +package io.github.openflocon.flocon.qrscanner + +import kotlin.test.Test +import kotlin.test.assertEquals + +class QrCodeAddressTest { + @Test + fun `a plain address is kept`() { + assertEquals("192.168.1.20", addressFromQrCode("192.168.1.20")) + } + + @Test + fun `whitespace around the address is removed`() { + assertEquals("my-mac.local", addressFromQrCode(" my-mac.local\n")) + } + + @Test + fun `a url gives its host`() { + assertEquals("192.168.1.20", addressFromQrCode("flocon://192.168.1.20")) + assertEquals("my-mac.local", addressFromQrCode("wss://my-mac.local:9023/path?token=1")) + assertEquals("192.168.1.20", addressFromQrCode("https://user@192.168.1.20:9023#top")) + } + + @Test + fun `text that is not an address is kept so it is reported as invalid`() { + assertEquals("hello world", addressFromQrCode("hello world")) + assertEquals("://", addressFromQrCode("://")) + assertEquals("flocon://", addressFromQrCode("flocon://")) + } +} diff --git a/FloconAndroid/settings.gradle.kts b/FloconAndroid/settings.gradle.kts index 4bb3a9219..84c527341 100644 --- a/FloconAndroid/settings.gradle.kts +++ b/FloconAndroid/settings.gradle.kts @@ -59,6 +59,8 @@ include(":files") include(":files-no-op") include(":sharedprefs") include(":sharedprefs-no-op") +include(":qrscanner") +include(":qrscanner-no-op") include(":network:core") include(":network:core-no-op") include(":database:core") diff --git a/docs/setup.md b/docs/setup.md index 10735c8ab..477fead1c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -127,7 +127,23 @@ Until a desktop is reachable, everything the plugins log is kept in memory. `Flo ### Connecting with a QR code -The desktop app has a **Connect** button in the top bar (QR code icon). It opens a dialog listing the IP addresses of the computer on the local network (one per network interface) and, for each one, a button that shows the address as a QR code. The QR code contains only the IP address, the same value as `Flocon.connect("192.168.1.20")`. Scanning it from the SDK is not available yet. +The desktop app has a **Connect** button in the top bar (QR code icon). It opens a dialog listing the IP addresses of the computer on the local network (one per network interface) and, for each one, a button that shows the address as a QR code. The QR code contains only the IP address, the same value as `Flocon.connect("192.168.1.20")`. + +On Android, the optional `flocon-qr-scanner` module scans it for you, so nobody has to type the IP. It opens the Google code scanner, a screen of Google Play Services, so the app needs no camera permission but the device needs Google Play Services: + +```kotlin +debugImplementation("io.github.openflocon:flocon-qr-scanner:LAST_VERSION") +releaseImplementation("io.github.openflocon:flocon-qr-scanner-no-op:LAST_VERSION") +``` + +```kotlin +import io.github.openflocon.flocon.qrscanner.scanQrCodeAndConnect + +// from a button of a debug menu +val result = Flocon.scanQrCodeAndConnect(activity) +``` + +`scanQrCodeAndConnect` is a `suspend` function. It returns the `FloconConnectResult` of the connection (a failed scan, for example without Google Play Services, is a `Failed`), or `null` if the user closed the scanner without scanning. If you scan with your own screen, give the text you read to `Flocon.connectWithQrCode(content)`: the content is the IP address, or a URL whose host is that address (the scheme, the port and the path are ignored). Up to 5000 messages are kept: above that, the oldest are dropped. `connect { bufferCapacity = ... }` changes this size. From 275d48059b46fd4a1d39eb702c3c89e745c6f542 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 10:35:58 +0100 Subject: [PATCH 2/6] feat(sample): scan the desktop QR code from the connection screen --- .../myapplication/multi/ui/SampleQrScanner.kt | 12 +++++++++++ .../multi/ui/screens/ConnectionScreen.kt | 21 +++++++++++++++++++ FloconAndroid/sample/android/build.gradle.kts | 2 ++ .../com/flocon/sample/android/MainActivity.kt | 3 +++ 4 files changed, 38 insertions(+) create mode 100644 FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/SampleQrScanner.kt diff --git a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/SampleQrScanner.kt b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/SampleQrScanner.kt new file mode 100644 index 000000000..c649d1181 --- /dev/null +++ b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/SampleQrScanner.kt @@ -0,0 +1,12 @@ +package io.github.openflocon.flocon.myapplication.multi.ui + +import io.github.openflocon.flocon.FloconConnectResult + +/** + * Scans the QR code of Flocon Desktop and connects to it, on the platforms that can: the Android app sets it. + * The connection screen shows its button only when it is set. + */ +object SampleQrScanner { + /** Returns the result of the connection, or `null` if the scan was cancelled. */ + var scan: (suspend () -> FloconConnectResult?)? = null +} diff --git a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/screens/ConnectionScreen.kt b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/screens/ConnectionScreen.kt index 5efac7f75..fffa01dad 100644 --- a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/screens/ConnectionScreen.kt +++ b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/ui/screens/ConnectionScreen.kt @@ -43,6 +43,7 @@ import io.github.openflocon.flocon.FloconConnectResult import io.github.openflocon.flocon.FloconConnectionState import io.github.openflocon.flocon.connect import io.github.openflocon.flocon.connectionState +import io.github.openflocon.flocon.myapplication.multi.ui.SampleQrScanner import io.github.openflocon.flocon.myapplication.multi.ui.components.AccentButton import io.github.openflocon.flocon.myapplication.multi.ui.components.Card import io.github.openflocon.flocon.myapplication.multi.ui.components.InfoCard @@ -76,6 +77,18 @@ fun ConnectionScreen(onBack: () -> Unit) { } } + val scanQrCode = SampleQrScanner.scan?.let { scan -> + { + if (!connecting) { + scope.launch { + connecting = true + scan()?.let { result = it } + connecting = false + } + } + } + } + ScreenScaffold( title = "Remote connection", subtitle = "Connect to Flocon Desktop by IP", @@ -138,6 +151,14 @@ fun ConnectionScreen(onBack: () -> Unit) { onClick = connect, ) } + if (scanQrCode != null) { + AccentButton( + text = "Scan the QR code of Flocon Desktop", + enabled = !connecting, + onClick = scanQrCode, + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + ) + } when (val current = result) { is FloconConnectResult.Failed -> ResultLine("✕ ${current.reason}", colors.error) is FloconConnectResult.InvalidAddress -> ResultLine("✕ ${current.reason}", colors.error) diff --git a/FloconAndroid/sample/android/build.gradle.kts b/FloconAndroid/sample/android/build.gradle.kts index 8e8765d44..4d7f53aa3 100644 --- a/FloconAndroid/sample/android/build.gradle.kts +++ b/FloconAndroid/sample/android/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(projects.webview) implementation(projects.crashreporter) implementation(projects.tables) + implementation(projects.qrscanner) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp) @@ -159,6 +160,7 @@ val floconNoOpModules = mapOf( ":network:okhttp-interceptor" to ":network:okhttp-interceptor-no-op", ":sharedprefs" to ":sharedprefs-no-op", ":tables" to ":tables-no-op", + ":qrscanner" to ":qrscanner-no-op", ) configurations.configureEach { diff --git a/FloconAndroid/sample/android/src/main/java/com/flocon/sample/android/MainActivity.kt b/FloconAndroid/sample/android/src/main/java/com/flocon/sample/android/MainActivity.kt index 0fe07ff24..7402f6780 100644 --- a/FloconAndroid/sample/android/src/main/java/com/flocon/sample/android/MainActivity.kt +++ b/FloconAndroid/sample/android/src/main/java/com/flocon/sample/android/MainActivity.kt @@ -32,6 +32,7 @@ import io.github.openflocon.flocon.myapplication.multi.tester.AndroidTestContext import io.github.openflocon.flocon.myapplication.multi.ui.App import io.github.openflocon.flocon.myapplication.multi.ui.SampleFeature import io.github.openflocon.flocon.myapplication.multi.ui.SampleFeatures +import io.github.openflocon.flocon.myapplication.multi.ui.SampleQrScanner import io.github.openflocon.flocon.myapplication.multi.ui.registerAndroidSampleFeatures import io.github.openflocon.flocon.myapplication.multi.ui.registerAndroidSampleWebView import io.github.openflocon.flocon.webview.FloconWebView @@ -42,6 +43,7 @@ import io.github.openflocon.flocon.plugins.device.FloconDevice import io.github.openflocon.flocon.plugins.files.FloconFiles import io.github.openflocon.flocon.plugins.sharedprefs.FloconPreferences import io.github.openflocon.flocon.connect +import io.github.openflocon.flocon.qrscanner.scanQrCodeAndConnect import io.github.openflocon.flocon.startFlocon import io.github.openflocon.flocon.tables.FloconTable import io.ktor.client.HttpClient @@ -114,6 +116,7 @@ class MainActivity : ComponentActivity() { AndroidTestContext.inMemoryDb = Databases.getInMemoryDogDatabase(applicationContext) AndroidTestContext.grpcSayHello = { GrpcController.sayHello() } registerAndroidSampleFeatures(applicationContext) + SampleQrScanner.scan = { Flocon.scanQrCodeAndConnect(this@MainActivity) } registerAndroidSampleWebView() SampleFeatures.on(SampleFeature.Accessibility) { val announcement = "Item added to the cart" From 9359ac781fa255c87a80fc619764e15e20ffc778 Mon Sep 17 00:00:00 2001 From: Florent Champigny Date: Sat, 31 Oct 2026 12:45:54 +0100 Subject: [PATCH 3/6] renamed --- .../github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt index 19ea8c3b0..0104b701e 100644 --- a/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt +++ b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt @@ -30,7 +30,7 @@ suspend fun Flocon.Companion.scanQrCodeAndConnect(context: Context): FloconConne is Scan.Code -> connectWithQrCode(scan.content) } -private sealed interface Scan { +private sealed interface ScanQrCodeResult { data class Code(val content: String) : Scan data object Cancelled : Scan data class Failed(val reason: String) : Scan From 56b9ebd3ca4d5959fb8d388f9dd80ef25a39c2d4 Mon Sep 17 00:00:00 2001 From: Florent Champigny Date: Sat, 31 Oct 2026 12:48:39 +0100 Subject: [PATCH 4/6] renamed --- .../flocon/qrscanner/ScanQrCodeAndConnect.kt | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt index 0104b701e..ff74c9543 100644 --- a/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt +++ b/FloconAndroid/qrscanner/src/androidMain/kotlin/io/github/openflocon/flocon/qrscanner/ScanQrCodeAndConnect.kt @@ -6,8 +6,8 @@ import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import io.github.openflocon.flocon.Flocon import io.github.openflocon.flocon.FloconConnectResult -import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume /** * Opens the Google code scanner, then connects to the desktop app at the address held by the QR code @@ -25,27 +25,38 @@ import kotlinx.coroutines.suspendCancellableCoroutine */ suspend fun Flocon.Companion.scanQrCodeAndConnect(context: Context): FloconConnectResult? = when (val scan = scanQrCode(context)) { - Scan.Cancelled -> null - is Scan.Failed -> FloconConnectResult.Failed(scan.reason) - is Scan.Code -> connectWithQrCode(scan.content) + ScanQrCodeResult.Cancelled -> null + is ScanQrCodeResult.Failed -> FloconConnectResult.Failed(scan.reason) + is ScanQrCodeResult.Code -> connectWithQrCode(scan.content) } private sealed interface ScanQrCodeResult { - data class Code(val content: String) : Scan - data object Cancelled : Scan - data class Failed(val reason: String) : Scan + data class Code(val content: String) : ScanQrCodeResult + data object Cancelled : ScanQrCodeResult + data class Failed(val reason: String) : ScanQrCodeResult } -private suspend fun scanQrCode(context: Context): Scan = suspendCancellableCoroutine { continuation -> - val options = GmsBarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() - GmsBarcodeScanning.getClient(context, options) - .startScan() - .addOnSuccessListener { barcode -> - val content = barcode.rawValue - continuation.resume(if (content == null) Scan.Failed("The QR code is empty") else Scan.Code(content)) - } - .addOnCanceledListener { continuation.resume(Scan.Cancelled) } - .addOnFailureListener { error -> continuation.resume(Scan.Failed(error.message ?: "The scanner failed")) } -} +private suspend fun scanQrCode(context: Context): ScanQrCodeResult = + suspendCancellableCoroutine { continuation -> + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + GmsBarcodeScanning.getClient(context, options) + .startScan() + .addOnSuccessListener { barcode -> + val content = barcode.rawValue + continuation.resume( + if (content == null) { + ScanQrCodeResult.Failed("The QR code is empty") + } else { + ScanQrCodeResult.Code(content) + } + ) + } + .addOnCanceledListener { + continuation.resume(ScanQrCodeResult.Cancelled) + } + .addOnFailureListener { error -> + continuation.resume(ScanQrCodeResult.Failed(error.message ?: "The scanner failed")) + } + } From 98abc4e6a0047b844d9801fe8628adfab38225e3 Mon Sep 17 00:00:00 2001 From: Florent Champigny Date: Sat, 31 Oct 2026 12:49:25 +0100 Subject: [PATCH 5/6] fix --- .idea/.gitignore | 3 +++ docs/setup.md | 18 +----------------- 2 files changed, 4 insertions(+), 17 deletions(-) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/docs/setup.md b/docs/setup.md index 477fead1c..10735c8ab 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -127,23 +127,7 @@ Until a desktop is reachable, everything the plugins log is kept in memory. `Flo ### Connecting with a QR code -The desktop app has a **Connect** button in the top bar (QR code icon). It opens a dialog listing the IP addresses of the computer on the local network (one per network interface) and, for each one, a button that shows the address as a QR code. The QR code contains only the IP address, the same value as `Flocon.connect("192.168.1.20")`. - -On Android, the optional `flocon-qr-scanner` module scans it for you, so nobody has to type the IP. It opens the Google code scanner, a screen of Google Play Services, so the app needs no camera permission but the device needs Google Play Services: - -```kotlin -debugImplementation("io.github.openflocon:flocon-qr-scanner:LAST_VERSION") -releaseImplementation("io.github.openflocon:flocon-qr-scanner-no-op:LAST_VERSION") -``` - -```kotlin -import io.github.openflocon.flocon.qrscanner.scanQrCodeAndConnect - -// from a button of a debug menu -val result = Flocon.scanQrCodeAndConnect(activity) -``` - -`scanQrCodeAndConnect` is a `suspend` function. It returns the `FloconConnectResult` of the connection (a failed scan, for example without Google Play Services, is a `Failed`), or `null` if the user closed the scanner without scanning. If you scan with your own screen, give the text you read to `Flocon.connectWithQrCode(content)`: the content is the IP address, or a URL whose host is that address (the scheme, the port and the path are ignored). +The desktop app has a **Connect** button in the top bar (QR code icon). It opens a dialog listing the IP addresses of the computer on the local network (one per network interface) and, for each one, a button that shows the address as a QR code. The QR code contains only the IP address, the same value as `Flocon.connect("192.168.1.20")`. Scanning it from the SDK is not available yet. Up to 5000 messages are kept: above that, the oldest are dropped. `connect { bufferCapacity = ... }` changes this size. From ed76423aed995eeed9b9eff94c69d941557f6e69 Mon Sep 17 00:00:00 2001 From: Florent CHAMPIGNY Date: Sat, 31 Oct 2026 12:51:29 +0100 Subject: [PATCH 6/6] Delete .idea/.gitignore --- .idea/.gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521a..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml