perf(scanner): stop redoing everything on every advertisement, plus map point events

Yes, the scanner was duplicating. refreshCategoryOptions(),
updateRadar() and pairedAdapter.notifyDataSetChanged() ran from
acceptSignal on every advertisement AND from pruneTask once a second
- the same three calls either way.

Worse, the whole persist path ran per advertisement, and ScanCallback
is delivered on the main looper: a hex dump of the advertisement,
lookupCompany(), get(), lookupOui(), then telemetry(),
setDetectedCategory(), recordObservation() and setVendor(). Eight
database operations on the UI thread per beacon, at
SCAN_MODE_LOW_LATENCY, times every transmitter in range. The radar
sweep re-posts itself every 33ms and was competing for that thread,
which is why it dragged.

A device already on the radar now only gets its RSSI and last-seen
stamp refreshed until PERSIST_INTERVAL_MS (3s) is up. A device's
first sighting still takes the full path and refreshes at once.

The radar view also allocated per frame at 30fps: a six-stop
RadialGradient plus two arrays, and a fresh ArrayList and Comparator
for the target sort. Both reused now. setTargets no longer
invalidates while scanning, since onDraw already re-posts itself.
Left setLayerType(SOFTWARE) alone - setShadowLayer only works on text
under hardware acceleration, so removing it would drop the ring glow.

Separately, history points now say why they exist. location_history
had no event column, so schema 16 adds one; connected() and
disconnected() stamp it and a plain position refresh leaves it null.
The popup shows a linked or broken-link line for the two states and
stays quiet for older rows that predate the column.

History points also render as the device's own marker at half size
rather than a plain circle, sharing one divIcon per device so a long
trail does not build a DOM node per point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
n0tst3v3
2026-08-19 10:27:15 -06:00
co-authored by Claude Opus 5
parent 0f01b836c6
commit f16efce66e
5 changed files with 170 additions and 50 deletions
@@ -39,6 +39,18 @@ public class BluetoothRadarView extends View {
private float zoom = 1.0f; private float zoom = 1.0f;
private boolean suppressTap; private boolean suppressTap;
private double sweepAngle = -90.0; private double sweepAngle = -90.0;
/* Draw-time scratch, reused every frame -- see onDraw. */
private android.graphics.RadialGradient glow;
private float glowCx = -1f, glowCy = -1f, glowR = -1f;
private final ArrayList<Target> ordered = new ArrayList<Target>();
private final Comparator<Target> byAngle = new Comparator<Target>() {
public int compare(Target first, Target second) {
float a = angleFor(first.address);
float b = angleFor(second.address);
return a < b ? -1 : (a > b ? 1 : 0);
}
};
public BluetoothRadarView(Context context) { public BluetoothRadarView(Context context) {
super(context); super(context);
scaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { scaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@@ -68,7 +80,10 @@ public class BluetoothRadarView extends View {
if (!targetAngles.containsKey(target.address)) if (!targetAngles.containsKey(target.address))
targetAngles.put(target.address, Float.valueOf(findOpenAngle(target.address))); targetAngles.put(target.address, Float.valueOf(findOpenAngle(target.address)));
} }
targets.clear(); targets.addAll(values); invalidate(); targets.clear(); targets.addAll(values);
// While scanning, onDraw already re-posts itself every 33ms; an
// extra invalidate per update just forces redundant frames.
if (!scanning) invalidate();
} }
protected synchronized void onDraw(Canvas canvas) { protected synchronized void onDraw(Canvas canvas) {
@@ -81,9 +96,15 @@ public class BluetoothRadarView extends View {
float glowRadius = radius + 34f; float glowRadius = radius + 34f;
float edgeStop = radius / glowRadius; float edgeStop = radius / glowRadius;
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
paint.setShader(new android.graphics.RadialGradient(cx, cy, glowRadius, // Rebuilt only when the geometry moves. This used to allocate a
// six-stop gradient and two arrays on every frame of a 30fps sweep.
if (glow == null || glowCx != cx || glowCy != cy || glowR != glowRadius) {
glow = new android.graphics.RadialGradient(cx, cy, glowRadius,
new int[] { Color.TRANSPARENT, Color.argb(5, 35, 180, 110), Color.argb(24, 45, 225, 140), Color.argb(48, 55, 235, 150), Color.argb(18, 35, 190, 120), Color.TRANSPARENT }, new int[] { Color.TRANSPARENT, Color.argb(5, 35, 180, 110), Color.argb(24, 45, 225, 140), Color.argb(48, 55, 235, 150), Color.argb(18, 35, 190, 120), Color.TRANSPARENT },
new float[] { 0f, 0.55f, edgeStop - 0.05f, edgeStop, edgeStop + 0.10f, 1f }, android.graphics.Shader.TileMode.CLAMP)); new float[] { 0f, 0.55f, edgeStop - 0.05f, edgeStop, edgeStop + 0.10f, 1f }, android.graphics.Shader.TileMode.CLAMP);
glowCx = cx; glowCy = cy; glowR = glowRadius;
}
paint.setShader(glow);
canvas.drawCircle(cx, cy, glowRadius, paint); canvas.drawCircle(cx, cy, glowRadius, paint);
paint.setShader(null); paint.setShader(null);
paint.setStyle(Paint.Style.STROKE); paint.setStyle(Paint.Style.STROKE);
@@ -123,14 +144,11 @@ public class BluetoothRadarView extends View {
paint.setTextAlign(Paint.Align.CENTER); paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(12 * getResources().getDisplayMetrics().scaledDensity); paint.setTextSize(12 * getResources().getDisplayMetrics().scaledDensity);
canvas.drawText("YOU", cx, cy + 24, paint); canvas.drawText("YOU", cx, cy + 24, paint);
ArrayList<Target> ordered = new ArrayList<Target>(targets); // Reused across frames; a fresh list and comparator per frame is
Collections.sort(ordered, new Comparator<Target>() { // pure garbage at 30fps.
public int compare(Target first, Target second) { ordered.clear();
float a = angleFor(first.address); ordered.addAll(targets);
float b = angleFor(second.address); Collections.sort(ordered, byAngle);
return a < b ? -1 : (a > b ? 1 : 0);
}
});
float density = getResources().getDisplayMetrics().scaledDensity; float density = getResources().getDisplayMetrics().scaledDensity;
for (int i = 0; i < ordered.size(); i++) { for (int i = 0; i < ordered.size(); i++) {
Target target = ordered.get(i); Target target = ordered.get(i);
@@ -41,6 +41,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -71,6 +72,10 @@ public class DeviceManagerActivity extends Activity {
private TextView pairedHeader; private TextView pairedHeader;
private ListView pairedList; private ListView pairedList;
private final Handler handler = new Handler(); private final Handler handler = new Handler();
/** How long one device may re-advertise before it is persisted again. */
private static final long PERSIST_INTERVAL_MS = 3000L;
private final HashMap<String, Long> lastPersisted = new HashMap<String, Long>();
private final LinkedHashMap<String, SignalEntry> signals = private final LinkedHashMap<String, SignalEntry> signals =
new LinkedHashMap<String, SignalEntry>(); new LinkedHashMap<String, SignalEntry>();
private final ArrayList<BluetoothDevice> paired = new ArrayList<BluetoothDevice>(); private final ArrayList<BluetoothDevice> paired = new ArrayList<BluetoothDevice>();
@@ -282,7 +287,7 @@ public class DeviceManagerActivity extends Activity {
if (!bluetooth.isEnabled()) { if (!bluetooth.isEnabled()) {
startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); return; startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); return;
} }
stopScan(); signals.clear(); refreshCategoryOptions(); scanning = true; radar.setScanning(true); scanButton.setText("STOP RADAR SCAN"); updateRadar(); stopScan(); signals.clear(); lastPersisted.clear(); refreshCategoryOptions(); scanning = true; radar.setScanning(true); scanButton.setText("STOP RADAR SCAN"); updateRadar();
scanStatus.setText("Scanning… tap a dot when your item appears"); scanStatus.setText("Scanning… tap a dot when your item appears");
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {
leScanner = bluetooth.getBluetoothLeScanner(); leScanner = bluetooth.getBluetoothLeScanner();
@@ -312,6 +317,7 @@ public class DeviceManagerActivity extends Activity {
private void acceptScanResult(ScanResult result) { private void acceptScanResult(ScanResult result) {
if (result == null) return; if (result == null) return;
if (touchOnly(result.getDevice(), result.getRssi())) return;
String advertisedName = null; String advertisedName = null;
String companyVendor = null; String companyVendor = null;
int appearance = -1; int appearance = -1;
@@ -355,14 +361,40 @@ public class DeviceManagerActivity extends Activity {
acceptSignal(device, rssi, null, null, -1, null, -1, null); acceptSignal(device, rssi, null, null, -1, null, -1, null);
} }
/**
* SCAN_MODE_LOW_LATENCY hands back the same beacon many times a second,
* and the ScanCallback is delivered on the main looper. Running the full
* path for each one put two SQLite reads, four writes, a hex dump of the
* advertisement and a spinner rebuild on the UI thread per advertisement
* - which is what starves the radar sweep.
*
* A device already on screen only needs its signal strength and its
* last-seen stamp refreshed until the interval is up. Returns true when
* the caller can stop there.
*/
private synchronized boolean touchOnly(BluetoothDevice device, int rssi) {
if (device == null || rssi == 0 || rssi == 127) return false;
String address = safeAddress(device);
SignalEntry entry = signals.get(address);
if (entry == null) return false;
Long last = lastPersisted.get(address);
long now = System.currentTimeMillis();
if (last == null || now - last.longValue() >= PERSIST_INTERVAL_MS) return false;
entry.rssi = rssi;
entry.seenAt = now;
return true;
}
private synchronized void acceptSignal(BluetoothDevice device, int rssi, String advertisedName, private synchronized void acceptSignal(BluetoothDevice device, int rssi, String advertisedName,
String companyVendor, int appearance, String broadcastType, String companyVendor, int appearance, String broadcastType,
int companyId, String advertisementHex) { int companyId, String advertisementHex) {
if (device == null || rssi == 0 || rssi == 127) return; if (device == null || rssi == 0 || rssi == 127) return;
String address = safeAddress(device); String address = safeAddress(device);
SignalEntry entry = signals.get(address); SignalEntry entry = signals.get(address);
if (entry == null) { entry = new SignalEntry(); signals.put(address, entry); } boolean firstSighting = entry == null;
if (firstSighting) { entry = new SignalEntry(); signals.put(address, entry); }
entry.device = device; entry.rssi = rssi; entry.seenAt = System.currentTimeMillis(); entry.device = device; entry.rssi = rssi; entry.seenAt = System.currentTimeMillis();
lastPersisted.put(address, Long.valueOf(entry.seenAt));
String cachedName = safeName(device); String cachedName = safeName(device);
if (useful(advertisedName)) entry.broadcastName = advertisedName; if (useful(advertisedName)) entry.broadcastName = advertisedName;
else if (!useful(entry.broadcastName) && useful(cachedName)) entry.broadcastName = cachedName; else if (!useful(entry.broadcastName) && useful(cachedName)) entry.broadcastName = cachedName;
@@ -381,10 +413,15 @@ public class DeviceManagerActivity extends Activity {
entry.vendorName, entry.category, entry.broadcastType, appearance, companyId, advertisementHex, recentObservationFix()); entry.vendorName, entry.category, entry.broadcastType, appearance, companyId, advertisementHex, recentObservationFix());
String vendor = entry.vendorName; String vendor = entry.vendorName;
if (vendor != null) database.setVendor(address, vendor); if (vendor != null) database.setVendor(address, vendor);
// pruneTask already runs exactly these three every second while a
// scan is live, so doing them per advertisement was duplicated work.
// A device the user has not seen yet still refreshes immediately.
if (firstSighting) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { refreshCategoryOptions(); updateRadar(); pairedAdapter.notifyDataSetChanged(); } public void run() { refreshCategoryOptions(); updateRadar(); pairedAdapter.notifyDataSetChanged(); }
}); });
} }
}
private TrackerDatabase.Fix recentObservationFix() { private TrackerDatabase.Fix recentObservationFix() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -412,7 +449,10 @@ public class DeviceManagerActivity extends Activity {
synchronized (DeviceManagerActivity.this) { synchronized (DeviceManagerActivity.this) {
for (Map.Entry<String, SignalEntry> item : signals.entrySet()) for (Map.Entry<String, SignalEntry> item : signals.entrySet())
if (item.getValue().seenAt < cutoff) expired.add(item.getKey()); if (item.getValue().seenAt < cutoff) expired.add(item.getKey());
for (int i = 0; i < expired.size(); i++) signals.remove(expired.get(i)); for (int i = 0; i < expired.size(); i++) {
signals.remove(expired.get(i));
lastPersisted.remove(expired.get(i));
}
if (selectedSignal != null && !signals.containsValue(selectedSignal)) { if (selectedSignal != null && !signals.containsValue(selectedSignal)) {
selectedSignal = null; radarActions.setVisibility(View.GONE); selectedSignal = null; radarActions.setVisibility(View.GONE);
} }
@@ -1,8 +1,22 @@
package com.wytehat.btlogger; package com.wytehat.btlogger;
public class LocationPoint { public class LocationPoint {
/** The item linked to the phone at this position. */
public static final String EVENT_CONNECTED = "connected";
/** The link to the phone dropped at this position. */
public static final String EVENT_DISCONNECTED = "disconnected";
public double latitude; public double latitude;
public double longitude; public double longitude;
public float accuracy; public float accuracy;
public long timestamp; public long timestamp;
/**
* Why this point was recorded: EVENT_CONNECTED, EVENT_DISCONNECTED, or
* null for a plain position refresh while the link state was unchanged.
* Null for every row logged before the column existed.
*/
public String event;
} }
@@ -34,9 +34,31 @@ public final class MapHtmlBuilder {
public int aggressiveness = HistoryNormalizer.DEFAULT_AGGRESSIVENESS; public int aggressiveness = HistoryNormalizer.DEFAULT_AGGRESSIVENESS;
} }
/** Diameter of a device's own marker; history points draw at half. */
private static final int DEVICE_MARKER_SIZE = 22;
/** A location-locked device draws smaller, since its position is fixed. */
private static final int LOCKED_MARKER_SIZE = 10;
private MapHtmlBuilder() { private MapHtmlBuilder() {
} }
/** The round white-bordered marker, shared by devices and history points. */
private static String divIcon(
String color,
int markerSize,
int iconSize,
int anchor) {
return "L.divIcon({className:'',html:\""
+ "<div style='width:" + markerSize
+ "px;height:" + markerSize
+ "px;border-radius:50%;background:" + color
+ ";border:2px solid white;box-shadow:0 1px 4px #333'>"
+ "</div>\",iconSize:[" + iconSize + "," + iconSize
+ "],iconAnchor:[" + anchor + "," + anchor + "]})";
}
public static String build( public static String build(
List<DeviceRecord> rows, List<DeviceRecord> rows,
String selectedAddress, String selectedAddress,
@@ -90,7 +112,7 @@ public final class MapHtmlBuilder {
appendTrail(script, i, history, color); appendTrail(script, i, history, color);
} }
appendHistoryPoints(script, record, history, color, options); appendHistoryPoints(script, i, record, history, color, options);
} }
appendDeviceMarker( appendDeviceMarker(
@@ -206,26 +228,39 @@ public final class MapHtmlBuilder {
private static void appendHistoryPoints( private static void appendHistoryPoints(
StringBuilder script, StringBuilder script,
int index,
DeviceRecord record, DeviceRecord record,
List<LocationPoint> history, List<LocationPoint> history,
String color, String color,
Options options) { Options options) {
/*
* Same marker the device itself uses, at half size. Defined once per
* device and shared by all of its history points -- a divIcon is a
* DOM node per marker, so building one per point would be costly on
* a long trail.
*/
int markerSize = DEVICE_MARKER_SIZE / 2;
int iconSize = markerSize + 6;
int anchor = iconSize / 2;
script.append("var hicon").append(index).append('=')
.append(divIcon(color, markerSize, iconSize, anchor))
.append(';');
for (int p = 0; p < history.size(); p++) { for (int p = 0; p < history.size(); p++) {
LocationPoint point = history.get(p); LocationPoint point = history.get(p);
if (point == null) continue; if (point == null) continue;
script.append("L.circleMarker([") script.append("L.marker([")
.append(point.latitude) .append(point.latitude)
.append(',') .append(',')
.append(point.longitude) .append(point.longitude)
.append("],{radius:5,color:'") .append("],{icon:hicon")
.append(color) .append(index)
.append("',fillColor:'") .append("})")
.append(color)
.append("',fillOpacity:.9,weight:2})")
.append(".addTo(map).bindPopup('") .append(".addTo(map).bindPopup('")
.append(MapFormat.js(historyPopup(record, point, options))) .append(MapFormat.js(historyPopup(record, point, options)))
.append("');"); .append("');");
@@ -245,30 +280,18 @@ public final class MapHtmlBuilder {
boolean locked = record.locationLocked == 1; boolean locked = record.locationLocked == 1;
int markerSize = locked ? 10 : 22; int markerSize = locked
? LOCKED_MARKER_SIZE
: DEVICE_MARKER_SIZE;
int iconSize = markerSize + 6; int iconSize = markerSize + 6;
int anchor = iconSize / 2; int anchor = iconSize / 2;
String markerColor = locked ? "#00A86B" : color; String markerColor = locked ? "#00A86B" : color;
script.append("var icon").append(index) script.append("var icon").append(index).append('=')
.append("=L.divIcon({className:'',html:\"") .append(divIcon(markerColor, markerSize, iconSize, anchor))
.append("<div style='width:") .append(';');
.append(markerSize)
.append("px;height:")
.append(markerSize)
.append("px;border-radius:50%;background:")
.append(markerColor)
.append(";border:2px solid white;box-shadow:0 1px 4px #333'>")
.append("</div>\",iconSize:[")
.append(iconSize)
.append(',')
.append(iconSize)
.append("],iconAnchor:[")
.append(anchor)
.append(',')
.append(anchor)
.append("]});");
script.append("var marker").append(index) script.append("var marker").append(index)
.append("=L.marker([") .append("=L.marker([")
@@ -492,6 +515,20 @@ public final class MapHtmlBuilder {
: "Previous recorded point") : "Previous recorded point")
.append("</b>"); .append("</b>");
/*
* Why the point exists at all. Rows logged before the event column
* was added carry no event, and a plain position refresh carries
* none either, so say nothing rather than guess.
*/
if (LocationPoint.EVENT_CONNECTED.equals(point.event)) {
html.append("<br>🔗 Connected to the phone here");
} else if (LocationPoint.EVENT_DISCONNECTED.equals(point.event)) {
html.append("<br>💔 Link to the phone dropped here");
}
long time = HistoryNormalizer.pointTime(point); long time = HistoryNormalizer.pointTime(point);
if (time > 0) { if (time > 0) {
@@ -11,7 +11,7 @@ import java.util.Locale;
public class TrackerDatabase extends SQLiteOpenHelper { public class TrackerDatabase extends SQLiteOpenHelper {
private static final String DB_NAME = "bluetooth_tracker.db"; private static final String DB_NAME = "bluetooth_tracker.db";
private static final int DB_VERSION = 15; private static final int DB_VERSION = 16;
public TrackerDatabase(Context context) { public TrackerDatabase(Context context) {
super(context, DB_NAME, null, DB_VERSION); super(context, DB_NAME, null, DB_VERSION);
@@ -32,6 +32,7 @@ public class TrackerDatabase extends SQLiteOpenHelper {
point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude")); point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy")); point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy"));
point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at")); point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at"));
point.event = cursor.getString(cursor.getColumnIndex("event"));
result.add(point); result.add(point);
} }
} finally { cursor.close(); } } finally { cursor.close(); }
@@ -100,6 +101,8 @@ public class TrackerDatabase extends SQLiteOpenHelper {
addColumn(db, "ALTER TABLE devices ADD COLUMN track_broadcast INTEGER DEFAULT 0"); addColumn(db, "ALTER TABLE devices ADD COLUMN track_broadcast INTEGER DEFAULT 0");
if (oldVersion < 14) createSupportingTables(db); if (oldVersion < 14) createSupportingTables(db);
if (oldVersion < 15) seedVendors(db); if (oldVersion < 15) seedVendors(db);
if (oldVersion < 16)
addColumn(db, "ALTER TABLE location_history ADD COLUMN event TEXT");
} }
private void addColumn(SQLiteDatabase db, String sql) { private void addColumn(SQLiteDatabase db, String sql) {
@@ -108,7 +111,7 @@ public class TrackerDatabase extends SQLiteOpenHelper {
private void createSupportingTables(SQLiteDatabase db) { private void createSupportingTables(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS location_history (id INTEGER PRIMARY KEY AUTOINCREMENT," + 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)"); "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 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," + 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," + "address TEXT,session_id TEXT,sampled_at INTEGER,latitude REAL,longitude REAL," +
@@ -166,7 +169,8 @@ public class TrackerDatabase extends SQLiteOpenHelper {
if (old != null) preserveProfile(values, old, fix); if (old != null) preserveProfile(values, old, fix);
getWritableDatabase().insertWithOnConflict("devices", null, values, getWritableDatabase().insertWithOnConflict("devices", null, values,
SQLiteDatabase.CONFLICT_REPLACE); SQLiteDatabase.CONFLICT_REPLACE);
if (fix != null) insertHistory(address, fix, time); if (fix != null)
insertHistory(address, fix, time, LocationPoint.EVENT_CONNECTED);
} }
public synchronized void disconnected(String address, String broadcast, long time, public synchronized void disconnected(String address, String broadcast, long time,
@@ -177,7 +181,8 @@ public class TrackerDatabase extends SQLiteOpenHelper {
values.put("reason", reason); values.put("reason", reason);
values.put("is_tracked", 1); values.put("is_tracked", 1);
getWritableDatabase().update("devices", values, "address=?", new String[] { address }); getWritableDatabase().update("devices", values, "address=?", new String[] { address });
if (fix != null) insertHistory(address, fix, time); if (fix != null)
insertHistory(address, fix, time, LocationPoint.EVENT_DISCONNECTED);
} }
public synchronized void telemetry(String address, String broadcast, Integer battery, public synchronized void telemetry(String address, String broadcast, Integer battery,
@@ -242,16 +247,21 @@ public class TrackerDatabase extends SQLiteOpenHelper {
values.put("has_location", 1); values.put("has_location", 1);
values.put("updated_at", time); values.put("updated_at", time);
getWritableDatabase().update("devices", values, "address=?", new String[] { address }); getWritableDatabase().update("devices", values, "address=?", new String[] { address });
insertHistory(address, fix, time); insertHistory(address, fix, time, null);
} }
private void insertHistory(String address, Fix fix, long time) { /**
* @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(); ContentValues values = new ContentValues();
values.put("address", address); values.put("address", address);
values.put("latitude", fix.latitude); values.put("latitude", fix.latitude);
values.put("longitude", fix.longitude); values.put("longitude", fix.longitude);
values.put("accuracy", fix.accuracy); values.put("accuracy", fix.accuracy);
values.put("logged_at", time); values.put("logged_at", time);
values.put("event", event);
getWritableDatabase().insert("location_history", null, values); getWritableDatabase().insert("location_history", null, values);
} }
@@ -437,6 +447,7 @@ public class TrackerDatabase extends SQLiteOpenHelper {
point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude")); point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy")); point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy"));
point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at")); point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at"));
point.event = cursor.getString(cursor.getColumnIndex("event"));
result.add(point); result.add(point);
} }
} finally { cursor.close(); } } finally { cursor.close(); }