Skip to content

Commit

Permalink
Do not include data older than 30 seconds in the reports
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaakko committed Oct 14, 2024
1 parent b48e0c2 commit 841da5c
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/xyz/malkki/neostumbler/domain/CellTower.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/xyz/malkki/neostumbler/domain/ObservedDevice.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
94 changes: 44 additions & 50 deletions app/src/main/java/xyz/malkki/neostumbler/scanner/WirelessScanner.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand All @@ -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<CellTower>, wifis: List<WifiAccessPoint>, bluetooths: List<BluetoothBeacon>): 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"
Expand All @@ -257,6 +241,16 @@ class WirelessScanner(

!ssid.isNullOrBlank() && !ssid.endsWith("_nomap")
}

private fun <T : ObservedDevice> List<T>.filterOldData(currentTimestamp: Long): List<T> = filter { device ->
(currentTimestamp - device.timestamp).milliseconds <= OBSERVED_DEVICE_MAX_AGE
}

private fun <T : ObservedDevice> List<T>.partitionByLocationTimestamp(location1: Location?, location2: Location): Pair<List<T>, List<T>> = 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<LocationWithSource, AirPressureObservation?>
Original file line number Diff line number Diff line change
Expand Up @@ -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<Location> {
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(
Expand Down

0 comments on commit 841da5c

Please sign in to comment.