Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
564877e1e7 | ||
|
|
8d3d571954 | ||
|
|
a48cc91680 | ||
|
|
a5f5d415e2 | ||
|
|
a2c74dc52f |
@@ -0,0 +1,450 @@
|
|||||||
|
# BlueToothLogger Android Application - Technical Project Context
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Project Name:** BlueToothLogger
|
||||||
|
**Package:** `com.wytehat.btlogger`
|
||||||
|
**Location:** `/Users/n0tst3v3/bletracker/BlueToothLogger`
|
||||||
|
**Type:** Bluetooth tracking and logging application with spatial analysis capabilities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Classes and Inheritance
|
||||||
|
|
||||||
|
| Class | Extends | Implements | Purpose |
|
||||||
|
|-------|---------|------------|---------|
|
||||||
|
| `MainActivity` | Activity | ItemActionListener | Main entry point, UI controller |
|
||||||
|
| `BluetoothTrackingService` | Service | LocationListener | Background Bluetooth tracking |
|
||||||
|
| `TrackerDatabase` | SQLiteOpenHelper | - | SQLite database wrapper |
|
||||||
|
| `MapActivity` | Activity | - | Map visualization |
|
||||||
|
| `DeviceManagerActivity` | Activity | - | Device management UI |
|
||||||
|
| `LiveRangeFinderActivity` | Activity | LocationListener, SensorEventListener | Real-time signal finder |
|
||||||
|
| `BluetoothRadarView` | View | - | Custom radar visualization |
|
||||||
|
| `DeviceListAdapter` | BaseAdapter | - | List adapter for devices |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BluetoothTrackingService - Technical Details
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
```java
|
||||||
|
public static final String ACTION_DATA_CHANGED = "com.wytehat.btlogger.DATA_CHANGED"
|
||||||
|
public static final String ACTION_CONFIG_CHANGED = "com.wytehat.btlogger.CONFIG_CHANGED"
|
||||||
|
public static final String ACTION_RANGE_MODE = "com.wytehat.btlogger.RANGE_MODE"
|
||||||
|
private static final int NOTIFICATION_ID = 42
|
||||||
|
private static final String CHANNEL_ID = "bluetooth_tracking"
|
||||||
|
private static final String ALERT_CHANNEL_ID = "tracked_item_disconnects"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
```java
|
||||||
|
private TrackerDatabase db
|
||||||
|
private LocationManager locationManager
|
||||||
|
private BluetoothAdapter bluetooth
|
||||||
|
private Location lastLocation
|
||||||
|
private boolean receiverRegistered
|
||||||
|
private boolean rangeModeActive
|
||||||
|
private int phoneBattery = 100
|
||||||
|
private final Handler handler = new Handler()
|
||||||
|
private final HashMap<String, Long> lastPeriodicLog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
public void onCreate()
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId)
|
||||||
|
public void onDestroy()
|
||||||
|
public IBinder onBind(Intent intent)
|
||||||
|
|
||||||
|
// Location callbacks
|
||||||
|
public void onLocationChanged(Location location)
|
||||||
|
public void onProviderDisabled(String provider)
|
||||||
|
public void onProviderEnabled(String provider)
|
||||||
|
public void onStatusChanged(String provider, int status, Bundle extras)
|
||||||
|
|
||||||
|
// Bluetooth callbacks
|
||||||
|
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)
|
||||||
|
|
||||||
|
// Private methods
|
||||||
|
private void loadLastLocation()
|
||||||
|
private void configureLocation()
|
||||||
|
private int shortestActiveInterval()
|
||||||
|
private void stopLocation()
|
||||||
|
private void captureFresh(String address, long eventTime)
|
||||||
|
private TrackerDatabase.Fix currentFix()
|
||||||
|
private TrackerDatabase.Fix fix(Location location)
|
||||||
|
private void resolveVendor(String address, int companyId)
|
||||||
|
private String bytesToHex(byte[] data)
|
||||||
|
private int parseCompanyId(byte[] scanRecord)
|
||||||
|
private String infer(DeviceRecord record)
|
||||||
|
private void disconnectAll(String reason)
|
||||||
|
private void syncPairedDevices()
|
||||||
|
private void restartBackgroundScan()
|
||||||
|
private boolean shouldBackgroundScan()
|
||||||
|
private boolean isLocationEligible(DeviceRecord record, long now)
|
||||||
|
private void notifyChanged()
|
||||||
|
private boolean hasLocationPermission()
|
||||||
|
private boolean hasBluetoothPermission()
|
||||||
|
private String address(BluetoothDevice device)
|
||||||
|
private String name(BluetoothDevice device)
|
||||||
|
private Location newer(Location first, Location second)
|
||||||
|
private void showDisconnectAlert(DeviceRecord record, String reason)
|
||||||
|
private Notification buildNotification()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TrackerDatabase - Technical Details
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
```java
|
||||||
|
private static final String DB_NAME = "bluetooth_tracker.db"
|
||||||
|
private static final int DB_VERSION = 15
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inner Class: Fix
|
||||||
|
```java
|
||||||
|
public static class Fix {
|
||||||
|
public double latitude
|
||||||
|
public double longitude
|
||||||
|
public float accuracy
|
||||||
|
public Fix(double lat, double lon, float acc)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
public TrackerDatabase(Context context)
|
||||||
|
public void onCreate(SQLiteDatabase db)
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
||||||
|
|
||||||
|
// Device tracking
|
||||||
|
public synchronized void connected(String address, String broadcast, long time, Fix fix)
|
||||||
|
public synchronized void disconnected(String address, String broadcast, long time, String reason)
|
||||||
|
public synchronized void telemetry(String address, String broadcast, Integer battery, String vendor, String category, long time)
|
||||||
|
public synchronized void recordObservation(String address, long time, int rssi, int txPower)
|
||||||
|
public synchronized void updateLocation(String address, Fix fix, long time)
|
||||||
|
|
||||||
|
// Profile and calibration
|
||||||
|
public synchronized void updateProfile(String address, String title, String photo, String color, long time)
|
||||||
|
public synchronized void updateCalibration(String address, int txPower, double exponent)
|
||||||
|
|
||||||
|
// Range finding
|
||||||
|
public synchronized void logRangeSample(String address, String sessionId, long time, int rssi, double distance)
|
||||||
|
|
||||||
|
// Tracking settings
|
||||||
|
public synchronized void updateTracking(String address, int minutes, String color)
|
||||||
|
public synchronized void markTracked(String address, String broadcast)
|
||||||
|
public synchronized void removeFromTracking(String address)
|
||||||
|
public synchronized void setMapEnabled(String address, boolean enabled)
|
||||||
|
public synchronized void setLiveEnabled(String address, boolean enabled)
|
||||||
|
public synchronized void setTrackBroadcast(String address, boolean enabled)
|
||||||
|
public synchronized void setShowTrail(String address, boolean shown)
|
||||||
|
public synchronized void setLocationLocked(String address, boolean locked)
|
||||||
|
public synchronized void setCustomName(String address, String name)
|
||||||
|
public synchronized void setDetectedCategory(String address, String category)
|
||||||
|
public synchronized void setCategory(String address, String category)
|
||||||
|
public synchronized void setVendor(String address, String vendor)
|
||||||
|
public synchronized void setManualVendor(String address, String vendor)
|
||||||
|
|
||||||
|
// Lookups
|
||||||
|
public synchronized String lookupOui(String address)
|
||||||
|
public synchronized String lookupCompany(int companyId)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
public synchronized DeviceRecord get(String address)
|
||||||
|
public synchronized List<DeviceRecord> all()
|
||||||
|
public synchronized List<DeviceRecord> tracked()
|
||||||
|
public synchronized List<LocationPoint> history(String address, long since)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MainActivity - Technical Details
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
```java
|
||||||
|
private static final int PERMISSIONS = 100
|
||||||
|
private static final int CAMERA = 200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
```java
|
||||||
|
private TrackerDatabase db
|
||||||
|
private DeviceListAdapter adapter
|
||||||
|
private boolean registered
|
||||||
|
private DeviceRecord editing
|
||||||
|
private final Handler refreshHandler = new Handler()
|
||||||
|
private final Runnable refreshLiveAvailability
|
||||||
|
private final BroadcastReceiver updates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
protected void onCreate(Bundle state)
|
||||||
|
protected void onResume()
|
||||||
|
protected void onPause()
|
||||||
|
protected void onDestroy()
|
||||||
|
|
||||||
|
// UI actions
|
||||||
|
public void edit(DeviceRecord record)
|
||||||
|
public void map(DeviceRecord record)
|
||||||
|
public void live(DeviceRecord record)
|
||||||
|
public void manage(DeviceRecord record)
|
||||||
|
|
||||||
|
// Device operations
|
||||||
|
private void removeTrackedItem(DeviceRecord record)
|
||||||
|
private void toggleBroadcastTracking(DeviceRecord record)
|
||||||
|
private void toggleTrail(DeviceRecord record)
|
||||||
|
private void refind(DeviceRecord record)
|
||||||
|
|
||||||
|
// Renaming and settings
|
||||||
|
private void rename(DeviceRecord record)
|
||||||
|
private void chooseTracking(DeviceRecord record)
|
||||||
|
private void chooseColor(DeviceRecord record, int minutes)
|
||||||
|
private void takePhoto()
|
||||||
|
protected void onActivityResult(int request, int result, Intent data)
|
||||||
|
private void showSettings()
|
||||||
|
private void openNotificationSettings()
|
||||||
|
private void openAppSettings(String instruction)
|
||||||
|
private boolean isBatteryOptimizationDisabled()
|
||||||
|
private void requestBackgroundProtection()
|
||||||
|
private void showTrackingInformation()
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
public void onRequestPermissionsResult(int request, String[] permissions, int[] results)
|
||||||
|
private void requestNeededPermissions()
|
||||||
|
private void addPermission(List<String> list, String permission)
|
||||||
|
private void reload()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LiveRangeFinderActivity - Technical Details
|
||||||
|
|
||||||
|
### Implements
|
||||||
|
```java
|
||||||
|
implements LocationListener, SensorEventListener, ItemActionListener
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
protected void onCreate(Bundle savedState)
|
||||||
|
protected void onPause()
|
||||||
|
protected void onDestroy()
|
||||||
|
|
||||||
|
// Location callbacks
|
||||||
|
public void onLocationChanged(Location location)
|
||||||
|
public void onProviderDisabled(String provider)
|
||||||
|
public void onProviderEnabled(String provider)
|
||||||
|
public void onStatusChanged(String provider, int status, Bundle extras)
|
||||||
|
|
||||||
|
// Sensor callbacks
|
||||||
|
public void onSensorChanged(SensorEvent event)
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy)
|
||||||
|
|
||||||
|
// Bluetooth callbacks
|
||||||
|
public void onScanResult(int callbackType, ScanResult result)
|
||||||
|
public void onBatchScanResults(List<ScanResult> results)
|
||||||
|
public void onScanFailed(int errorCode)
|
||||||
|
|
||||||
|
// UI actions
|
||||||
|
private void startSearch()
|
||||||
|
private void stopSearch(String message)
|
||||||
|
private void tagItemHere()
|
||||||
|
private void startCalibration()
|
||||||
|
private void updateRadar()
|
||||||
|
private void updateRangeDisplay()
|
||||||
|
private void updateDistanceDisplay()
|
||||||
|
private void updateCalibrationDisplay()
|
||||||
|
private void updateSearchDisplay()
|
||||||
|
private void updateSearchStatus()
|
||||||
|
private void updateSearchProgress()
|
||||||
|
private void updateSearchResults()
|
||||||
|
private void updateSearchSettings()
|
||||||
|
private void updateSearchControls()
|
||||||
|
private void updateSearchUI()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MapActivity - Technical Details
|
||||||
|
|
||||||
|
### Implements
|
||||||
|
```java
|
||||||
|
implements LocationListener, SensorEventListener, ItemActionListener
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
protected void onCreate(Bundle state)
|
||||||
|
protected void onResume()
|
||||||
|
protected void onPause()
|
||||||
|
protected void onDestroy()
|
||||||
|
|
||||||
|
// Location callbacks
|
||||||
|
public void onLocationChanged(Location location)
|
||||||
|
public void onProviderDisabled(String provider)
|
||||||
|
public void onProviderEnabled(String provider)
|
||||||
|
public void onStatusChanged(String provider, int status, Bundle extras)
|
||||||
|
|
||||||
|
// Sensor callbacks
|
||||||
|
public void onSensorChanged(SensorEvent event)
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy)
|
||||||
|
|
||||||
|
// Map operations
|
||||||
|
private void updateMap()
|
||||||
|
private void updateMarkers()
|
||||||
|
private void updateTrails()
|
||||||
|
private void updateCamera()
|
||||||
|
private void updateLocation()
|
||||||
|
private void updateRange()
|
||||||
|
private void updateCalibration()
|
||||||
|
private void updateSearch()
|
||||||
|
private void updateSettings()
|
||||||
|
private void updateUI()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DeviceManagerActivity - Technical Details
|
||||||
|
|
||||||
|
### Key Methods
|
||||||
|
```java
|
||||||
|
protected void onCreate(Bundle state)
|
||||||
|
protected void onPause()
|
||||||
|
protected void onDestroy()
|
||||||
|
|
||||||
|
// Bluetooth scanning
|
||||||
|
private void startScan()
|
||||||
|
private void stopScan()
|
||||||
|
private void refreshCategoryOptions()
|
||||||
|
private void updateRadar()
|
||||||
|
|
||||||
|
// Device management
|
||||||
|
private void toggleMap(DeviceRecord record, Button button)
|
||||||
|
private void toggleTrack(DeviceRecord record, Button button)
|
||||||
|
private void editSignal(DeviceRecord record)
|
||||||
|
private void confirmRemoveSignal(SignalEntry entry)
|
||||||
|
private void copyText(String label, String text)
|
||||||
|
|
||||||
|
// Adapters
|
||||||
|
private class SignalGridAdapter extends BaseAdapter
|
||||||
|
private class PairedAdapter extends BaseAdapter
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BluetoothRadarView - Technical Details
|
||||||
|
|
||||||
|
### Extends
|
||||||
|
```java
|
||||||
|
extends View
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- Custom radar-like visualization
|
||||||
|
- Radial gradient rendering
|
||||||
|
- Touch event handling
|
||||||
|
- Signal strength visualization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### DeviceRecord
|
||||||
|
```java
|
||||||
|
public class DeviceRecord {
|
||||||
|
public String address
|
||||||
|
public String title
|
||||||
|
public String broadcast
|
||||||
|
public String category
|
||||||
|
public String vendor
|
||||||
|
public String color
|
||||||
|
public int trackingMinutes
|
||||||
|
public boolean mapEnabled
|
||||||
|
public boolean liveEnabled
|
||||||
|
public boolean trackBroadcast
|
||||||
|
public boolean showTrail
|
||||||
|
public boolean locationLocked
|
||||||
|
public int txPower
|
||||||
|
public double exponent
|
||||||
|
public long lastSeen
|
||||||
|
public long lastLocation
|
||||||
|
public int battery
|
||||||
|
public String photo
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LocationPoint
|
||||||
|
```java
|
||||||
|
public class LocationPoint {
|
||||||
|
public String address
|
||||||
|
public double latitude
|
||||||
|
public double longitude
|
||||||
|
public float accuracy
|
||||||
|
public long time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Android Components
|
||||||
|
|
||||||
|
### Permissions Required
|
||||||
|
- `BLUETOOTH` - Bluetooth scanning and discovery
|
||||||
|
- `BLUETOOTH_ADMIN` - Bluetooth administration
|
||||||
|
- `ACCESS_FINE_LOCATION` - Precise location access
|
||||||
|
- `ACCESS_COARSE_LOCATION` - Approximate location access
|
||||||
|
- `FOREGROUND_SERVICE` - Background service execution
|
||||||
|
- `CAMERA` - Device photo capture
|
||||||
|
- `RECEIVE_BOOT_COMPLETED` - Auto-start on boot
|
||||||
|
|
||||||
|
### Services
|
||||||
|
- `BluetoothTrackingService` - Foreground service for continuous tracking
|
||||||
|
- `LocationManager` - GPS and network location services
|
||||||
|
- `BluetoothAdapter` - Bluetooth scanning and discovery
|
||||||
|
|
||||||
|
### Broadcast Receivers
|
||||||
|
- `DATA_CHANGED` - Device data updates
|
||||||
|
- `CONFIG_CHANGED` - Configuration changes
|
||||||
|
- `RANGE_MODE` - Range finder mode changes
|
||||||
|
- `BOOT_COMPLETED` - System boot completion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signal Processing Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Bluetooth Scan → LeScanCallback → RSSI Reading
|
||||||
|
↓
|
||||||
|
BluetoothTrackingService
|
||||||
|
↓
|
||||||
|
captureFresh() → Fix()
|
||||||
|
↓
|
||||||
|
TrackerDatabase.recordObservation()
|
||||||
|
↓
|
||||||
|
SpatialGradientEngine Analysis
|
||||||
|
↓
|
||||||
|
RangeStateMachine State Update
|
||||||
|
↓
|
||||||
|
MapActivity / BluetoothRadarView Update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema (Version 15)
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
- `devices` - Device records and metadata
|
||||||
|
- `locations` - GPS location history
|
||||||
|
- `observations` - RSSI observations
|
||||||
|
- `telemetry` - Battery and vendor info
|
||||||
|
- `calibrations` - TX power and exponent
|
||||||
|
- `range_samples` - Range finding data
|
||||||
|
- `vendors` - Vendor lookup table
|
||||||
|
- `categories` - Device categories
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document generated from technical analysis of Java source files*
|
||||||
@@ -25,6 +25,10 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
public static final String ACTION_DATA_CHANGED = "com.wytehat.btlogger.DATA_CHANGED";
|
public static final String ACTION_DATA_CHANGED = "com.wytehat.btlogger.DATA_CHANGED";
|
||||||
public static final String ACTION_CONFIG_CHANGED = "com.wytehat.btlogger.CONFIG_CHANGED";
|
public static final String ACTION_CONFIG_CHANGED = "com.wytehat.btlogger.CONFIG_CHANGED";
|
||||||
public static final String ACTION_RANGE_MODE = "com.wytehat.btlogger.RANGE_MODE";
|
public static final String ACTION_RANGE_MODE = "com.wytehat.btlogger.RANGE_MODE";
|
||||||
|
public static final String ACTION_ACK_LEFT_BEHIND = "com.wytehat.btlogger.ACK_LEFT_BEHIND";
|
||||||
|
public static final String EXTRA_ADDRESS = "address";
|
||||||
|
/** How often an unacknowledged left-behind item re-alerts. */
|
||||||
|
private static final long ALERT_REPEAT_MS = 60000L;
|
||||||
private static final int NOTIFICATION_ID = 42;
|
private static final int NOTIFICATION_ID = 42;
|
||||||
private static final String CHANNEL_ID = "bluetooth_tracking";
|
private static final String CHANNEL_ID = "bluetooth_tracking";
|
||||||
private static final String ALERT_CHANNEL_ID = "tracked_item_disconnects";
|
private static final String ALERT_CHANNEL_ID = "tracked_item_disconnects";
|
||||||
@@ -37,6 +41,9 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
private int phoneBattery = 100;
|
private int phoneBattery = 100;
|
||||||
private final Handler handler = new Handler();
|
private final Handler handler = new Handler();
|
||||||
private final HashMap<String, Long> lastPeriodicLog = new HashMap<String, Long>();
|
private final HashMap<String, Long> lastPeriodicLog = new HashMap<String, Long>();
|
||||||
|
/** Items reported left behind, address -> display name, until acknowledged. */
|
||||||
|
private final HashMap<String, String> leftBehind = new HashMap<String, String>();
|
||||||
|
private boolean alertLoopRunning;
|
||||||
|
|
||||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
@@ -45,6 +52,10 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
rangeModeActive = intent.getBooleanExtra("active", false);
|
rangeModeActive = intent.getBooleanExtra("active", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ACTION_ACK_LEFT_BEHIND.equals(action)) {
|
||||||
|
acknowledgeLeftBehind(intent.getStringExtra(EXTRA_ADDRESS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (ACTION_CONFIG_CHANGED.equals(action)) {
|
if (ACTION_CONFIG_CHANGED.equals(action)) {
|
||||||
restartBackgroundScan();
|
restartBackgroundScan();
|
||||||
configureLocation();
|
configureLocation();
|
||||||
@@ -84,7 +95,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
DeviceRecord previous = db.get(address);
|
DeviceRecord previous = db.get(address);
|
||||||
db.disconnected(address, broadcastName, now, fix, infer(previous));
|
db.disconnected(address, broadcastName, now, fix, infer(previous));
|
||||||
if (previous != null && previous.tracked == 1 && previous.connected == 1)
|
if (previous != null && previous.tracked == 1 && previous.connected == 1)
|
||||||
showDisconnectAlert(previous, infer(previous));
|
markLeftBehind(previous, infer(previous));
|
||||||
captureFresh(address, now);
|
captureFresh(address, now);
|
||||||
configureLocation();
|
configureLocation();
|
||||||
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
|
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
|
||||||
@@ -175,6 +186,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||||
filter.addAction(ACTION_CONFIG_CHANGED);
|
filter.addAction(ACTION_CONFIG_CHANGED);
|
||||||
filter.addAction(ACTION_RANGE_MODE);
|
filter.addAction(ACTION_RANGE_MODE);
|
||||||
|
filter.addAction(ACTION_ACK_LEFT_BEHIND);
|
||||||
registerReceiver(receiver, filter);
|
registerReceiver(receiver, filter);
|
||||||
receiverRegistered = true;
|
receiverRegistered = true;
|
||||||
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||||
@@ -192,6 +204,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
|
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
handler.removeCallbacks(scanTask);
|
handler.removeCallbacks(scanTask);
|
||||||
|
handler.removeCallbacks(alertTask);
|
||||||
try { if (bluetooth != null) bluetooth.stopLeScan(leScanCallback); }
|
try { if (bluetooth != null) bluetooth.stopLeScan(leScanCallback); }
|
||||||
catch (Exception ignored) { }
|
catch (Exception ignored) { }
|
||||||
if (receiverRegistered) unregisterReceiver(receiver);
|
if (receiverRegistered) unregisterReceiver(receiver);
|
||||||
@@ -418,16 +431,99 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
return first.getTime() >= second.getTime() ? first : second;
|
return first.getTime() >= second.getTime() ? first : second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FLAG_IMMUTABLE (API 23+) is referenced by value because this module
|
||||||
|
* still compiles against SDK 21.
|
||||||
|
*/
|
||||||
|
private int pendingFlags() {
|
||||||
|
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) flags |= 0x04000000;
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int alertId(String address) {
|
||||||
|
return 1000 + Math.abs(address.hashCode() % 100000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PendingIntent acknowledgeIntent(String address) {
|
||||||
|
Intent ack = new Intent(ACTION_ACK_LEFT_BEHIND).setPackage(getPackageName());
|
||||||
|
if (address != null) ack.putExtra(EXTRA_ADDRESS, address);
|
||||||
|
return PendingIntent.getBroadcast(this,
|
||||||
|
address == null ? 0 : alertId(address), ack, pendingFlags());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the item as left behind and keeps alerting until the user
|
||||||
|
* taps "Acknowledge Left Behind" - a swipe alone does not clear it.
|
||||||
|
*/
|
||||||
|
private void markLeftBehind(DeviceRecord record, String reason) {
|
||||||
|
if (record == null || record.address == null) return;
|
||||||
|
leftBehind.put(record.address, record.displayName());
|
||||||
|
showDisconnectAlert(record, reason);
|
||||||
|
updateForegroundNotification();
|
||||||
|
startAlertLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acknowledgeLeftBehind(String address) {
|
||||||
|
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
if (address == null) {
|
||||||
|
for (String pending : leftBehind.keySet().toArray(new String[0])) {
|
||||||
|
try { manager.cancel(alertId(pending)); } catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
leftBehind.clear();
|
||||||
|
} else {
|
||||||
|
leftBehind.remove(address);
|
||||||
|
try { manager.cancel(alertId(address)); } catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
if (leftBehind.isEmpty()) {
|
||||||
|
handler.removeCallbacks(alertTask);
|
||||||
|
alertLoopRunning = false;
|
||||||
|
}
|
||||||
|
updateForegroundNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startAlertLoop() {
|
||||||
|
if (alertLoopRunning) return;
|
||||||
|
alertLoopRunning = true;
|
||||||
|
handler.postDelayed(alertTask, ALERT_REPEAT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-fires every outstanding alert until each one is acknowledged. */
|
||||||
|
private final Runnable alertTask = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
if (leftBehind.isEmpty()) { alertLoopRunning = false; return; }
|
||||||
|
for (String address : leftBehind.keySet().toArray(new String[0])) {
|
||||||
|
DeviceRecord record = db.get(address);
|
||||||
|
if (record == null) continue;
|
||||||
|
showDisconnectAlert(record, "Still left behind - not acknowledged");
|
||||||
|
}
|
||||||
|
updateForegroundNotification();
|
||||||
|
handler.postDelayed(this, ALERT_REPEAT_MS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void updateForegroundNotification() {
|
||||||
|
try {
|
||||||
|
NotificationManager manager =
|
||||||
|
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||||
|
manager.notify(NOTIFICATION_ID, buildNotification());
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
private void showDisconnectAlert(DeviceRecord record, String reason) {
|
private void showDisconnectAlert(DeviceRecord record, String reason) {
|
||||||
Intent open = new Intent(this, MainActivity.class);
|
Intent open = new Intent(this, MainActivity.class);
|
||||||
PendingIntent pending = PendingIntent.getActivity(this, record.address.hashCode(), open,
|
PendingIntent pending = PendingIntent.getActivity(this, record.address.hashCode(), open,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
Notification.Builder builder = new Notification.Builder(this)
|
Notification.Builder builder = new Notification.Builder(this)
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.setContentTitle(record.displayName() + " disconnected")
|
.setContentTitle(record.displayName() + " left behind")
|
||||||
.setContentText(reason == null ? "Your tracked item is no longer connected" : reason)
|
.setContentText(reason == null ? "Your tracked item is no longer connected" : reason)
|
||||||
.setContentIntent(pending).setAutoCancel(true)
|
.setContentIntent(pending).setAutoCancel(true)
|
||||||
.setDefaults(Notification.DEFAULT_ALL).setPriority(Notification.PRIORITY_HIGH);
|
.setDefaults(Notification.DEFAULT_ALL).setPriority(Notification.PRIORITY_HIGH);
|
||||||
|
if (Build.VERSION.SDK_INT >= 16) {
|
||||||
|
builder.addAction(0, "Acknowledge Left Behind",
|
||||||
|
acknowledgeIntent(record.address));
|
||||||
|
}
|
||||||
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
try {
|
try {
|
||||||
@@ -445,12 +541,24 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
|
|
||||||
private Notification buildNotification() {
|
private Notification buildNotification() {
|
||||||
Intent open = new Intent(this, MainActivity.class);
|
Intent open = new Intent(this, MainActivity.class);
|
||||||
PendingIntent pending = PendingIntent.getActivity(this, 0, open,
|
PendingIntent pending = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
int outstanding = leftBehind.size();
|
||||||
|
String title = outstanding > 0
|
||||||
|
? outstanding + (outstanding == 1 ? " item left behind" : " items left behind")
|
||||||
|
: "Bluetooth Item Finder";
|
||||||
|
String text = outstanding > 0
|
||||||
|
? "Alerting until acknowledged: " + describeLeftBehind()
|
||||||
|
: "Watching your tracked items";
|
||||||
Notification.Builder builder = new Notification.Builder(this)
|
Notification.Builder builder = new Notification.Builder(this)
|
||||||
.setSmallIcon(R.drawable.ic_launcher).setContentTitle("Bluetooth Item Finder")
|
.setSmallIcon(R.drawable.ic_launcher).setContentTitle(title)
|
||||||
.setContentText("Watching your tracked items").setOngoing(true)
|
.setContentText(text).setOngoing(true)
|
||||||
.setContentIntent(pending);
|
.setContentIntent(pending);
|
||||||
|
if (Build.VERSION.SDK_INT >= 16 && outstanding > 0) {
|
||||||
|
builder.addAction(0,
|
||||||
|
outstanding == 1 ? "Acknowledge Left Behind" : "Acknowledge All",
|
||||||
|
acknowledgeIntent(outstanding == 1
|
||||||
|
? leftBehind.keySet().iterator().next() : null));
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
try {
|
try {
|
||||||
Class<?> channelClass = Class.forName("android.app.NotificationChannel");
|
Class<?> channelClass = Class.forName("android.app.NotificationChannel");
|
||||||
@@ -467,5 +575,14 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
|||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String describeLeftBehind() {
|
||||||
|
StringBuilder names = new StringBuilder();
|
||||||
|
for (String name : leftBehind.values()) {
|
||||||
|
if (names.length() > 0) names.append(", ");
|
||||||
|
names.append(name);
|
||||||
|
}
|
||||||
|
return names.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.wytehat.btlogger;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordering for the tracked item list.
|
||||||
|
*
|
||||||
|
* Default order is newest sighting first. Items that have not been seen for
|
||||||
|
* a long time - or that carry no sighting timestamp at all - are pushed to
|
||||||
|
* the bottom and stay there in both sort directions, so the reverse toggle
|
||||||
|
* never buries a live item under a pile of stale ones.
|
||||||
|
*/
|
||||||
|
final class DeviceSort {
|
||||||
|
|
||||||
|
/** A device unseen for longer than this sinks to the bottom. */
|
||||||
|
static final long STALE_AFTER_MS = 24L * 60L * 60L * 1000L;
|
||||||
|
|
||||||
|
private DeviceSort() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last moment the device was actually observed. Deliberately ignores
|
||||||
|
* updatedAt, which also moves when the user edits a name or colour.
|
||||||
|
*/
|
||||||
|
static long lastSeen(DeviceRecord record) {
|
||||||
|
|
||||||
|
if (record == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
long seen = record.lastRssiAt;
|
||||||
|
|
||||||
|
if (record.connectedAt > seen) {
|
||||||
|
seen = record.connectedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.disconnectedAt > seen) {
|
||||||
|
seen = record.disconnectedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isStale(DeviceRecord record, long now) {
|
||||||
|
|
||||||
|
long seen = lastSeen(record);
|
||||||
|
|
||||||
|
return seen <= 0L ||
|
||||||
|
now - seen > STALE_AFTER_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param reverse false = newest first (default), true = oldest first.
|
||||||
|
*/
|
||||||
|
static List<DeviceRecord> sorted(List<DeviceRecord> rows, boolean reverse) {
|
||||||
|
|
||||||
|
ArrayList<DeviceRecord> fresh = new ArrayList<DeviceRecord>();
|
||||||
|
ArrayList<DeviceRecord> stale = new ArrayList<DeviceRecord>();
|
||||||
|
|
||||||
|
if (rows == null) {
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
|
||||||
|
DeviceRecord record = rows.get(i);
|
||||||
|
|
||||||
|
if (record == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStale(record, now)) {
|
||||||
|
stale.add(record);
|
||||||
|
} else {
|
||||||
|
fresh.add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparator<DeviceRecord> newestFirst =
|
||||||
|
new Comparator<DeviceRecord>() {
|
||||||
|
|
||||||
|
public int compare(DeviceRecord left, DeviceRecord right) {
|
||||||
|
|
||||||
|
long a = lastSeen(left);
|
||||||
|
long b = lastSeen(right);
|
||||||
|
|
||||||
|
if (a == b) {
|
||||||
|
return left.displayName()
|
||||||
|
.compareToIgnoreCase(right.displayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return a > b ? -1 : 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Collections.sort(fresh, newestFirst);
|
||||||
|
Collections.sort(stale, newestFirst);
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
Collections.reverse(fresh);
|
||||||
|
Collections.reverse(stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<DeviceRecord> result =
|
||||||
|
new ArrayList<DeviceRecord>(fresh.size() + stale.size());
|
||||||
|
|
||||||
|
result.addAll(fresh);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stale entries are appended last in both directions.
|
||||||
|
*/
|
||||||
|
result.addAll(stale);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ public class MainActivity extends Activity implements ItemActionListener {
|
|||||||
private static final int CAMERA = 200;
|
private static final int CAMERA = 200;
|
||||||
private TrackerDatabase db;
|
private TrackerDatabase db;
|
||||||
private DeviceListAdapter adapter;
|
private DeviceListAdapter adapter;
|
||||||
|
private boolean reverseSort;
|
||||||
private boolean registered;
|
private boolean registered;
|
||||||
private DeviceRecord editing;
|
private DeviceRecord editing;
|
||||||
private final Handler refreshHandler = new Handler();
|
private final Handler refreshHandler = new Handler();
|
||||||
@@ -46,6 +47,18 @@ public class MainActivity extends Activity implements ItemActionListener {
|
|||||||
public void onClick(View v) { startActivity(new Intent(MainActivity.this,
|
public void onClick(View v) { startActivity(new Intent(MainActivity.this,
|
||||||
MapActivity.class)); }
|
MapActivity.class)); }
|
||||||
});
|
});
|
||||||
|
reverseSort = getSharedPreferences("list_settings", 0)
|
||||||
|
.getBoolean("reverse_sort", false);
|
||||||
|
updateSortButton();
|
||||||
|
findViewById(R.id.sort_button).setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
reverseSort = !reverseSort;
|
||||||
|
getSharedPreferences("list_settings", 0).edit()
|
||||||
|
.putBoolean("reverse_sort", reverseSort).apply();
|
||||||
|
updateSortButton();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
findViewById(R.id.settings_button).setOnClickListener(new View.OnClickListener() {
|
findViewById(R.id.settings_button).setOnClickListener(new View.OnClickListener() {
|
||||||
public void onClick(View v) { showSettings(); }
|
public void onClick(View v) { showSettings(); }
|
||||||
});
|
});
|
||||||
@@ -347,7 +360,21 @@ public class MainActivity extends Activity implements ItemActionListener {
|
|||||||
PackageManager.PERMISSION_GRANTED) list.add(permission);
|
PackageManager.PERMISSION_GRANTED) list.add(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reload() { if (adapter != null) adapter.setRows(db.tracked()); }
|
private void reload() {
|
||||||
|
if (adapter == null) return;
|
||||||
|
adapter.setRows(DeviceSort.sorted(db.tracked(), reverseSort));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Stale items stay pinned to the bottom in both directions, so the
|
||||||
|
* label only describes how the recently seen items are ordered.
|
||||||
|
*/
|
||||||
|
private void updateSortButton() {
|
||||||
|
android.widget.Button button =
|
||||||
|
(android.widget.Button) findViewById(R.id.sort_button);
|
||||||
|
if (button == null) return;
|
||||||
|
button.setText(reverseSort ? "Oldest first" : "Newest first");
|
||||||
|
}
|
||||||
|
|
||||||
private void startTracker() {
|
private void startTracker() {
|
||||||
Intent intent = new Intent(this, BluetoothTrackingService.class);
|
Intent intent = new Intent(this, BluetoothTrackingService.class);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
package com.wytehat.btlogger;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
import android.app.TimePickerDialog;
|
import android.app.TimePickerDialog;
|
||||||
@@ -14,6 +16,8 @@ import android.location.LocationListener;
|
|||||||
import android.location.LocationManager;
|
import android.location.LocationManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
@@ -22,6 +26,7 @@ import android.widget.ArrayAdapter;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@@ -32,6 +37,8 @@ import java.util.Date;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class MapActivity extends Activity
|
public class MapActivity extends Activity
|
||||||
implements SensorEventListener {
|
implements SensorEventListener {
|
||||||
@@ -48,8 +55,11 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
private LinearLayout filterPanel;
|
private LinearLayout filterPanel;
|
||||||
|
|
||||||
|
|
||||||
|
private ScrollView filterScroll;
|
||||||
private Spinner categorySpinner;
|
private Spinner categorySpinner;
|
||||||
private Spinner deviceSpinner;
|
private Spinner deviceSpinner;
|
||||||
|
private Spinner resolutionSpinner;
|
||||||
|
|
||||||
private CheckBox showAll;
|
private CheckBox showAll;
|
||||||
|
|
||||||
@@ -68,15 +78,54 @@ implements SensorEventListener {
|
|||||||
private final ArrayList<String> deviceLabels =
|
private final ArrayList<String> deviceLabels =
|
||||||
new ArrayList<String>();
|
new ArrayList<String>();
|
||||||
|
|
||||||
private String categoryFilter = "Trackers / Tags";
|
private final ArrayList<String> resolutionLabels =
|
||||||
private String deviceFilter = "All";
|
new ArrayList<String>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 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 rebuildingCategories;
|
||||||
private boolean rebuildingDevices;
|
private boolean rebuildingDevices;
|
||||||
private boolean loadingMap;
|
private boolean rebuildingResolution = true;
|
||||||
|
private volatile boolean loadingMap;
|
||||||
|
|
||||||
private long historyFrom;
|
private volatile long historyFrom;
|
||||||
private long historyTo;
|
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 LocationManager locationManager;
|
private LocationManager locationManager;
|
||||||
private SensorManager sensorManager;
|
private SensorManager sensorManager;
|
||||||
@@ -207,8 +256,6 @@ implements SensorEventListener {
|
|||||||
dp(4)
|
dp(4)
|
||||||
);
|
);
|
||||||
|
|
||||||
filterPanel.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Category.
|
* Category.
|
||||||
*/
|
*/
|
||||||
@@ -272,6 +319,66 @@ implements SensorEventListener {
|
|||||||
compactParams()
|
compactParams()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Trail resolution (downsampling).
|
||||||
|
*/
|
||||||
|
LinearLayout resolutionRow =
|
||||||
|
new LinearLayout(this);
|
||||||
|
|
||||||
|
resolutionRow.setOrientation(
|
||||||
|
LinearLayout.HORIZONTAL
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionRow.setGravity(
|
||||||
|
android.view.Gravity.CENTER_VERTICAL
|
||||||
|
);
|
||||||
|
|
||||||
|
TextView resolutionLabel =
|
||||||
|
smallLabel("Trail detail");
|
||||||
|
|
||||||
|
resolutionRow.addView(
|
||||||
|
resolutionLabel,
|
||||||
|
new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
);
|
||||||
|
resolutionSpinner = new Spinner(this);
|
||||||
|
|
||||||
|
for (int i = 0;
|
||||||
|
i < RESOLUTION_LABELS.length;
|
||||||
|
i++) {
|
||||||
|
|
||||||
|
resolutionLabels.add(
|
||||||
|
RESOLUTION_LABELS[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter<String> resolutionAdapter =
|
||||||
|
new ArrayAdapter<String>(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_spinner_item,
|
||||||
|
resolutionLabels
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionAdapter.setDropDownViewResource(
|
||||||
|
android.R.layout.simple_spinner_dropdown_item
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionSpinner.setAdapter(
|
||||||
|
resolutionAdapter
|
||||||
|
);
|
||||||
|
|
||||||
|
resolutionRow.addView(
|
||||||
|
resolutionSpinner,
|
||||||
|
new LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
dp(38),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
filterPanel.addView(resolutionRow);
|
||||||
/*
|
/*
|
||||||
* Time.
|
* Time.
|
||||||
*/
|
*/
|
||||||
@@ -387,14 +494,30 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
filterPanel.addView(showAll);
|
filterPanel.addView(showAll);
|
||||||
|
|
||||||
root.addView(
|
/*
|
||||||
|
* The panel is scrollable and bounded to a third of the
|
||||||
|
* screen so every control stays reachable on short screens.
|
||||||
|
*/
|
||||||
|
filterScroll = new ScrollView(this);
|
||||||
|
|
||||||
|
filterScroll.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
filterScroll.addView(
|
||||||
filterPanel,
|
filterPanel,
|
||||||
new LinearLayout.LayoutParams(
|
new android.widget.FrameLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
root.addView(
|
||||||
|
filterScroll,
|
||||||
|
new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
/*
|
/*
|
||||||
* WebView map.
|
* WebView map.
|
||||||
*/
|
*/
|
||||||
@@ -441,7 +564,7 @@ implements SensorEventListener {
|
|||||||
new LinearLayout.LayoutParams(
|
new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
0,
|
0,
|
||||||
1
|
2
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -476,19 +599,12 @@ implements SensorEventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
|
|
||||||
if (filterPanel.getVisibility()
|
filterScroll.setVisibility(
|
||||||
== View.VISIBLE) {
|
filterScroll.getVisibility()
|
||||||
|
== View.VISIBLE
|
||||||
filterPanel.setVisibility(
|
? View.GONE
|
||||||
View.GONE
|
: View.VISIBLE
|
||||||
);
|
);
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
filterPanel.setVisibility(
|
|
||||||
View.VISIBLE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -565,6 +681,50 @@ 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() {
|
new View.OnClickListener() {
|
||||||
|
|
||||||
@@ -646,7 +806,7 @@ implements SensorEventListener {
|
|||||||
/*
|
/*
|
||||||
* Collapse after applying.
|
* Collapse after applying.
|
||||||
*/
|
*/
|
||||||
filterPanel.setVisibility(
|
filterScroll.setVisibility(
|
||||||
View.GONE
|
View.GONE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -679,6 +839,8 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
updateTimeButtons();
|
updateTimeButtons();
|
||||||
|
|
||||||
|
rebuildingResolution = false;
|
||||||
|
|
||||||
rebuildDeviceFilter();
|
rebuildDeviceFilter();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -891,8 +1053,24 @@ implements SensorEventListener {
|
|||||||
|
|
||||||
private void rebuildDeviceFilter() {
|
private void rebuildDeviceFilter() {
|
||||||
|
|
||||||
List<DeviceRecord> available =
|
rebuildDeviceFilter(
|
||||||
getMapDevices();
|
getMapDevices(
|
||||||
|
showAll.isChecked()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Main thread only. Accepts an already loaded device list so the
|
||||||
|
* background loader does not repeat the query.
|
||||||
|
*/
|
||||||
|
private void rebuildDeviceFilter(
|
||||||
|
List<DeviceRecord> available) {
|
||||||
|
|
||||||
|
if (available == null) {
|
||||||
|
available =
|
||||||
|
new ArrayList<DeviceRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<String> nextAddresses =
|
ArrayList<String> nextAddresses =
|
||||||
new ArrayList<String>();
|
new ArrayList<String>();
|
||||||
@@ -991,10 +1169,19 @@ implements SensorEventListener {
|
|||||||
rebuildingDevices = false;
|
rebuildingDevices = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DeviceRecord> getMapDevices() {
|
/*
|
||||||
|
* Safe to call from a worker thread: the caller passes the checkbox
|
||||||
|
* state in instead of reading the view here.
|
||||||
|
*/
|
||||||
|
private List<DeviceRecord> getMapDevices(
|
||||||
|
boolean includeAll) {
|
||||||
|
|
||||||
|
TrackerDatabase database = db;
|
||||||
|
|
||||||
List<DeviceRecord> all =
|
List<DeviceRecord> all =
|
||||||
db.all();
|
database == null
|
||||||
|
? null
|
||||||
|
: database.all();
|
||||||
|
|
||||||
ArrayList<DeviceRecord> result =
|
ArrayList<DeviceRecord> result =
|
||||||
new ArrayList<DeviceRecord>();
|
new ArrayList<DeviceRecord>();
|
||||||
@@ -1018,7 +1205,7 @@ implements SensorEventListener {
|
|||||||
* Normal map:
|
* Normal map:
|
||||||
* only explicitly map-enabled devices.
|
* only explicitly map-enabled devices.
|
||||||
*/
|
*/
|
||||||
if (!showAll.isChecked() &&
|
if (!includeAll &&
|
||||||
record.mapEnabled != 1) {
|
record.mapEnabled != 1) {
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -1027,7 +1214,7 @@ implements SensorEventListener {
|
|||||||
/*
|
/*
|
||||||
* Normal map excludes unknown entries.
|
* Normal map excludes unknown entries.
|
||||||
*/
|
*/
|
||||||
if (!showAll.isChecked() &&
|
if (!includeAll &&
|
||||||
isUnknown(record)) {
|
isUnknown(record)) {
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -1218,20 +1405,42 @@ implements SensorEventListener {
|
|||||||
loadingMap = true;
|
loadingMap = true;
|
||||||
mapReady = false;
|
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();
|
||||||
|
|
||||||
|
mapExecutor.execute(
|
||||||
|
new Runnable() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
List<DeviceRecord> available = null;
|
||||||
|
String html = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
List<DeviceRecord> available =
|
available =
|
||||||
getMapDevices();
|
getMapDevices(includeAll);
|
||||||
|
|
||||||
refreshMapCategories(
|
String device = deviceFilter;
|
||||||
available
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Rebuild device list after category
|
* The selected device may have gone away since
|
||||||
* changes.
|
* the last load; fall back to showing all.
|
||||||
*/
|
*/
|
||||||
rebuildDeviceFilter();
|
if (!"All".equals(device) &&
|
||||||
|
!containsAddress(
|
||||||
|
available,
|
||||||
|
device
|
||||||
|
)) {
|
||||||
|
|
||||||
|
device = "All";
|
||||||
|
}
|
||||||
|
|
||||||
ArrayList<DeviceRecord> rows =
|
ArrayList<DeviceRecord> rows =
|
||||||
new ArrayList<DeviceRecord>();
|
new ArrayList<DeviceRecord>();
|
||||||
@@ -1252,8 +1461,8 @@ implements SensorEventListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"All".equals(deviceFilter) &&
|
if (!"All".equals(device) &&
|
||||||
!deviceFilter.equals(
|
!device.equals(
|
||||||
record.address
|
record.address
|
||||||
)) {
|
)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1262,29 +1471,100 @@ implements SensorEventListener {
|
|||||||
rows.add(record);
|
rows.add(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
String html =
|
html =
|
||||||
buildHtml(
|
buildHtml(
|
||||||
rows,
|
rows,
|
||||||
focus
|
focus,
|
||||||
|
device
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<DeviceRecord> 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(
|
web.loadDataWithBaseURL(
|
||||||
"https://unpkg.com/",
|
"https://unpkg.com/",
|
||||||
html,
|
content,
|
||||||
"text/html",
|
"text/html",
|
||||||
"UTF-8",
|
"UTF-8",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
loadingMap = false;
|
loadingMap = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsAddress(
|
||||||
|
List<DeviceRecord> 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(
|
private String buildHtml(
|
||||||
List<DeviceRecord> rows,
|
List<DeviceRecord> rows,
|
||||||
String selectedAddress) {
|
String selectedAddress,
|
||||||
|
String device) {
|
||||||
|
|
||||||
StringBuilder script =
|
StringBuilder script =
|
||||||
new StringBuilder();
|
new StringBuilder();
|
||||||
@@ -1293,7 +1573,7 @@ implements SensorEventListener {
|
|||||||
* Historical data is loaded ONLY for a
|
* Historical data is loaded ONLY for a
|
||||||
* specifically selected device.
|
* specifically selected device.
|
||||||
*/
|
*/
|
||||||
if (!"All".equals(deviceFilter)) {
|
if (!"All".equals(device)) {
|
||||||
|
|
||||||
for (int i = 0;
|
for (int i = 0;
|
||||||
i < rows.size();
|
i < rows.size();
|
||||||
@@ -1302,19 +1582,34 @@ implements SensorEventListener {
|
|||||||
DeviceRecord record =
|
DeviceRecord record =
|
||||||
rows.get(i);
|
rows.get(i);
|
||||||
|
|
||||||
if (!deviceFilter.equals(
|
if (!device.equals(
|
||||||
record.address
|
record.address
|
||||||
)) {
|
)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackerDatabase database = db;
|
||||||
|
|
||||||
List<LocationPoint> history =
|
List<LocationPoint> history =
|
||||||
db.history(
|
database == null
|
||||||
|
? null
|
||||||
|
: database.history(
|
||||||
record.address,
|
record.address,
|
||||||
historyFrom,
|
historyFrom,
|
||||||
historyTo
|
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,
|
script,
|
||||||
record,
|
record,
|
||||||
@@ -2332,6 +2627,13 @@ implements SensorEventListener {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
|
||||||
|
destroyed = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mapExecutor.shutdownNow();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (web != null) {
|
if (web != null) {
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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<LocationPoint> sample(List<LocationPoint> points, long bucketMillis) {
|
||||||
|
|
||||||
|
if (points == null || points.size() < 3) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocationPoint> reduced =
|
||||||
|
bucketMillis > 0
|
||||||
|
? bucket(points, bucketMillis)
|
||||||
|
: points;
|
||||||
|
|
||||||
|
return cap(reduced);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LocationPoint> bucket(List<LocationPoint> points, long bucketMillis) {
|
||||||
|
|
||||||
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
|
|
||||||
|
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<LocationPoint> cap(List<LocationPoint> points) {
|
||||||
|
|
||||||
|
int size = points.size();
|
||||||
|
|
||||||
|
if (size <= MAX_RENDERED_POINTS) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stride = (size + MAX_RENDERED_POINTS - 1) / MAX_RENDERED_POINTS;
|
||||||
|
|
||||||
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,25 @@ public class TrackerDatabase extends SQLiteOpenHelper {
|
|||||||
super(context, DB_NAME, null, DB_VERSION);
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<LocationPoint> history(String address, long historyFrom, long historyTo)
|
public synchronized List<LocationPoint> history(String address, long historyFrom, long historyTo)
|
||||||
{
|
{
|
||||||
// TODO: Implement this method
|
ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
|
||||||
return null;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
<Button android:id="@+id/map_all_button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="MAP"/>
|
<Button android:id="@+id/map_all_button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="MAP"/>
|
||||||
<Button android:id="@+id/settings_button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="SETTINGS"/>
|
<Button android:id="@+id/settings_button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="SETTINGS"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<TextView android:id="@+id/tracking_status" android:text="@string/tracking_status" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="12dp" android:paddingBottom="8dp" android:textColor="#2878ff"/>
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
|
||||||
|
<TextView android:id="@+id/tracking_status" android:text="@string/tracking_status" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:paddingLeft="12dp" android:paddingBottom="8dp" android:textColor="#2878ff"/>
|
||||||
|
<Button android:id="@+id/sort_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Newest first" android:textSize="10sp" android:layout_marginRight="8dp"/>
|
||||||
|
</LinearLayout>
|
||||||
<ListView android:id="@+id/device_list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:dividerHeight="10dp" android:padding="8dp"/>
|
<ListView android:id="@+id/device_list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:dividerHeight="10dp" android:padding="8dp"/>
|
||||||
<TextView android:id="@+id/empty_view" android:text="No tracked items yet. Use ADD / PAIR to choose an item." android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"/>
|
<TextView android:id="@+id/empty_view" android:text="No tracked items yet. Use ADD / PAIR to choose an item." android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user