From 01235573868408ff566b2e0997c8bf340dfb2aa3 Mon Sep 17 00:00:00 2001 From: Olivier Booklage Date: Sat, 30 May 2026 19:32:20 +0200 Subject: [PATCH] fix(ble): Cap GATT write chunks at 512 bytes to avoid crash on Android 13+ The default MTU-based splitter chunks writes at MTU - 3 bytes. When the system negotiates the maximum MTU of 517 (e.g. on a Pixel 8), chunks reach 514 bytes, which BluetoothGatt.writeCharacteristic rejects since Android 13 (max attribute length is 512), throwing IllegalArgumentException and crashing the app a few seconds after connecting to the watch. Use a custom DataSplitter capped at 512 bytes regardless of the negotiated MTU. --- .../sync/asteroid/AsteroidBleManager.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/asteroidos/sync/asteroid/AsteroidBleManager.java b/app/src/main/java/org/asteroidos/sync/asteroid/AsteroidBleManager.java index e05a6256..280a7938 100644 --- a/app/src/main/java/org/asteroidos/sync/asteroid/AsteroidBleManager.java +++ b/app/src/main/java/org/asteroidos/sync/asteroid/AsteroidBleManager.java @@ -40,8 +40,30 @@ import no.nordicsemi.android.ble.BleManager; import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataSplitter; public class AsteroidBleManager extends BleManager { + // Android's BluetoothGatt.writeCharacteristic() rejects any value longer + // than the maximum GATT attribute length (512 bytes) since Android 13, + // throwing IllegalArgumentException. The default MTU-based splitter chunks + // at MTU - 3 bytes, which is 514 when the system negotiates the maximum MTU + // of 517 (as on the Pixel 8), exceeding the limit and crashing the app on + // the first long notification. Cap each chunk at 512 bytes regardless of + // the negotiated MTU. The watch reassembles the message from the chunks, so + // a smaller chunk size has no effect other than splitting into more writes. + private static final int MAX_ATTRIBUTE_LENGTH = 512; + + private static final DataSplitter CAPPED_SPLITTER = (message, index, maxLength) -> { + final int size = Math.min(maxLength, MAX_ATTRIBUTE_LENGTH); + final int offset = index * size; + if (offset >= message.length) + return null; + final int length = Math.min(size, message.length - offset); + final byte[] chunk = new byte[length]; + System.arraycopy(message, offset, chunk, 0, length); + return chunk; + }; + public static final String TAG = AsteroidBleManager.class.toString(); @Nullable public BluetoothGattCharacteristic batteryCharacteristic; @@ -59,7 +81,7 @@ public AsteroidBleManager(@NonNull final Context context, SynchronizationService public final void send(UUID characteristic, byte[] data) { writeCharacteristic(sendingCharacteristics.get(characteristic), data, - Objects.requireNonNull(sendingCharacteristics.get(characteristic)).getWriteType()).split().enqueue(); + Objects.requireNonNull(sendingCharacteristics.get(characteristic)).getWriteType()).split(CAPPED_SPLITTER).enqueue(); } @NonNull