diff --git a/app/src/main/java/com/wytehat/btlogger/MapActivity.java b/app/src/main/java/com/wytehat/btlogger/MapActivity.java index 55be9d6..c35f08f 100644 --- a/app/src/main/java/com/wytehat/btlogger/MapActivity.java +++ b/app/src/main/java/com/wytehat/btlogger/MapActivity.java @@ -1,5 +1,3 @@ -package com.wytehat.btlogger; - import android.app.Activity; import android.app.DatePickerDialog; import android.app.TimePickerDialog; @@ -16,8 +14,6 @@ import android.location.LocationListener; import android.location.LocationManager; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -36,8 +32,6 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class MapActivity extends Activity implements SensorEventListener { @@ -54,9 +48,8 @@ implements SensorEventListener { private LinearLayout filterPanel; - private Spinner categorySpinner; - private Spinner deviceSpinner; - private Spinner resolutionSpinner; + private Spinner categorySpinner; + private Spinner deviceSpinner; private CheckBox showAll; @@ -75,54 +68,15 @@ implements SensorEventListener { private final ArrayList deviceLabels = new ArrayList(); - private final ArrayList resolutionLabels = - new ArrayList(); + private String categoryFilter = "Trackers / Tags"; + private String deviceFilter = "All"; - /* - * Time-bucket widths for the trail sampler. - * 0 = every stored point (still capped by TrackPointSampler). - */ - private static final long[] RESOLUTION_VALUES = { - 0L, - 15L * 60L * 1000L, - 60L * 60L * 1000L, - 120L * 60L * 1000L - }; + private boolean rebuildingCategories; + private boolean rebuildingDevices; + private boolean loadingMap; - private static final String[] RESOLUTION_LABELS = { - "All points", - "Every 15 min", - "Every 1 hour", - "Every 2 hours" - }; - - private volatile String categoryFilter = "Trackers / Tags"; - private volatile String deviceFilter = "All"; - - private boolean rebuildingCategories; - private boolean rebuildingDevices; - private boolean rebuildingResolution = true; - private volatile boolean loadingMap; - - private volatile long historyFrom; - private volatile long historyTo; - - /* - * Selected trail resolution, read by the background loader. - */ - private volatile long sampleInterval = RESOLUTION_VALUES[0]; - - /* - * Map data (SQLite reads + HTML generation) is built off the main - * thread; only the WebView load and the spinner refresh run on it. - */ - private final ExecutorService mapExecutor = - Executors.newSingleThreadExecutor(); - - private final Handler ui = - new Handler(Looper.getMainLooper()); - - private volatile boolean destroyed; + private long historyFrom; + private long historyTo; private LocationManager locationManager; private SensorManager sensorManager; @@ -313,53 +267,14 @@ implements SensorEventListener { deviceSpinner.setAdapter(deviceAdapter); - filterPanel.addView( - deviceSpinner, - compactParams() - ); + filterPanel.addView( + deviceSpinner, + compactParams() + ); - /* - * Trail resolution (downsampling). - */ - TextView resolutionLabel = - smallLabel("Trail detail"); - - filterPanel.addView(resolutionLabel); - - resolutionSpinner = new Spinner(this); - - for (int i = 0; - i < RESOLUTION_LABELS.length; - i++) { - - resolutionLabels.add( - RESOLUTION_LABELS[i] - ); - } - - ArrayAdapter resolutionAdapter = - new ArrayAdapter( - this, - android.R.layout.simple_spinner_item, - resolutionLabels - ); - - resolutionAdapter.setDropDownViewResource( - android.R.layout.simple_spinner_dropdown_item - ); - - resolutionSpinner.setAdapter( - resolutionAdapter - ); - - filterPanel.addView( - resolutionSpinner, - compactParams() - ); - - /* - * Time. - */ + /* + * Time. + */ TextView timeLabel = smallLabel("Location history"); @@ -650,51 +565,7 @@ implements SensorEventListener { } ); - /* - * Trail resolution: applied immediately so the user can compare - * densities without reopening the panel. - */ - resolutionSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - - @Override - public void onItemSelected( - AdapterView parent, - View view, - int position, - long id) { - - if (rebuildingResolution) { - return; - } - - if (position < 0 || - position >= - RESOLUTION_VALUES.length) { - - return; - } - - if (sampleInterval == - RESOLUTION_VALUES[position]) { - - return; - } - - sampleInterval = - RESOLUTION_VALUES[position]; - - reloadMap(); - } - - @Override - public void onNothingSelected( - AdapterView parent) { - } - } - ); - - fromButton.setOnClickListener( + fromButton.setOnClickListener( new View.OnClickListener() { @Override @@ -806,11 +677,9 @@ implements SensorEventListener { } ); - updateTimeButtons(); + updateTimeButtons(); - rebuildingResolution = false; - - rebuildDeviceFilter(); + rebuildDeviceFilter(); /* * The initial map is deliberately: @@ -1020,28 +889,12 @@ implements SensorEventListener { ); } - private void rebuildDeviceFilter() { + private void rebuildDeviceFilter() { - rebuildDeviceFilter( - getMapDevices( - showAll.isChecked() - ) - ); - } + List available = + getMapDevices(); - /* - * Main thread only. Accepts an already loaded device list so the - * background loader does not repeat the query. - */ - private void rebuildDeviceFilter( - List available) { - - if (available == null) { - available = - new ArrayList(); - } - - ArrayList nextAddresses = + ArrayList nextAddresses = new ArrayList(); ArrayList nextLabels = @@ -1138,19 +991,10 @@ implements SensorEventListener { rebuildingDevices = false; } - /* - * Safe to call from a worker thread: the caller passes the checkbox - * state in instead of reading the view here. - */ - private List getMapDevices( - boolean includeAll) { + private List getMapDevices() { - TrackerDatabase database = db; - - List all = - database == null - ? null - : database.all(); + List all = + db.all(); ArrayList result = new ArrayList(); @@ -1174,17 +1018,17 @@ implements SensorEventListener { * Normal map: * only explicitly map-enabled devices. */ - if (!includeAll && - record.mapEnabled != 1) { + if (!showAll.isChecked() && + record.mapEnabled != 1) { continue; } /* - * Normal map excludes unknown entries. - */ - if (!includeAll && - isUnknown(record)) { + * Normal map excludes unknown entries. + */ + if (!showAll.isChecked() && + isUnknown(record)) { continue; } @@ -1365,175 +1209,82 @@ implements SensorEventListener { return 0; } - private void reloadMap() { + private void reloadMap() { - if (loadingMap) { - return; - } + if (loadingMap) { + return; + } - loadingMap = true; - mapReady = false; + loadingMap = true; + mapReady = false; - /* - * View state is snapshotted on the main thread; everything after - * this point (SQLite reads, trail sampling, HTML generation) runs - * on the worker. - */ - final boolean includeAll = - showAll.isChecked(); + try { - mapExecutor.execute( - new Runnable() { + List available = + getMapDevices(); - @Override - public void run() { + refreshMapCategories( + available + ); - List available = null; - String html = null; + /* + * Rebuild device list after category + * changes. + */ + rebuildDeviceFilter(); - try { + ArrayList rows = + new ArrayList(); - available = - getMapDevices(includeAll); + for (int i = 0; + i < available.size(); + i++) { - String device = deviceFilter; + DeviceRecord record = + available.get(i); - /* - * The selected device may have gone away since - * the last load; fall back to showing all. - */ - if (!"All".equals(device) && - !containsAddress( - available, - device - )) { + if (record == null || + !record.hasLocation) { + continue; + } - device = "All"; - } + if (!categoryMatches(record)) { + continue; + } - ArrayList rows = - new ArrayList(); + if (!"All".equals(deviceFilter) && + !deviceFilter.equals( + record.address + )) { + continue; + } - for (int i = 0; - i < available.size(); - i++) { + rows.add(record); + } - DeviceRecord record = - available.get(i); + String html = + buildHtml( + rows, + focus + ); - if (record == null || - !record.hasLocation) { - continue; - } + web.loadDataWithBaseURL( + "https://unpkg.com/", + html, + "text/html", + "UTF-8", + null + ); - if (!categoryMatches(record)) { - continue; - } + } finally { - if (!"All".equals(device) && - !device.equals( - record.address - )) { - continue; - } + loadingMap = false; + } + } - rows.add(record); - } - - html = - buildHtml( - rows, - focus, - device - ); - - } catch (Exception ignored) { - } - - final List loaded = - available; - - final String content = html; - - ui.post( - new Runnable() { - - @Override - public void run() { - - try { - - if (destroyed || - web == null) { - return; - } - - refreshMapCategories( - loaded - ); - - /* - * Rebuild device list after - * category changes. - */ - rebuildDeviceFilter( - loaded - ); - - if (content != null) { - - web.loadDataWithBaseURL( - "https://unpkg.com/", - content, - "text/html", - "UTF-8", - null - ); - } - - } finally { - - loadingMap = false; - } - } - } - ); - } - } - ); - } - - private boolean containsAddress( - List records, - String address) { - - if (records == null || - address == null) { - - return false; - } - - for (int i = 0; - i < records.size(); - i++) { - - DeviceRecord record = - records.get(i); - - if (record != null && - address.equals(record.address)) { - - return true; - } - } - - return false; - } - - private String buildHtml( - List rows, - String selectedAddress, - String device) { + private String buildHtml( + List rows, + String selectedAddress) { StringBuilder script = new StringBuilder(); @@ -1542,44 +1293,29 @@ implements SensorEventListener { * Historical data is loaded ONLY for a * specifically selected device. */ - if (!"All".equals(device)) { + if (!"All".equals(deviceFilter)) { - for (int i = 0; - i < rows.size(); - i++) { + for (int i = 0; + i < rows.size(); + i++) { - DeviceRecord record = - rows.get(i); + DeviceRecord record = + rows.get(i); - if (!device.equals( - record.address - )) { - continue; - } + if (!deviceFilter.equals( + record.address + )) { + continue; + } - TrackerDatabase database = db; + List history = + db.history( + record.address, + historyFrom, + historyTo + ); - List history = - database == null - ? null - : database.history( - record.address, - historyFrom, - historyTo - ); - - /* - * Dense histories are downsampled here, on the - * worker thread, so the WebView never receives - * thousands of markers. - */ - history = - TrackPointSampler.sample( - history, - sampleInterval - ); - - appendHistory( + appendHistory( script, record, history, @@ -2594,18 +2330,11 @@ implements SensorEventListener { } @Override - protected void onDestroy() { + protected void onDestroy() { - destroyed = true; + try { - try { - mapExecutor.shutdownNow(); - } catch (Exception ignored) { - } - - try { - - if (web != null) { + if (web != null) { web.stopLoading(); web.destroy(); web = null; diff --git a/app/src/main/java/com/wytehat/btlogger/TrackPointSampler.java b/app/src/main/java/com/wytehat/btlogger/TrackPointSampler.java deleted file mode 100644 index 346984d..0000000 --- a/app/src/main/java/com/wytehat/btlogger/TrackPointSampler.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.wytehat.btlogger; - -import java.util.ArrayList; -import java.util.List; - -/** - * Downsamples dense GPS history before it is turned into map markers. - * - * Time-bucket sampling: the timeline is cut into fixed buckets and one - * representative point (the most accurate fix) survives per bucket. The very - * first and last fixes are always kept so the trail keeps its real endpoints. - * - * A hard ceiling is applied afterwards - even "All Points" cannot hand the - * WebView an unbounded number of circle markers. - */ -final class TrackPointSampler { - - /** Ceiling on rendered points, applied regardless of the chosen interval. */ - static final int MAX_RENDERED_POINTS = 750; - - private TrackPointSampler() { - } - - /** - * @param bucketMillis bucket width, or 0 for "All Points" (cap only). - */ - static List sample(List points, long bucketMillis) { - - if (points == null || points.size() < 3) { - return points; - } - - List reduced = - bucketMillis > 0 - ? bucket(points, bucketMillis) - : points; - - return cap(reduced); - } - - private static List bucket(List points, long bucketMillis) { - - ArrayList result = new ArrayList(); - - LocationPoint best = null; - long currentBucket = 0; - - for (int i = 0; i < points.size(); i++) { - - LocationPoint point = points.get(i); - - if (point == null) { - continue; - } - - long bucketIndex = point.timestamp / bucketMillis; - - if (best == null) { - best = point; - currentBucket = bucketIndex; - continue; - } - - if (bucketIndex != currentBucket) { - result.add(best); - best = point; - currentBucket = bucketIndex; - continue; - } - - if (moreAccurate(point, best)) { - best = point; - } - } - - if (best != null) { - result.add(best); - } - - /* - * Always terminate on the newest fix - that is the one the user cares - * about when reading a trail. - */ - LocationPoint last = points.get(points.size() - 1); - - if (last != null && - (result.isEmpty() || - result.get(result.size() - 1) != last)) { - - result.add(last); - } - - return result; - } - - private static List cap(List points) { - - int size = points.size(); - - if (size <= MAX_RENDERED_POINTS) { - return points; - } - - int stride = (size + MAX_RENDERED_POINTS - 1) / MAX_RENDERED_POINTS; - - ArrayList result = new ArrayList(); - - for (int i = 0; i < size; i += stride) { - result.add(points.get(i)); - } - - LocationPoint last = points.get(size - 1); - - if (result.isEmpty() || - result.get(result.size() - 1) != last) { - - result.add(last); - } - - return result; - } - - private static boolean moreAccurate(LocationPoint candidate, LocationPoint current) { - - if (candidate.accuracy <= 0) { - return false; - } - - return current.accuracy <= 0 || - candidate.accuracy < current.accuracy; - } -} diff --git a/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java b/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java index 74bce15..e5c2e04 100644 --- a/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java +++ b/app/src/main/java/com/wytehat/btlogger/TrackerDatabase.java @@ -17,25 +17,10 @@ public class TrackerDatabase extends SQLiteOpenHelper { super(context, DB_NAME, null, DB_VERSION); } - public synchronized List history(String address, long historyFrom, long historyTo) + public 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")); - result.add(point); - } - } finally { cursor.close(); } - return result; + // TODO: Implement this method + return null; } public void onCreate(SQLiteDatabase db) {