From 60feffc256db882db68c270e02244377800f9e32 Mon Sep 17 00:00:00 2001 From: n0tst3v3 Date: Wed, 19 Aug 2026 18:03:11 -0600 Subject: [PATCH] feat(track): record which devices are paired, and report it on upload The app knew about bonded devices only in passing - syncPairedDevices marked them tracked and threw the bond state away - so nothing downstream could tell a paired device from one merely seen. Adds devices.paired (schema 17), set from ACTION_BOND_STATE_CHANGED and from the bonded list at startup, and sends it as a `paired` column on every uploaded row. The flag is its own column rather than inferred from is_tracked, which answers a different question: a device can be tracked without ever having been paired. syncPairedDevices clears the flag before re-asserting it from getBondedDevices. A device unpaired while the service was dead never broadcast BOND_NONE, so the bonded list is the only reliable answer at startup, and without the clear such a device would stay marked paired forever. Co-Authored-By: Claude Opus 5 --- .../btlogger/BluetoothTrackingService.java | 13 ++++++++-- .../com/wytehat/btlogger/DeviceRecord.java | 3 +++ .../com/wytehat/btlogger/ServerUploader.java | 13 ++++++---- .../com/wytehat/btlogger/TrackerDatabase.java | 25 +++++++++++++++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/wytehat/btlogger/BluetoothTrackingService.java b/app/src/main/java/com/wytehat/btlogger/BluetoothTrackingService.java index f29007b..b7e95d1 100644 --- a/app/src/main/java/com/wytehat/btlogger/BluetoothTrackingService.java +++ b/app/src/main/java/com/wytehat/btlogger/BluetoothTrackingService.java @@ -117,11 +117,14 @@ public class BluetoothTrackingService extends Service implements LocationListene } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); - if (state == BluetoothDevice.BOND_BONDED) + if (state == BluetoothDevice.BOND_BONDED) { db.markTracked(address, broadcastName); - else if (state == BluetoothDevice.BOND_NONE) + db.setPaired(address, true); + } else if (state == BluetoothDevice.BOND_NONE) { + db.setPaired(address, false); db.disconnected(address, broadcastName, now, fix, "Manual / Intentional Disconnect"); + } } resolveVendor(address, -1); notifyChanged(); @@ -370,10 +373,16 @@ public class BluetoothTrackingService extends Service implements LocationListene private void syncPairedDevices() { if (bluetooth == null) return; try { + // The bonded list is the whole truth, so the flag is cleared first: + // a device unpaired while the service was dead sent no BOND_NONE + // broadcast, and would otherwise stay marked as paired forever. + db.clearPaired(); + for (BluetoothDevice device : bluetooth.getBondedDevices()) { String value = address(device); if (value != null) { db.markTracked(value, name(device)); + db.setPaired(value, true); resolveVendor(value, -1); } } diff --git a/app/src/main/java/com/wytehat/btlogger/DeviceRecord.java b/app/src/main/java/com/wytehat/btlogger/DeviceRecord.java index bd5b459..ad5901e 100644 --- a/app/src/main/java/com/wytehat/btlogger/DeviceRecord.java +++ b/app/src/main/java/com/wytehat/btlogger/DeviceRecord.java @@ -14,6 +14,9 @@ public class DeviceRecord { public int locationLocked; public int showTrail = 1; public int trackBroadcast; + + /** 1 when this device is bonded to the phone. */ + public int paired; public String reason; public String photoUri; public String iconPreset; diff --git a/app/src/main/java/com/wytehat/btlogger/ServerUploader.java b/app/src/main/java/com/wytehat/btlogger/ServerUploader.java index 90341ba..a817264 100644 --- a/app/src/main/java/com/wytehat/btlogger/ServerUploader.java +++ b/app/src/main/java/com/wytehat/btlogger/ServerUploader.java @@ -51,7 +51,7 @@ public final class ServerUploader { /** Columns the server's importer recognises without any aliasing. */ private static final String HEADER = - "mac_address,device_name,device_class,rssi,latitude,longitude,timestamp"; + "mac_address,device_name,device_class,rssi,latitude,longitude,timestamp,paired"; private static final int CONNECT_TIMEOUT_MS = 20000; private static final int READ_TIMEOUT_MS = 60000; @@ -177,7 +177,8 @@ public final class ServerUploader { record.latitude, record.longitude, record.updatedAt > 0 ? record.updatedAt - : System.currentTimeMillis()); + : System.currentTimeMillis(), + record.paired == 1); } List history = database.history(record.address, 0L); @@ -198,7 +199,8 @@ public final class ServerUploader { // empty rather than repeating the device's latest reading, // which was measured somewhere else entirely. appendRow(csv, stamp, record.address, name, category, "", - point.latitude, point.longitude, time); + point.latitude, point.longitude, time, + record.paired == 1); } } @@ -208,7 +210,7 @@ public final class ServerUploader { private static void appendRow(StringBuilder csv, SimpleDateFormat stamp, String mac, String name, String category, String rssi, double latitude, - double longitude, long time) { + double longitude, long time, boolean paired) { csv.append(field(mac)).append(',') .append(field(name)).append(',') @@ -216,7 +218,8 @@ public final class ServerUploader { .append(rssi).append(',') .append(latitude).append(',') .append(longitude).append(',') - .append(stamp.format(new Date(time))) + .append(stamp.format(new Date(time))).append(',') + .append(paired ? "1" : "0") .append('\n'); } diff --git a/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java b/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java index 1828e3b..78e2463 100644 --- a/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java +++ b/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java @@ -11,7 +11,7 @@ import java.util.Locale; public class TrackerDatabase extends SQLiteOpenHelper { private static final String DB_NAME = "bluetooth_tracker.db"; - private static final int DB_VERSION = 16; + private static final int DB_VERSION = 17; public TrackerDatabase(Context context) { super(context, DB_NAME, null, DB_VERSION); @@ -47,7 +47,7 @@ public class TrackerDatabase extends SQLiteOpenHelper { "reason TEXT,updated_at INTEGER DEFAULT 0,photo_uri TEXT,icon_preset TEXT DEFAULT 'bag'," + "gps_polling_rate INTEGER DEFAULT 900000,pin_color TEXT DEFAULT '#E53935'," + "tracking_priority INTEGER DEFAULT 15,photo_path TEXT,device_gps_interval INTEGER DEFAULT 15," + - "is_tracked INTEGER DEFAULT 0,custom_name TEXT,broadcast_name TEXT,vendor_name TEXT,tx_power_1m INTEGER DEFAULT -59,path_loss_exponent REAL DEFAULT 2.7,last_rssi_at INTEGER DEFAULT 0,device_category TEXT,category_manual INTEGER DEFAULT 0,vendor_manual INTEGER DEFAULT 0,map_enabled INTEGER DEFAULT 0,live_enabled INTEGER DEFAULT 0,location_locked INTEGER DEFAULT 0,show_trail INTEGER DEFAULT 1,track_broadcast INTEGER DEFAULT 0)"); + "is_tracked INTEGER DEFAULT 0,custom_name TEXT,broadcast_name TEXT,vendor_name TEXT,tx_power_1m INTEGER DEFAULT -59,path_loss_exponent REAL DEFAULT 2.7,last_rssi_at INTEGER DEFAULT 0,device_category TEXT,category_manual INTEGER DEFAULT 0,vendor_manual INTEGER DEFAULT 0,map_enabled INTEGER DEFAULT 0,live_enabled INTEGER DEFAULT 0,location_locked INTEGER DEFAULT 0,show_trail INTEGER DEFAULT 1,track_broadcast INTEGER DEFAULT 0,paired INTEGER DEFAULT 0)"); createSupportingTables(db); seedVendors(db); } @@ -103,6 +103,8 @@ public class TrackerDatabase extends SQLiteOpenHelper { if (oldVersion < 15) seedVendors(db); if (oldVersion < 16) addColumn(db, "ALTER TABLE location_history ADD COLUMN event TEXT"); + if (oldVersion < 17) + addColumn(db, "ALTER TABLE devices ADD COLUMN paired INTEGER DEFAULT 0"); } private void addColumn(SQLiteDatabase db, String sql) { @@ -331,6 +333,24 @@ public class TrackerDatabase extends SQLiteOpenHelper { getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } + /** + * Records whether this device is bonded to the phone. + * + * Kept as its own column rather than inferred from is_tracked: a device can + * be tracked without ever having been paired, and the two answer different + * questions. + */ + /** Clears the flag on every device, before the bonded list re-asserts it. */ + public synchronized void clearPaired() { + ContentValues values = new ContentValues(); values.put("paired", 0); + getWritableDatabase().update("devices", values, null, null); + } + + public synchronized void setPaired(String address, boolean paired) { + ContentValues values = new ContentValues(); values.put("paired", paired ? 1 : 0); + getWritableDatabase().update("devices", values, "address=?", new String[] { address }); + } + public synchronized void setMapEnabled(String address, boolean enabled) { ContentValues values = new ContentValues(); values.put("map_enabled", enabled ? 1 : 0); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); @@ -517,6 +537,7 @@ public class TrackerDatabase extends SQLiteOpenHelper { r.locationLocked = integer(c, "location_locked"); r.showTrail = integer(c, "show_trail"); r.trackBroadcast = integer(c, "track_broadcast"); + r.paired = integer(c, "paired"); r.connected = integer(c, "connected"); r.connectedAt = number(c, "connected_at"); r.disconnectedAt = number(c, "disconnected_at");