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:
co-authored by
Claude Opus 5
parent
0f01b836c6
commit
f16efce66e
@@ -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(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user