package com.wytehat.btlogger; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class TrackerDatabase extends SQLiteOpenHelper { private static final String DB_NAME = "bluetooth_tracker.db"; private static final int DB_VERSION = 17; public TrackerDatabase(Context context) { super(context, DB_NAME, null, DB_VERSION); } public synchronized List history(String address, long historyFrom, long historyTo) { ArrayList result = new ArrayList(); if (address == null) return result; Cursor cursor = getReadableDatabase().query("location_history", null, "address=? AND logged_at>=? AND logged_at<=?", new String[] { address, String.valueOf(historyFrom), String.valueOf(historyTo) }, null, null, "logged_at ASC"); try { while (cursor.moveToNext()) { LocationPoint point = new LocationPoint(); point.latitude = cursor.getDouble(cursor.getColumnIndex("latitude")); point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude")); point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy")); point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at")); point.event = cursor.getString(cursor.getColumnIndex("event")); result.add(point); } } finally { cursor.close(); } return result; } public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE devices (address TEXT PRIMARY KEY,name TEXT," + "connected INTEGER NOT NULL DEFAULT 0,connected_at INTEGER DEFAULT 0," + "disconnected_at INTEGER DEFAULT 0,latitude REAL,longitude REAL,accuracy REAL," + "has_location INTEGER DEFAULT 0,battery INTEGER DEFAULT -1,rssi INTEGER DEFAULT 0," + "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,paired INTEGER DEFAULT 0)"); createSupportingTables(db); seedVendors(db); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { addColumn(db, "ALTER TABLE devices ADD COLUMN photo_uri TEXT"); addColumn(db, "ALTER TABLE devices ADD COLUMN icon_preset TEXT DEFAULT 'bag'"); addColumn(db, "ALTER TABLE devices ADD COLUMN gps_polling_rate INTEGER DEFAULT 900000"); } if (oldVersion < 3) { addColumn(db, "ALTER TABLE devices ADD COLUMN pin_color TEXT DEFAULT '#E53935'"); addColumn(db, "ALTER TABLE devices ADD COLUMN tracking_priority INTEGER DEFAULT 15"); addColumn(db, "ALTER TABLE devices ADD COLUMN photo_path TEXT"); addColumn(db, "ALTER TABLE devices ADD COLUMN device_gps_interval INTEGER DEFAULT 15"); addColumn(db, "ALTER TABLE devices ADD COLUMN is_tracked INTEGER DEFAULT 0"); addColumn(db, "ALTER TABLE devices ADD COLUMN custom_name TEXT"); addColumn(db, "ALTER TABLE devices ADD COLUMN broadcast_name TEXT"); addColumn(db, "ALTER TABLE devices ADD COLUMN vendor_name TEXT"); db.execSQL("UPDATE devices SET broadcast_name=name,photo_path=photo_uri," + "device_gps_interval=CASE WHEN gps_polling_rate=0 THEN 0 ELSE gps_polling_rate/60000 END," + "tracking_priority=CASE WHEN gps_polling_rate=0 THEN 0 ELSE gps_polling_rate/60000 END," + "is_tracked=CASE WHEN connected=1 OR connected_at>0 OR photo_uri IS NOT NULL THEN 1 ELSE 0 END"); createSupportingTables(db); seedVendors(db); } if (oldVersion < 4) { addColumn(db, "ALTER TABLE devices ADD COLUMN tx_power_1m INTEGER DEFAULT -59"); addColumn(db, "ALTER TABLE devices ADD COLUMN path_loss_exponent REAL DEFAULT 2.7"); createSupportingTables(db); } if (oldVersion < 5) { addColumn(db, "ALTER TABLE devices ADD COLUMN last_rssi_at INTEGER DEFAULT 0"); } if (oldVersion < 6) addColumn(db, "ALTER TABLE devices ADD COLUMN device_category TEXT"); if (oldVersion < 7) addColumn(db, "ALTER TABLE devices ADD COLUMN category_manual INTEGER DEFAULT 0"); if (oldVersion < 8) addColumn(db, "ALTER TABLE devices ADD COLUMN vendor_manual INTEGER DEFAULT 0"); if (oldVersion < 9) db.execSQL("UPDATE devices SET updated_at=0 WHERE last_rssi_at=0 AND rssi=0 AND connected_at=0 AND disconnected_at=0 AND has_location=0 AND battery<0"); if (oldVersion < 10) { addColumn(db, "ALTER TABLE devices ADD COLUMN map_enabled INTEGER DEFAULT 0"); addColumn(db, "ALTER TABLE devices ADD COLUMN live_enabled INTEGER DEFAULT 0"); db.execSQL("UPDATE devices SET map_enabled=1 WHERE has_location=1 AND is_tracked=1"); } if (oldVersion < 11) addColumn(db, "ALTER TABLE devices ADD COLUMN location_locked INTEGER DEFAULT 0"); if (oldVersion < 12) { addColumn(db, "ALTER TABLE devices ADD COLUMN show_trail INTEGER DEFAULT 1"); db.execSQL("UPDATE devices SET show_trail=0 WHERE location_locked=1"); } if (oldVersion < 13) addColumn(db, "ALTER TABLE devices ADD COLUMN track_broadcast INTEGER DEFAULT 0"); if (oldVersion < 14) createSupportingTables(db); 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) { try { db.execSQL(sql); } catch (Exception ignored) { } } private void createSupportingTables(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS location_history (id INTEGER PRIMARY KEY AUTOINCREMENT," + "address TEXT,latitude REAL,longitude REAL,accuracy REAL,logged_at INTEGER,event TEXT)"); db.execSQL("CREATE INDEX IF NOT EXISTS history_address_time ON location_history(address,logged_at)"); db.execSQL("CREATE TABLE IF NOT EXISTS range_samples (id INTEGER PRIMARY KEY AUTOINCREMENT," + "address TEXT,session_id TEXT,sampled_at INTEGER,latitude REAL,longitude REAL," + "accuracy REAL,raw_rssi INTEGER,filtered_rssi REAL,heading REAL,range_state TEXT)"); db.execSQL("CREATE INDEX IF NOT EXISTS range_sample_session ON range_samples(session_id,sampled_at)"); db.execSQL("CREATE TABLE IF NOT EXISTS vendor_lookup (lookup_type TEXT,hex_code TEXT," + "vendor_name TEXT,PRIMARY KEY(lookup_type,hex_code))"); db.execSQL("CREATE TABLE IF NOT EXISTS device_observations (id INTEGER PRIMARY KEY AUTOINCREMENT," + "address TEXT,first_seen_at INTEGER,last_seen_at INTEGER,rssi INTEGER,strongest_rssi INTEGER," + "sighting_count INTEGER DEFAULT 1,broadcast_name TEXT,vendor_name TEXT,device_category TEXT," + "broadcast_type TEXT,appearance INTEGER DEFAULT -1,company_id INTEGER DEFAULT -1," + "advertisement_hex TEXT,latitude REAL,longitude REAL,accuracy REAL,has_location INTEGER DEFAULT 0," + "uploaded INTEGER DEFAULT 0)"); db.execSQL("CREATE INDEX IF NOT EXISTS observation_address_time ON device_observations(address,last_seen_at)"); db.execSQL("CREATE INDEX IF NOT EXISTS observation_upload ON device_observations(uploaded,last_seen_at)"); } private void seedVendors(SQLiteDatabase db) { addVendor(db, "OUI", "A4C3F0", "Intel Corporate"); addVendor(db, "OUI", "0000F0", "Samsung Electronics"); addVendor(db, "OUI", "0452C7", "Bose Corporation"); addVendor(db, "OUI", "C4E510", "Tile, Inc."); addVendor(db, "OUI", "001A80", "Sony Corporation"); addVendor(db, "OUI", "5882A8", "Xiaomi Communications"); addVendor(db, "OUI", "F4F5D8", "Google LLC"); addVendor(db, "COMPANY", "0006", "Microsoft Corporation"); addVendor(db, "COMPANY", "0059", "Nordic Semiconductor ASA"); addVendor(db, "COMPANY", "0075", "Samsung Electronics"); addVendor(db, "COMPANY", "009E", "Bose Corporation"); addVendor(db, "COMPANY", "00E0", "Google LLC"); addVendor(db, "COMPANY", "018E", "Fitbit, Inc."); addVendor(db, "COMPANY", "067C", "Tile, Inc."); addVendor(db, "COMPANY", "0171", "Amazon.com Services LLC"); addVendor(db, "COMPANY", "00D0", "Dexcom, Inc."); addVendor(db, "COMPANY", "0131", "Cypress Semiconductor"); } private void addVendor(SQLiteDatabase db, String type, String code, String vendor) { ContentValues values = new ContentValues(); values.put("lookup_type", type); values.put("hex_code", code); values.put("vendor_name", vendor); db.insertWithOnConflict("vendor_lookup", null, values, SQLiteDatabase.CONFLICT_REPLACE); } public synchronized void connected(String address, String broadcast, long time, Fix fix) { DeviceRecord old = get(address); ContentValues values = base(address, broadcast, time, fix); values.put("connected", 1); values.put("connected_at", time); values.put("disconnected_at", 0); values.put("reason", "Connected"); values.put("is_tracked", 1); if (old == null) values.put("pin_color", colorFor(address)); if (old != null) preserveProfile(values, old, fix); getWritableDatabase().insertWithOnConflict("devices", null, values, SQLiteDatabase.CONFLICT_REPLACE); if (fix != null) insertHistory(address, fix, time, LocationPoint.EVENT_CONNECTED); } public synchronized void disconnected(String address, String broadcast, long time, Fix fix, String reason) { ContentValues values = base(address, broadcast, time, fix); values.put("connected", 0); values.put("disconnected_at", time); values.put("reason", reason); values.put("is_tracked", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); if (fix != null) insertHistory(address, fix, time, LocationPoint.EVENT_DISCONNECTED); } public synchronized void telemetry(String address, String broadcast, Integer battery, Integer rssi, long time, Fix fix) { DeviceRecord old = get(address); ContentValues values = base(address, broadcast, time, fix); if (battery != null) values.put("battery", battery.intValue()); if (rssi != null) { values.put("rssi", rssi.intValue()); values.put("last_rssi_at", time); } if (old == null) { values.put("connected", 0); getWritableDatabase().insert("devices", null, values); } else { getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } } public synchronized void recordObservation(String address, long time, int rssi, String broadcastName, String vendorName, String category, String broadcastType, int appearance, int companyId, String advertisementHex, Fix fix) { if (address == null || address.length() == 0 || rssi == 0 || rssi == 127) return; SQLiteDatabase db = getWritableDatabase(); Cursor cursor = db.query("device_observations", new String[] { "id", "last_seen_at", "strongest_rssi", "sighting_count" }, "address=?", new String[] { address }, null, null, "last_seen_at DESC", "1"); long id = -1L; long last = 0L; int strongest = rssi; int count = 0; try { if (cursor.moveToFirst()) { id = cursor.getLong(0); last = cursor.getLong(1); strongest = Math.max(rssi, cursor.getInt(2)); count = cursor.getInt(3); } } finally { cursor.close(); } ContentValues values = new ContentValues(); values.put("address", address); values.put("last_seen_at", time); values.put("rssi", rssi); values.put("strongest_rssi", strongest); if (broadcastName != null) values.put("broadcast_name", broadcastName); if (vendorName != null) values.put("vendor_name", vendorName); if (category != null) values.put("device_category", category); if (broadcastType != null) values.put("broadcast_type", broadcastType); values.put("appearance", appearance); values.put("company_id", companyId); if (advertisementHex != null) values.put("advertisement_hex", advertisementHex); if (fix != null) { values.put("latitude", fix.latitude); values.put("longitude", fix.longitude); values.put("accuracy", fix.accuracy); values.put("has_location", 1); } if (id >= 0L && time - last <= 15000L) { values.put("sighting_count", count + 1); db.update("device_observations", values, "id=?", new String[] { String.valueOf(id) }); } else { values.put("first_seen_at", time); values.put("sighting_count", 1); values.put("uploaded", 0); db.insert("device_observations", null, values); } } public synchronized void updateLocation(String address, Fix fix, long time) { if (fix == null) return; ContentValues values = new ContentValues(); values.put("latitude", fix.latitude); values.put("longitude", fix.longitude); values.put("accuracy", fix.accuracy); values.put("has_location", 1); values.put("updated_at", time); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); insertHistory(address, fix, time, null); } /** * @param event LocationPoint.EVENT_CONNECTED or EVENT_DISCONNECTED for a * link state change, null for a plain position refresh. */ private void insertHistory(String address, Fix fix, long time, String event) { ContentValues values = new ContentValues(); values.put("address", address); values.put("latitude", fix.latitude); values.put("longitude", fix.longitude); values.put("accuracy", fix.accuracy); values.put("logged_at", time); values.put("event", event); getWritableDatabase().insert("location_history", null, values); } public synchronized void updateProfile(String address, String title, String photo, String icon) { ContentValues values = new ContentValues(); if (title != null && title.trim().length() > 0) values.put("custom_name", title.trim()); values.put("photo_uri", photo); values.put("photo_path", photo); values.put("icon_preset", icon); values.put("is_tracked", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void updateCalibration(String address, int txPower, double exponent) { ContentValues values = new ContentValues(); values.put("tx_power_1m", txPower); values.put("path_loss_exponent", exponent); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void logRangeSample(String address, String sessionId, long time, double latitude, double longitude, float accuracy, int rawRssi, double filteredRssi, float heading, String state) { ContentValues values = new ContentValues(); values.put("address", address); values.put("session_id", sessionId); values.put("sampled_at", time); values.put("latitude", latitude); values.put("longitude", longitude); values.put("accuracy", accuracy); values.put("raw_rssi", rawRssi); values.put("filtered_rssi", filteredRssi); values.put("heading", heading); values.put("range_state", state); getWritableDatabase().insert("range_samples", null, values); } public synchronized void updateTracking(String address, int minutes, String color) { ContentValues values = new ContentValues(); values.put("tracking_priority", minutes); values.put("device_gps_interval", minutes); values.put("gps_polling_rate", minutes == 0 ? 0 : minutes * 60000); values.put("pin_color", color); values.put("is_tracked", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void markTracked(String address, String broadcast) { DeviceRecord old = get(address); if (old == null) { ContentValues created = base(address, broadcast, 0L, null); created.put("connected", 0); created.put("is_tracked", 1); created.put("pin_color", colorFor(address)); getWritableDatabase().insert("devices", null, created); return; } ContentValues values = new ContentValues(); values.put("is_tracked", 1); if (old.tracked == 0) values.put("pin_color", colorFor(address)); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void removeFromTracking(String address) { ContentValues values = new ContentValues(); values.put("is_tracked", 0); values.put("map_enabled", 0); values.put("live_enabled", 0); values.put("track_broadcast", 0); 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 }); } public synchronized void setLiveEnabled(String address, boolean enabled) { ContentValues values = new ContentValues(); values.put("live_enabled", enabled ? 1 : 0); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setTrackBroadcast(String address, boolean enabled) { ContentValues values = new ContentValues(); values.put("track_broadcast", enabled ? 1 : 0); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setShowTrail(String address, boolean shown) { ContentValues values = new ContentValues(); values.put("show_trail", shown ? 1 : 0); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setLocationLocked(String address, boolean locked) { ContentValues values = new ContentValues(); values.put("location_locked", locked ? 1 : 0); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setCustomName(String address, String name) { ContentValues values = new ContentValues(); values.put("custom_name", name == null ? null : name.trim()); values.put("is_tracked", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setDetectedCategory(String address, String category) { DeviceRecord record = get(address); if (record != null && record.categoryManual == 1) return; ContentValues values = new ContentValues(); values.put("device_category", category); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setCategory(String address, String category) { if (category == null) return; ContentValues values = new ContentValues(); values.put("device_category", category); values.put("category_manual", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setVendor(String address, String vendor) { DeviceRecord old = get(address); if (old != null && old.vendorManual == 1) return; if (vendor == null) return; ContentValues values = new ContentValues(); values.put("vendor_name", vendor); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized void setManualVendor(String address, String vendor) { ContentValues values = new ContentValues(); values.put("vendor_name", vendor == null ? null : vendor.trim()); values.put("vendor_manual", 1); getWritableDatabase().update("devices", values, "address=?", new String[] { address }); } public synchronized String lookupOui(String address) { if (address == null) return null; String code = address.replace(":", "").replace("-", "").toUpperCase(Locale.US); if (code.length() < 6) return null; return lookupVendor("OUI", code.substring(0, 6)); } public synchronized String lookupCompany(int companyId) { return lookupVendor("COMPANY", String.format(Locale.US, "%04X", companyId & 0xffff)); } private String lookupVendor(String type, String code) { Cursor cursor = getReadableDatabase().query("vendor_lookup", new String[] { "vendor_name" }, "lookup_type=? AND hex_code=?", new String[] { type, code }, null, null, null); try { return cursor.moveToFirst() ? cursor.getString(0) : null; } finally { cursor.close(); } } public synchronized DeviceRecord get(String address) { Cursor cursor = getReadableDatabase().query("devices", null, "address=?", new String[] { address }, null, null, null); try { return cursor.moveToFirst() ? read(cursor) : null; } finally { cursor.close(); } } public synchronized List all() { return queryDevices(null, null); } public synchronized List tracked() { return queryDevices("is_tracked=1", null); } private List queryDevices(String where, String[] args) { ArrayList result = new ArrayList(); Cursor cursor = getReadableDatabase().query("devices", null, where, args, null, null, "updated_at DESC"); try { while (cursor.moveToNext()) result.add(read(cursor)); } finally { cursor.close(); } return result; } public synchronized List history(String address, long since) { ArrayList result = new ArrayList(); Cursor cursor = getReadableDatabase().query("location_history", null, "address=? AND logged_at>=?", new String[] { address, String.valueOf(since) }, null, null, "logged_at ASC"); try { while (cursor.moveToNext()) { LocationPoint point = new LocationPoint(); point.latitude = cursor.getDouble(cursor.getColumnIndex("latitude")); point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude")); point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy")); point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at")); point.event = cursor.getString(cursor.getColumnIndex("event")); result.add(point); } } finally { cursor.close(); } return result; } private ContentValues base(String address, String broadcast, long time, Fix fix) { ContentValues values = new ContentValues(); values.put("address", address); if (meaningfulBroadcast(broadcast)) { values.put("name", broadcast.trim()); values.put("broadcast_name", broadcast.trim()); } values.put("updated_at", time); if (fix != null) { values.put("latitude", fix.latitude); values.put("longitude", fix.longitude); values.put("accuracy", fix.accuracy); values.put("has_location", 1); } return values; } private boolean meaningfulBroadcast(String value) { return value != null && value.trim().length() > 0 && !"Unknown Device".equalsIgnoreCase(value.trim()) && !"Unknown item".equalsIgnoreCase(value.trim()); } private void preserveProfile(ContentValues values, DeviceRecord old, Fix fix) { values.put("battery", old.battery); values.put("rssi", old.rssi); values.put("last_rssi_at", old.lastRssiAt); values.put("photo_uri", old.photoUri); values.put("photo_path", old.photoUri); values.put("icon_preset", old.iconPreset); values.put("pin_color", old.pinColor); values.put("tracking_priority", old.trackingPriority); values.put("device_gps_interval", old.trackingPriority); values.put("gps_polling_rate", old.gpsPollingRate); values.put("track_broadcast", old.trackBroadcast); values.put("custom_name", old.customName); values.put("vendor_name", old.vendorName); values.put("tx_power_1m", old.txPowerAtOneMeter); values.put("path_loss_exponent", old.pathLossExponent); if (fix == null && old.hasLocation) { values.put("latitude", old.latitude); values.put("longitude", old.longitude); values.put("accuracy", old.accuracy); values.put("has_location", 1); } } private DeviceRecord read(Cursor c) { DeviceRecord r = new DeviceRecord(); r.address = string(c, "address"); r.name = string(c, "name"); r.customName = string(c, "custom_name"); r.broadcastName = string(c, "broadcast_name"); r.vendorName = string(c, "vendor_name"); r.category = string(c, "device_category"); if (r.category == null) r.category = "Other"; r.categoryManual = integer(c, "category_manual"); r.vendorManual = integer(c, "vendor_manual"); r.mapEnabled = integer(c, "map_enabled"); r.liveEnabled = integer(c, "live_enabled"); 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"); r.latitude = decimal(c, "latitude"); r.longitude = decimal(c, "longitude"); r.accuracy = (float) decimal(c, "accuracy"); r.hasLocation = integer(c, "has_location") == 1; r.battery = integer(c, "battery"); r.rssi = integer(c, "rssi"); r.lastRssiAt = number(c, "last_rssi_at"); r.reason = string(c, "reason"); r.updatedAt = number(c, "updated_at"); r.photoUri = string(c, "photo_path"); if (r.photoUri == null) r.photoUri = string(c, "photo_uri"); r.iconPreset = string(c, "icon_preset"); r.pinColor = string(c, "pin_color"); if (r.pinColor == null) r.pinColor = "#E53935"; r.trackingPriority = integer(c, "device_gps_interval"); r.gpsPollingRate = r.trackingPriority == 0 ? 0 : r.trackingPriority * 60000; r.tracked = integer(c, "is_tracked"); r.txPowerAtOneMeter = integer(c, "tx_power_1m"); if (r.txPowerAtOneMeter == 0) r.txPowerAtOneMeter = -59; r.pathLossExponent = decimal(c, "path_loss_exponent"); if (r.pathLossExponent <= 0) r.pathLossExponent = 2.7; return r; } private int column(Cursor c, String name) { return c.getColumnIndex(name); } private String string(Cursor c, String name) { int index = column(c, name); return index < 0 || c.isNull(index) ? null : c.getString(index); } private int integer(Cursor c, String name) { int index = column(c, name); return index < 0 || c.isNull(index) ? 0 : c.getInt(index); } private long number(Cursor c, String name) { int index = column(c, name); return index < 0 || c.isNull(index) ? 0 : c.getLong(index); } private double decimal(Cursor c, String name) { int index = column(c, name); return index < 0 || c.isNull(index) ? 0 : c.getDouble(index); } private String colorFor(String address) { String[] colors = { "#E53935", "#1E88E5", "#43A047", "#FB8C00", "#8E24AA", "#00897B" }; int hash = address == null ? 0 : address.hashCode(); if (hash == Integer.MIN_VALUE) hash = 0; return colors[Math.abs(hash) % colors.length]; } private String safe(String value) { return value == null || value.length() == 0 ? "Unknown item" : value; } public static class Fix { public double latitude; public double longitude; public float accuracy; public Fix(double lat, double lon, float acc) { latitude = lat; longitude = lon; accuracy = acc; } } }