From 841da5cd7a6fae380ea1b6f25d93479c03a0dcd9 Mon Sep 17 00:00:00 2001 From: Jaakko Malkki Date: Mon, 14 Oct 2024 18:01:33 +0300 Subject: [PATCH] Do not include data older than 30 seconds in the reports --- .../neostumbler/domain/BluetoothBeacon.kt | 4 +- .../malkki/neostumbler/domain/CellTower.kt | 4 +- .../neostumbler/domain/ObservedDevice.kt | 11 +++ .../neostumbler/domain/WifiAccessPoint.kt | 4 +- .../neostumbler/scanner/WirelessScanner.kt | 94 +++++++++---------- .../scanner/WirelessScannerTest.kt | 46 +++++++++ 6 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/xyz/malkki/neostumbler/domain/ObservedDevice.kt diff --git a/app/src/main/java/xyz/malkki/neostumbler/domain/BluetoothBeacon.kt b/app/src/main/java/xyz/malkki/neostumbler/domain/BluetoothBeacon.kt index 557c9138..93a5f9bb 100644 --- a/app/src/main/java/xyz/malkki/neostumbler/domain/BluetoothBeacon.kt +++ b/app/src/main/java/xyz/malkki/neostumbler/domain/BluetoothBeacon.kt @@ -10,8 +10,8 @@ data class BluetoothBeacon( val id2: String?, val id3: String?, val signalStrength: Int, - val timestamp: Long -) { + override val timestamp: Long +) : ObservedDevice { companion object { fun fromBeacon(beacon: Beacon): BluetoothBeacon { val timestamp = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - beacon.lastCycleDetectionTimestamp) diff --git a/app/src/main/java/xyz/malkki/neostumbler/domain/CellTower.kt b/app/src/main/java/xyz/malkki/neostumbler/domain/CellTower.kt index 9b8fb4c8..2ccf5a81 100644 --- a/app/src/main/java/xyz/malkki/neostumbler/domain/CellTower.kt +++ b/app/src/main/java/xyz/malkki/neostumbler/domain/CellTower.kt @@ -25,8 +25,8 @@ data class CellTower( /** * Timestamp when the cell tower was observed in milliseconds since boot */ - val timestamp: Long -) { + override val timestamp: Long +) : ObservedDevice { companion object { private fun CellInfo.serving(): Int? { if (cellConnectionStatus == CellInfo.CONNECTION_UNKNOWN) { diff --git a/app/src/main/java/xyz/malkki/neostumbler/domain/ObservedDevice.kt b/app/src/main/java/xyz/malkki/neostumbler/domain/ObservedDevice.kt new file mode 100644 index 00000000..a0fb0ba5 --- /dev/null +++ b/app/src/main/java/xyz/malkki/neostumbler/domain/ObservedDevice.kt @@ -0,0 +1,11 @@ +package xyz.malkki.neostumbler.domain + +/** + * Interface for observations of wireless devices + */ +interface ObservedDevice { + /** + * Timestamp when the observation was made + */ + val timestamp: Long +} \ No newline at end of file diff --git a/app/src/main/java/xyz/malkki/neostumbler/domain/WifiAccessPoint.kt b/app/src/main/java/xyz/malkki/neostumbler/domain/WifiAccessPoint.kt index a8a90ad6..e7ecb193 100644 --- a/app/src/main/java/xyz/malkki/neostumbler/domain/WifiAccessPoint.kt +++ b/app/src/main/java/xyz/malkki/neostumbler/domain/WifiAccessPoint.kt @@ -12,8 +12,8 @@ data class WifiAccessPoint( val frequency: Int?, val signalStrength: Int?, val ssid: String?, - val timestamp: Long -) { + override val timestamp: Long +) : ObservedDevice { companion object { fun fromScanResult(scanResult: ScanResult): WifiAccessPoint { val radioType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/app/src/main/java/xyz/malkki/neostumbler/scanner/WirelessScanner.kt b/app/src/main/java/xyz/malkki/neostumbler/scanner/WirelessScanner.kt index 8c7c9552..ea1f666a 100644 --- a/app/src/main/java/xyz/malkki/neostumbler/scanner/WirelessScanner.kt +++ b/app/src/main/java/xyz/malkki/neostumbler/scanner/WirelessScanner.kt @@ -1,5 +1,6 @@ package xyz.malkki.neostumbler.scanner +import android.location.Location import android.os.SystemClock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -22,6 +23,7 @@ import xyz.malkki.neostumbler.common.LocationWithSource import xyz.malkki.neostumbler.domain.AirPressureObservation import xyz.malkki.neostumbler.domain.BluetoothBeacon import xyz.malkki.neostumbler.domain.CellTower +import xyz.malkki.neostumbler.domain.ObservedDevice import xyz.malkki.neostumbler.domain.Position import xyz.malkki.neostumbler.domain.WifiAccessPoint import xyz.malkki.neostumbler.extensions.combineWithLatestFrom @@ -40,8 +42,8 @@ private const val LOCATION_MAX_ACCURACY = 200 //Maximum age for locations private val LOCATION_MAX_AGE = 20.seconds -//Maximum age for beacons -private val BEACON_MAX_AGE = 20.seconds +//Maximum age for observed devices. This is used to filter out old data when e.g. there is no GPS signal and there's a gap between two locations +private val OBSERVED_DEVICE_MAX_AGE = 30.seconds //Maximum age of air pressure data, relative to the location timestamp private val AIR_PRESSURE_MAX_AGE = 2.seconds @@ -110,14 +112,6 @@ class WirelessScanner( emptyFlow() } } - .map { bluetoothBeacons -> - val now = timeSource.invoke() - - bluetoothBeacons.filter { - //Beacon library seems to sometimes return very old results -> filter them - (now - it.timestamp).milliseconds <= BEACON_MAX_AGE - } - } .collect { bluetoothBeacons -> mutex.withLock { bluetoothBeacons.forEach { bluetoothBeacon -> @@ -176,12 +170,7 @@ class WirelessScanner( .filter { it.second != null } - .flatMapConcat { (p1, p2) -> - val location1 = p1?.first - val airPressure1 = p1?.second - val location2 = p2?.first - val airPressure2 = p2?.second - + .flatMapConcat { (location1WithPressure, location2WithPressure) -> val (cells, wifis, bluetooths) = mutex.withLock { val cells = cellTowersByKey.values.toList() cellTowersByKey.clear() @@ -195,46 +184,28 @@ class WirelessScanner( Triple(cells, wifis, bluetooths) } - val (location1cells, location2cells) = cells.partition { - location1 != null && - abs(it.timestamp - location1.location.elapsedRealtimeMillisCompat) < abs(it.timestamp - location2!!.location.elapsedRealtimeMillisCompat) - } + val location1 = location1WithPressure?.first + val location2 = location2WithPressure?.first - val (location1wifis, location2wifis) = wifis.partition { - location1 != null && - abs(it.timestamp - location1.location.elapsedRealtimeMillisCompat) < abs(it.timestamp - location2!!.location.elapsedRealtimeMillisCompat) - } + val now = timeSource.invoke() - val (location1bluetooths, location2bluetooths) = bluetooths.partition { - location1 != null && - abs(it.timestamp - location1.location.elapsedRealtimeMillisCompat) < abs(it.timestamp - location2!!.location.elapsedRealtimeMillisCompat) - } + val (location1cells, location2cells) = cells + .filterOldData(now) + .partitionByLocationTimestamp(location1?.location, location2!!.location) + + val (location1wifis, location2wifis) = wifis + .filterOldData(now) + .partitionByLocationTimestamp(location1?.location, location2.location) + + val (location1bluetooths, location2bluetooths) = bluetooths + .filterOldData(now) + .partitionByLocationTimestamp(location1?.location, location2.location) listOfNotNull( location1?.let { - ReportData( - position = Position.fromLocation( - location = it.location, - source = it.source.name.lowercase(Locale.ROOT), - airPressure = airPressure1?.airPressure?.toDouble() - ), - cellTowers = location1cells, - wifiAccessPoints = location1wifis - .takeIf { it.size >= 2 } ?: emptyList(), - bluetoothBeacons = location1bluetooths - ) + createReport(location1WithPressure, location1cells, location1wifis, location1bluetooths) }, - ReportData( - position = Position.fromLocation( - location = location2!!.location, - source = location2.source.name.lowercase(Locale.ROOT), - airPressure = airPressure2?.airPressure?.toDouble() - ), - cellTowers = location2cells, - wifiAccessPoints = location2wifis - .takeIf { it.size >= 2 } ?: emptyList(), - bluetoothBeacons = location2bluetooths - ) + createReport(location2WithPressure, location2cells, location2wifis, location2bluetooths) ).asFlow() } .filter { @@ -246,6 +217,19 @@ class WirelessScanner( private val CellTower.key: String get() = listOf(mobileCountryCode, mobileNetworkCode, locationAreaCode, cellId, primaryScramblingCode).joinToString("/") + private fun createReport(location: LocationWithAirPressure, cells: List, wifis: List, bluetooths: List): ReportData { + return ReportData( + position = Position.fromLocation( + location = location.first.location, + source = location.first.source.name.lowercase(Locale.ROOT), + airPressure = location.second?.airPressure?.toDouble() + ), + cellTowers = cells, + wifiAccessPoints = wifis.takeIf { it.size >= 2 } ?: emptyList(), + bluetoothBeacons = bluetooths + ) + } + /** * Filters Wi-Fi networks that should not be sent to geolocation services, i.e. * hidden networks with empty SSID or those with SSID ending in "_nomap" @@ -257,6 +241,16 @@ class WirelessScanner( !ssid.isNullOrBlank() && !ssid.endsWith("_nomap") } + + private fun List.filterOldData(currentTimestamp: Long): List = filter { device -> + (currentTimestamp - device.timestamp).milliseconds <= OBSERVED_DEVICE_MAX_AGE + } + + private fun List.partitionByLocationTimestamp(location1: Location?, location2: Location): Pair, List> = partition { + //location1 can be null, because locations are collected pairwise and the first location does not have a pair + location1 != null && + abs(it.timestamp - location1.elapsedRealtimeMillisCompat) < abs(it.timestamp - location2.elapsedRealtimeMillisCompat) + } } private typealias LocationWithAirPressure = Pair \ No newline at end of file diff --git a/app/src/test/java/xyz/malkki/neostumbler/scanner/WirelessScannerTest.kt b/app/src/test/java/xyz/malkki/neostumbler/scanner/WirelessScannerTest.kt index 70fff217..575e4e80 100644 --- a/app/src/test/java/xyz/malkki/neostumbler/scanner/WirelessScannerTest.kt +++ b/app/src/test/java/xyz/malkki/neostumbler/scanner/WirelessScannerTest.kt @@ -59,6 +59,52 @@ class WirelessScannerTest { } } + @Test + fun `Test no reports are created with old data`() { + val wirelessScanner = WirelessScanner( + locationSource = { + flowOf( + LocationWithSource( + source = LocationWithSource.LocationSource.GPS, + location = mock { + on { provider } doReturn "gps" + on { latitude } doReturn 50.0 + on { longitude } doReturn 10.0 + on { accuracy } doReturn 15.0f + on { elapsedRealtimeMillis } doReturn 60000 + } + ) + ) + }, + cellInfoSource = { emptyFlow() }, + wifiAccessPointSource = { emptyFlow() }, + bluetoothBeaconSource = { + flowOf(listOf( + BluetoothBeacon( + macAddress = "01:01:01:01:01", + signalStrength = -68, + timestamp = 25000, + beaconType = null, + id1 = null, + id2 = null, + id3 = null, + ) + )) + }, + airPressureSource = { emptyFlow() } + ) + + val reportFlow = wirelessScanner.createReports() + + assertThrows(TimeoutCancellationException::class.java) { + runBlocking { + withTimeout(1.seconds) { + reportFlow.first() + } + } + } + } + @Test fun `Test no reports are created when Wi-Fi networks have an opt-out`() { val wirelessScanner = WirelessScanner(