From 3c917da56a84fa061e8c67dc5245bd655da08914 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 5 Aug 2026 18:44:16 +0200 Subject: [PATCH] Modify code to support minor API versions --- .../wearos/BluetoothServerService.kt | 4 +- docs/BLUETOOTH_SERVER.md | 38 +++++++++++++------ .../shared/WearBluetoothProtocol.kt | 1 + .../catima/wear/BluetoothCardClient.kt | 22 ++++++++--- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt index 5de57a587..c582ffde8 100644 --- a/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt +++ b/app/src/main/java/protect/card_locker/wearos/BluetoothServerService.kt @@ -148,10 +148,10 @@ class BluetoothServerService : Service() { val firstLine = reader.readLine()?.trim() ?: return if (firstLine == WearBluetoothProtocol.BT_CMD_VERSIONS) { - val versions = JSONArray().put(WearBluetoothProtocol.PROTOCOL_VERSION).toString() + val versions = JSONArray().put("${WearBluetoothProtocol.PROTOCOL_VERSION}.${WearBluetoothProtocol.PROTOCOL_MINOR_VERSION}").toString() writer.println(versions) writer.flush() - Log.d(TAG, "Sent supported versions to $deviceName") + Log.d(TAG, "Sent supported versions to $deviceName: $versions") return } diff --git a/docs/BLUETOOTH_SERVER.md b/docs/BLUETOOTH_SERVER.md index 841982e15..91d04bc8e 100644 --- a/docs/BLUETOOTH_SERVER.md +++ b/docs/BLUETOOTH_SERVER.md @@ -14,7 +14,14 @@ The Bluetooth server is currently only used by the Catima WearOS companion app. ## API versions -There is currently only a single API version. Version 1. +For each API version, we have a major and a minor version. The rules for versioning are as follows: +1. A new minor version may add fields or endpoints, but must never modify or remove existing fields or endpoints. +2. A new minor version must not change any authentication systems compared to the previous minor version. +3. A device cannot request Catima to use a specific minor version, it will always return data as per the most recently supported minor version. + +The main driver of compatibility is the major version. + +Due to this system, a device only supporting API version 1.0 can talk to Catima supporting API version 1.1 with no issues, as long as it ignores fields it doesn't know. On the other side, a device wanting to use API version 1.1 with Catima on API version 1.0 has to accept certain fields may not be available. Which minor version introduces which additional fields and endpoints is documented in this file. ### Unversioned @@ -22,11 +29,20 @@ There is currently only a single API version. Version 1. Return type: JSON. -Value: +Returns all supported major versions and the most recent supported minor version. + +Example request: ``` -[1] +/VERSIONS ``` +Example return value: +``` +["1.0"] +``` + +In this example case, the phone supports only API version 1. Of API version 1, the most recent minor version supported is 0. + ### V1 All V1 endpoints are authenticated and must be communicated with by sending 2 lines over an RfConn socket. First the full command, then a device-specific token (recommended: a base64-encoded version of a 32 characters long SecureRandom-generated string). @@ -47,14 +63,14 @@ Request a single page of cards with page and total page details. The page ID is Each card contains the following fields: -| Column | Type | Description | -|-------------------|-------------------|----------------------------| -| `id` | `int` | Unique card ID | -| `store` | `String` | Card name | -| `cardId` | `String` | Card ID. | -| `barcodeId` | `Nullable String` | Barcode value. If empty, it's the same as the card ID. | -| `barcodeType` | `Nullable String` | The barcode type name, matching [com.google.zxing.BarcodeFormat](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). When null, there is no barcode for this card. | -| `headerColor` | `int` | Header color, matching [Android color int](https://developer.android.com/reference/android/graphics/Color). | +| Column | Type | Description | Introduced in minor version | +|-------------------|-------------------|----------------------------| --------------------------- | +| `id` | `int` | Unique card ID | 0 | +| `store` | `String` | Card name | 0 | +| `cardId` | `String` | Card ID. | 0 | +| `barcodeId` | `Nullable String` | Barcode value. If empty, it's the same as the card ID. | 0 | +| `barcodeType` | `Nullable String` | The barcode type name, matching [com.google.zxing.BarcodeFormat](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). When null, there is no barcode for this card. | 0 | +| `headerColor` | `int` | Header color, matching [Android color int](https://developer.android.com/reference/android/graphics/Color). | 0 | Example request: ``` diff --git a/shared/src/main/java/protect/card_locker/shared/WearBluetoothProtocol.kt b/shared/src/main/java/protect/card_locker/shared/WearBluetoothProtocol.kt index 3928e6364..af49d74da 100644 --- a/shared/src/main/java/protect/card_locker/shared/WearBluetoothProtocol.kt +++ b/shared/src/main/java/protect/card_locker/shared/WearBluetoothProtocol.kt @@ -6,6 +6,7 @@ object WearBluetoothProtocol { val BT_SERVICE_UUID: UUID = UUID.fromString("e5b4f020-3a7e-4b6d-9f2c-1a8c5d3e7f90") const val BT_SERVICE_NAME = "CatimaWear" const val PROTOCOL_VERSION = 1 + const val PROTOCOL_MINOR_VERSION = 0 const val BT_CMD_VERSIONS = "/VERSIONS" const val BT_CMD_TOKEN_PREFIX = "TOKEN:" const val BT_CMD_VERSION_PREFIX = "/V$PROTOCOL_VERSION/" diff --git a/wear/src/main/java/me/hackerchick/catima/wear/BluetoothCardClient.kt b/wear/src/main/java/me/hackerchick/catima/wear/BluetoothCardClient.kt index 915c50874..6d462f5f0 100644 --- a/wear/src/main/java/me/hackerchick/catima/wear/BluetoothCardClient.kt +++ b/wear/src/main/java/me/hackerchick/catima/wear/BluetoothCardClient.kt @@ -97,13 +97,25 @@ object BluetoothCardClient { socket.connect() val supportedVersions = requestSupportedVersions(socket) ?: return null to SyncStatus.PHONE_NOT_REACHABLE - if (WearBluetoothProtocol.PROTOCOL_VERSION !in supportedVersions) { - Log.w(TAG, "Phone does not support API version ${WearBluetoothProtocol.PROTOCOL_VERSION}") + var majorVersionIsSupported = false + var mostRecentMinorVersion = -1 + run breaking@{ + supportedVersions.forEach { + val supportedVersionParts = it.split('.') + if (supportedVersionParts[0] == WearBluetoothProtocol.PROTOCOL_VERSION.toString()) { + majorVersionIsSupported = true + mostRecentMinorVersion = supportedVersionParts[1].toInt() + return@breaking + } + } + } + if (!majorVersionIsSupported) { + Log.w(TAG, "Phone does not support major API version ${WearBluetoothProtocol.PROTOCOL_VERSION}") return null to SyncStatus.VERSION_INCOMPATIBLE } socket.close() socket = null - Log.d(TAG, "Connected to $deviceName with API version ${WearBluetoothProtocol.PROTOCOL_VERSION}") + Log.d(TAG, "Connected to $deviceName with major API version ${WearBluetoothProtocol.PROTOCOL_VERSION}, phone supports up to minor API version ${mostRecentMinorVersion}, we can use up to ${WearBluetoothProtocol.PROTOCOL_MINOR_VERSION}") socket = device.createRfcommSocketToServiceRecord(WearBluetoothProtocol.BT_SERVICE_UUID) socket.connect() @@ -173,7 +185,7 @@ object BluetoothCardClient { writer.println("${WearBluetoothProtocol.BT_CMD_TOKEN_PREFIX}$token") } - private fun requestSupportedVersions(socket: BluetoothSocket): Set? { + private fun requestSupportedVersions(socket: BluetoothSocket): Set? { val writer = PrintWriter(OutputStreamWriter(socket.outputStream, "UTF-8"), false) val reader = BufferedReader(InputStreamReader(socket.inputStream, "UTF-8")) writer.print("${WearBluetoothProtocol.BT_CMD_VERSIONS}\n") @@ -183,7 +195,7 @@ object BluetoothCardClient { val versions = JSONArray(response) buildSet { for (index in 0 until versions.length()) { - add(versions.getInt(index)) + add(versions.getString(index)) } } } catch (e: Exception) {