# Native BLE Fallback APK (single MainActivity, multi-variant broadcast)

When a PWA can't be made to work because the device's exact protocol variant
isn't known with certainty (e.g. LEDDMX-03 where 4 different 9-byte shapes
are documented but none proven for a given firmware), build a tiny native
APK that **broadcasts every known variant on every tap**. One button press
sends all 4 candidates in sequence with ~50ms gaps, the device takes the
one that matches, and the user sees the LED change immediately.

This is the working template used to build `com.igled.led` (5.6 MB, 1
activity, ~250 lines of Java). Build it from the server in ~30s with stock
Gradle, no Android Studio, install + launch + grant perms in one
`adb install && pm grant && am start` chain.

## When to use

- PWA built and deployed, user reports "no prende" after a few variant tries
- The published spec disagrees with itself (e.g. multiple GitHub sources
  show different `byte[1]` values for the same command)
- The device is on the user's LAN, they're watching it, you have ADB

## Project layout

```
led-controller/
├── local.properties         # sdk.dir=/opt/android-sdk
├── build.gradle             # top-level, AGP version
├── settings.gradle          # pluginManagement + repos
├── gradle.properties        # android.useAndroidX=true
└── app/
    ├── build.gradle         # namespace, compileSdk 34, minSdk 26
    └── src/main/
        ├── AndroidManifest.xml
        ├── res/layout/activity_main.xml
        ├── res/values/strings.xml
        └── java/com/igled/led/MainActivity.java
```

## Versions (proven compatible)

| Component | Version |
|---|---|
| Gradle | 8.5 (system, /usr/local/bin/gradle) |
| AGP | 8.2.2 (works with Gradle 8.5; AGP 8.5+ needs Gradle 8.7+) |
| compileSdk | 34 |
| minSdk | 26 (Android 8, covers 100% of active devices) |
| Java | 17 |

## Key MainActivity patterns

### 1. Permission model: request + grant from ADB

Don't rely on the runtime dialog. Pre-grant via adb so the user never has
to tap "Allow":

```bash
adb shell pm grant <pkg> android.permission.BLUETOOTH_CONNECT
adb shell pm grant <pkg> android.permission.BLUETOOTH_SCAN
adb shell pm grant <pkg> android.permission.ACCESS_FINE_LOCATION
adb shell pm grant <pkg> android.permission.ACCESS_COARSE_LOCATION
```

Manifest declares the modern per-permission types:
```xml
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```

### 2. Service UUID with fallback

The published UUIDs (FFF0/FFF2 for ELK, FFE0/FFE1 for LEDBLE) are good
first guesses. If service discovery returns null, **walk every service and
pick any characteristic with `PROPERTY_WRITE`**:

```java
BluetoothGattService svc = g.getService(UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"));
if (svc == null) {
    for (BluetoothGattService s : g.getServices()) {
        for (BluetoothGattCharacteristic c : s.getCharacteristics()) {
            if ((c.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) {
                writeCh = c;
            }
        }
    }
} else {
    writeCh = svc.getCharacteristic(UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"));
    if (writeCh == null) { /* same fallback loop */ }
}
```

### 3. Auto-scan filter for LED devices

`acceptAllDevices` is too noisy. Match common name prefixes:

```java
String n = dev.getName().toUpperCase();
if (n.contains("LED") || n.contains("DMX") || n.contains("LAMP")
 || n.contains("SP") || n.contains("RGB") || n.contains("ELK")
 || n.contains("HOM") || n.contains("BLE") || n.contains("SHOE")
 || n.contains("LB")) {
    connect(dev);
}
```

### 4. Multi-variant broadcast (the whole point)

Each user action (ON, color, brightness, effect) calls a single method
that fires 3-4 candidate packets back-to-back. BLE ACKs are slow (~30ms)
so 4 packets take ~120ms total — fast enough that the user sees one
"flash" and not four:

```java
private void cmdPower(boolean on) {
    byte[] v1 = new byte[]{(byte)0x7B,(byte)0x04,(byte)0x04,(byte)0x01,0,0,0,0,(byte)0xBF};
    byte[] v2 = new byte[]{(byte)0x7E,(byte)0x04,(byte)0x04,(byte)(on?0x01:0x00),0,0,0,0,(byte)0xEF};
    byte[] v3 = new byte[]{(byte)0x7E,(byte)0xFF,(byte)0x04,0x01,0,(byte)0xFF,(byte)0xFF,0,(byte)0xEF};
    byte[] v4 = new byte[]{(byte)0x7E,0,(byte)0x01,(byte)(on?0x01:0x00),0,0,0,0,(byte)0xEF};
    for (byte[] p : new byte[][]{v1, v2, v3, v4}) writeBytes(p);
}
```

**Pitfall:** if the inner method parameter is also a `byte[]` or has a
common name like `b`, Java will fail with "variable b is already defined"
in nested `new byte[]{...}` literals. Rename to `v1`, `v2`, etc.

### 5. On-screen hex log

The whole value of the APK is **the user can see exactly what bytes went
out**. Append every `writeBytes()` call to a monospace TextView:

```java
private void writeBytes(byte[] data) {
    StringBuilder sb = new StringBuilder("→ ");
    for (byte b : data) sb.append(String.format("%02X ", b));
    log(sb.toString().trim());
    writeCh.setValue(data);
    gatt.writeCharacteristic(writeCh);
}
```

This makes the APK self-debugging — the user doesn't need a separate
snoop log, the screen IS the log.

## Full one-shot build + deploy

```bash
# 1. Project already laid out under /home/ubuntu/<project>/
cd /home/ubuntu/led-controller

# 2. Build
ANDROID_HOME=/opt/android-sdk gradle assembleDebug
# → app/build/outputs/apk/debug/app-debug.apk

# 3. Install
adb -s <phone_ip:port> install -r app/build/outputs/apk/debug/app-debug.apk

# 4. Pre-grant (avoids permission dialog)
adb -s <phone_ip:port> shell pm grant com.igled.led \
    android.permission.BLUETOOTH_CONNECT
adb -s <phone_ip:port> shell pm grant com.igled.led \
    android.permission.BLUETOOTH_SCAN
adb -s <phone_ip:port> shell pm grant com.igled.led \
    android.permission.ACCESS_FINE_LOCATION
adb -s <phone_ip:port> shell pm grant com.igled.led \
    android.permission.ACCESS_COARSE_LOCATION

# 5. Launch
adb -s <phone_ip:port> shell am start -n com.igled.led/.MainActivity
```

The app appears on the user's phone ready to use. No permission prompts.

## Debugging a "still doesn't respond" APK

- **Service discovery returns no write char** → device uses a custom UUID
  not in our fallback list. Add `for (BluetoothGattService s : g.getServices())`
  with logging, capture the names, update the UUID candidates.
- **`writeCharacteristic` returns false** → characteristic doesn't have
  `WRITE_NO_RESPONSE` set. Try `gatt.beginReliableWrite()` or fall back
  to a `WRITE` (request-response) write.
- **Writes "succeed" but LED doesn't change** → wrong protocol variant.
  Add more candidates to the multi-variant broadcast. The right one will
  hit eventually.
- **`onCharacteristicWrite` returns status != GATT_SUCCESS** → MTU too
  small (default 20 bytes, our packets are 9 — fine) or device dropped
  the connection. Reconnect.

## When to give up and try something else

If 4+ variants all fail to even produce a write-failure (no LED change,
no error), the protocol is likely encrypted or uses a vendor-specific
write-without-response flag the BLE stack is rejecting silently. At that
point:

1. Decompile the original APK (see `references/leddmx-protocols.md` —
   "Reverse Engineering via APK Decompile") to get the actual `int[]`
   bytes the original app sends
2. OR connect the device via a known-good HA integration
   (e.g. `elkbledom` for ELK-BLEDOM family) to verify the device
   responds at all
3. OR have the user re-pair the device to the original app and confirm
   the LED still works there — if it doesn't, the strip is the problem,
   not the protocol
