diff --git a/.gitignore b/.gitignore index af24624..2568565 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,12 @@ src-tauri/target/ # scripts/sync-android-sources.sh after every init. src-tauri/gen/ +# src-tauri/android/ is source, but it holds a build.gradle.kts with no +# settings.gradle beside it, so any Gradle daemon that scans the tree — an IDE's, +# not the wrapper's — mistakes that directory for a project root and drops a +# cache in it. Nothing it writes is ours. +src-tauri/android/**/.gradle/ + # Android signing material. A keystore in the repo is a signing key given away; # CI writes both of these from secrets. *.jks diff --git a/scripts/sync-android-sources.sh b/scripts/sync-android-sources.sh index a740fcc..11ec176 100755 --- a/scripts/sync-android-sources.sh +++ b/scripts/sync-android-sources.sh @@ -119,4 +119,62 @@ echo " ✓ btleplug $BTLEPLUG_VERSION Java backend (com.nonpolynomial.btleplug) cp -r "$JNI_UTILS_SRC/java/src/main/java/io" "$APP/java/" echo " ✓ jni-utils $JNI_UTILS_VERSION Java support (io.github.gedgygedgy.rust)" +# --------------------------------------------------------------------------- +# 3. Patch droidplug: subscribing to an *indicate* characteristic. +# +# Upstream writes ENABLE_NOTIFICATION_VALUE (0x01 0x00) to the CCCD whatever the +# characteristic supports. A characteristic that indicates but does not notify +# rejects that write, and the subscribe fails with "Unable to write descriptor". +# BlueZ picks the value from the properties, so this is Android-only — which is +# what made it look like a Click problem rather than a backend one. +# +# The Click's Sync TX channel (00000004-19ca-…) is read/indicate, so without +# this we lose it on Android while every desktop build is fine. +PERIPHERAL="$APP/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java" +OLD_LINE='descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);' +NEW_LINE='descriptor.setValue(!enable ? BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE : (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);' + +if ! grep -qF "$OLD_LINE" "$PERIPHERAL"; then + echo "❌ droidplug's CCCD write is not where this patch expects it." >&2 + echo " btleplug $BTLEPLUG_VERSION may have fixed or moved it — re-check" >&2 + echo " Peripheral.setCharacteristicNotification before dropping this step." >&2 + exit 1 +fi +python3 - "$PERIPHERAL" "$OLD_LINE" "$NEW_LINE" <<'PATCH' +import sys +path, old, new = sys.argv[1], sys.argv[2], sys.argv[3] +text = open(path).read() +open(path, 'w').write(text.replace(old, new, 1)) +PATCH +echo " ✓ patched droidplug to subscribe to indicate characteristics" + +# Patch droidplug: negotiate a usable ATT MTU. +# +# Upstream never calls requestMtu, so Android stays at the 23-byte default and a +# notification carries at most 20 bytes of payload. BlueZ negotiates 517, which +# is why this is invisible on desktop and why the `−` pod's button frames — 21 +# bytes, one longer than the cap — arrive whole on a PC and truncated on a +# phone. A truncated frame fails to parse, so the paddle simply does nothing. +# +# Fire-and-forget: the request is queued on the connection and the negotiation +# completes long before service discovery and the CCCD writes, so there is +# nothing to wait for here. A refusal is survivable — it leaves us exactly where +# we already were. +MTU_ANCHOR=' if (newState == BluetoothGatt.STATE_CONNECTED) {' +MTU_PATCH=' if (newState == BluetoothGatt.STATE_CONNECTED) { + gatt.requestMtu(517);' + +if ! grep -qF "$MTU_ANCHOR" "$PERIPHERAL"; then + echo "❌ droidplug's connect callback is not where this patch expects it." >&2 + exit 1 +fi +python3 - "$PERIPHERAL" "$MTU_ANCHOR" "$MTU_PATCH" <<'PATCH' +import sys +path, old, new = sys.argv[1], sys.argv[2], sys.argv[3] +text = open(path).read() +assert text.count(old) == 1, f"expected exactly one connect callback, found {text.count(old)}" +open(path, 'w').write(text.replace(old, new, 1)) +PATCH +echo " ✓ patched droidplug to request a 517-byte MTU" + echo "✅ Android sources synced"