Skip to content

Commit

Permalink
Location: Improve update delivery
Browse files Browse the repository at this point in the history
  • Loading branch information
mar-v-in committed Apr 7, 2024
1 parent 539dd67 commit 827a8b9
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import java.io.PrintWriter
import java.lang.Math.pow
import java.nio.ByteBuffer
import java.util.LinkedList
import kotlin.math.max
import kotlin.math.min

class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDetailsCallback {
Expand Down Expand Up @@ -271,10 +272,6 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta
val scanResultTimestamp = min(wifis.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis())
val scanResultRealtimeMillis =
if (SDK_INT >= 17) SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp) else scanResultTimestamp
if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2 && lastWifiDetailsRealtimeMillis != 0L) {
Log.d(TAG, "Ignoring wifi details, similar age as last ($scanResultRealtimeMillis < $lastWifiDetailsRealtimeMillis + $interval / 2)")
return
}
@Suppress("DEPRECATION")
currentLocalMovingWifi = getSystemService<WifiManager>()?.connectionInfo
?.let { wifiInfo -> wifis.filter { it.macAddress == wifiInfo.bssid && it.isMoving } }
Expand All @@ -294,6 +291,10 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta
}

private fun updateWifiLocation(requestableWifis: List<WifiDetails>, scanResultRealtimeMillis: Long = 0, scanResultTimestamp: Long = 0) {
if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2 && lastWifiDetailsRealtimeMillis != 0L) {
Log.d(TAG, "Ignoring wifi details, similar age as last ($scanResultRealtimeMillis < $lastWifiDetailsRealtimeMillis + $interval / 2)")
return
}
val previousLastRealtimeMillis = lastWifiDetailsRealtimeMillis
if (scanResultRealtimeMillis != 0L) lastWifiDetailsRealtimeMillis = scanResultRealtimeMillis
lifecycleScope.launch {
Expand All @@ -302,9 +303,9 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta
lastWifiDetailsRealtimeMillis = previousLastRealtimeMillis
return@launch
}
if (scanResultTimestamp != 0L && location.time == 0L) location.time = scanResultTimestamp
if (SDK_INT >= 17 && scanResultRealtimeMillis != 0L && location.elapsedRealtimeNanos == 0L) location.elapsedRealtimeNanos =
scanResultRealtimeMillis * 1_000_000L
if (scanResultTimestamp != 0L) location.time = max(scanResultTimestamp, location.time)
if (SDK_INT >= 17 && scanResultRealtimeMillis != 0L) location.elapsedRealtimeNanos =
max(location.elapsedRealtimeNanos, scanResultRealtimeMillis * 1_000_000L)
synchronized(locationLock) {
lastWifiLocation = location
}
Expand Down Expand Up @@ -372,22 +373,43 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta
}

private fun sendLocationUpdate(now: Boolean = false) {
fun cliffLocations(old: Location?, new: Location?): Location? {
// We move from wifi towards cell with accuracy
if (old == null) return new
if (new == null) return old
val diff = new.elapsedMillis - old.elapsedMillis
if (diff < LOCATION_TIME_CLIFF_START_MS) return old
if (diff > LOCATION_TIME_CLIFF_END_MS) return new
val pct = (diff - LOCATION_TIME_CLIFF_START_MS).toDouble() / (LOCATION_TIME_CLIFF_END_MS - LOCATION_TIME_CLIFF_START_MS).toDouble()
return Location(old).apply {
provider = "cliff"
latitude = old.latitude * (1.0-pct) + new.latitude * pct
longitude = old.longitude * (1.0-pct) + new.longitude * pct
accuracy = (old.accuracy * (1.0-pct) + new.accuracy * pct).toFloat()
altitude = old.altitude * (1.0-pct) + new.altitude * pct
time = (old.time.toDouble() * (1.0-pct) + new.time.toDouble() * pct).toLong()
elapsedRealtimeNanos = (old.elapsedRealtimeNanos.toDouble() * (1.0-pct) + new.elapsedRealtimeNanos.toDouble() * pct).toLong()
}
}
val location = synchronized(locationLock) {
if (lastCellLocation == null && lastWifiLocation == null) return
when {
// Only non-null
lastCellLocation == null -> lastWifiLocation
lastWifiLocation == null -> lastCellLocation
// Consider cliff
lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastCellLocation
lastWifiLocation!!.elapsedMillis > lastCellLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_MS -> lastWifiLocation
// Consider cliff end
lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_END_MS -> lastCellLocation
lastWifiLocation!!.elapsedMillis > lastCellLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_START_MS -> lastWifiLocation
// Wifi out of cell range with higher precision
lastCellLocation!!.precision > lastWifiLocation!!.precision && lastWifiLocation!!.distanceTo(lastCellLocation!!) > 2 * lastCellLocation!!.accuracy -> lastCellLocation
// Consider cliff start
lastCellLocation!!.elapsedMillis > lastWifiLocation!!.elapsedMillis + LOCATION_TIME_CLIFF_START_MS -> cliffLocations(lastWifiLocation, lastCellLocation)
else -> lastWifiLocation
}
} ?: return
if (location == lastLocation) return
if (lastLocation == lastWifiLocation && location == lastCellLocation && !now) {
if (lastLocation == lastWifiLocation && lastLocation.let { it != null && location.accuracy > it.accuracy } && !now) {
Log.d(TAG, "Debounce inaccurate location update")
handler.postDelayed({
sendLocationUpdate(true)
}, DEBOUNCE_DELAY_MS)
Expand Down Expand Up @@ -456,7 +478,8 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta
const val GPS_BUFFER_SIZE = 60
const val GPS_PASSIVE_INTERVAL = 1000L
const val GPS_PASSIVE_MIN_ACCURACY = 25f
const val LOCATION_TIME_CLIFF_MS = 30000L
const val LOCATION_TIME_CLIFF_START_MS = 30000L
const val LOCATION_TIME_CLIFF_END_MS = 60000L
const val DEBOUNCE_DELAY_MS = 5000L
const val MAX_WIFI_SCAN_CACHE_AGE = 1000L * 60 * 60 * 24 // 1 day
const val MAX_LOCAL_WIFI_AGE_NS = 60_000_000_000L // 1 minute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
package org.microg.gms.location.network.ichnaea

data class GeolocateResponse(
val location: ResponseLocation?,
val accuracy: Double?,
val fallback: String?,
val error: ResponseError?
val location: ResponseLocation? = null,
val accuracy: Double? = null,
val fallback: String? = null,
val error: ResponseError? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import com.android.volley.Request.Method
import com.android.volley.VolleyError
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import org.json.JSONObject
import org.microg.gms.location.LocationSettings
import org.microg.gms.location.network.cell.CellDetails
import org.microg.gms.location.network.precision
import org.microg.gms.location.network.wifi.WifiDetails
import org.microg.gms.location.network.wifi.isMoving
import org.microg.gms.location.provider.BuildConfig
import org.microg.gms.utils.singleInstanceOf
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
Expand Down Expand Up @@ -75,12 +78,32 @@ class IchnaeaServiceClient(private val context: Context) {
}
}

private fun continueError(continuation: Continuation<GeolocateResponse>, error: VolleyError) {
try {
val response = JSONObject(error.networkResponse.data.decodeToString()).toGeolocateResponse()
if (response.error != null) {
continuation.resume(response)
return
} else if (response.location?.lat != null){
Log.w(TAG, "Received location in response with error code")
} else {
Log.w(TAG, "Received valid json without error in response with error code")
}
} catch (_: Exception) {
}
if (error.networkResponse != null) {
continuation.resume(GeolocateResponse(error = ResponseError(error.networkResponse.statusCode, error.message)))
return
}
continuation.resumeWithException(error)
}

private suspend fun rawGeoLocate(request: GeolocateRequest): GeolocateResponse = suspendCoroutine { continuation ->
val url = Uri.parse(settings.ichneaeEndpoint).buildUpon().appendPath("v1").appendPath("geolocate").build().toString()
queue.add(object : JsonObjectRequest(Method.POST, url, request.toJson(), {
continuation.resume(it.toGeolocateResponse())
}, {
continuation.resumeWithException(it)
continueError(continuation, it)
}) {
override fun getHeaders(): Map<String, String> = getRequestHeaders()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
package org.microg.gms.location.network.ichnaea

data class ResponseError(
val code: Int,
val message: String
val code: Int? = null,
val message: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,9 @@ class MovingWifiHelper(private val context: Context) {
"CDWiFi", "MAVSTART-WIFI" -> parsePassengera(location, data)
"AegeanWiFi" -> parseDisplayUgo(location, data)
"Cathay Pacific", "Telekom_FlyNet", "KrisWorld", "SWISS Connect", "Edelweiss Entertainment" -> parsePanasonic(location, data)
"FlyNet" -> parseBoardConnect(location, data)
"FlyNet", "Austrian FlyNet" -> parseBoardConnect(location, data)
"ACWiFi" -> parseAirCanada(location, data)
"OUIFI" -> parseSncf(location, data)
"_SNCF_WIFI_INOUI" -> parseSncf(location, data)
"_SNCF_WIFI_INTERCITES" -> parseSncf(location, data)
"_WIFI_LYRIA" -> parseSncf(location, data)
"NormandieTrainConnecte" -> parseSncf(location, data)
"OUIFI", "_SNCF_WIFI_INOUI", "_SNCF_WIFI_INTERCITES", "_WIFI_LYRIA", "NormandieTrainConnecte" -> parseSncf(location, data)
"agilis-Wifi" -> parseHotsplots(location, data)
else -> throw UnsupportedOperationException()
}
Expand Down Expand Up @@ -337,6 +333,7 @@ class MovingWifiHelper(private val context: Context) {
"_WIFI_LYRIA" to "https://wifi.tgv-lyria.com/router/api/train/gps",
"NormandieTrainConnecte" to "https://wifi.normandie.fr/router/api/train/gps",
"agilis-Wifi" to "http://hsp.hotsplots.net/status.json",
"Austrian FlyNet" to "https://www.austrian-flynet.com/map/api/flightData",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import android.content.Context
import android.content.Intent
import android.location.Criteria
import android.location.Location
import android.location.LocationManager
import android.os.Build.VERSION.SDK_INT
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.os.WorkSource
import androidx.annotation.RequiresApi
import androidx.core.app.PendingIntentCompat
import androidx.core.content.getSystemService
import com.android.location.provider.ProviderPropertiesUnbundled
import com.android.location.provider.ProviderRequestUnbundled
import org.microg.gms.location.*
Expand All @@ -40,6 +45,8 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
private var currentRequest: ProviderRequestUnbundled? = null
private var pendingIntent: PendingIntent? = null
private var lastReportedLocation: Location? = null
private val handler = Handler(Looper.getMainLooper())
private val reportAgainRunnable = Runnable { reportAgain() }

private fun updateRequest() {
if (enabled) {
Expand All @@ -65,6 +72,7 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false)
}
context.startService(intent)
reportAgain()
}
}

Expand Down Expand Up @@ -93,6 +101,13 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
SDK_INT >= 30 -> isAllowed = true
SDK_INT >= 29 -> isEnabled = true
}
try {
if (lastReportedLocation == null) {
lastReportedLocation = context.getSystemService<LocationManager>()?.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
}
} catch (_: SecurityException) {
} catch (_: Exception) {
}
}
}

Expand All @@ -107,18 +122,35 @@ class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
pendingIntent = null
currentRequest = null
enabled = false
handler.removeCallbacks(reportAgainRunnable)
}
}

private fun reportAgain() {
// Report location again if it's recent enough

This comment has been minimized.

Copy link
@t-m-w

t-m-w May 7, 2024

Contributor

@mar-v-in I'm trying to understand this change. The comment says "if it's recent enough", but the conditions below appear to be checking if it's old enough instead... maybe I'm just getting confused?

Anyway, I've noticed that compared to Play services, microG (at least as of this change) continues to report stale locations seemingly forever. This happens even if the device is put into airplane mode and no longer has the needed information to determine a network location at all.

It's easy to test this with something like SatStat's map tab, where network location is a blue pin that will turn gray when it's gone stale. With Play services, enter airplane mode and the pin will go gray within seconds. With microG, it seemingly never does, and adb shell dumpsys location shows that the old value is repeated as if it's still accurate.

Is the condition below incorrect, or is this intentional?

This comment has been minimized.

Copy link
@mar-v-in

mar-v-in May 8, 2024

Author Member

Nice catch. You're right, the comparison is the wrong way round here. Also I figured another culprit that can cause the described issue in SatStat caused by the wifi scan cache (if the same location from the scan cache is reported directly after each other - which is not unlikely to happen when you don't move around - it currently won't be updated properly, leading to all ends thinking there is no recent wifi based location).

I'm currently testing a fix for those issues and will make sure it will be in the next release.

This comment has been minimized.

Copy link
@t-m-w

t-m-w May 8, 2024

Contributor

Sounds great, thanks!

This came up as I have been trying an early implementation of the approach you described in this comment, of preferring low-accuracy GPS to network location if the network location is too far off. Not sure if it's quite what you had in mind, but I was asked to look into it and I wanted to see if I could make something work.

lastReportedLocation?.let {
if (it.elapsedMillis + MIN_INTERVAL_MILLIS < SystemClock.elapsedRealtime() ||
it.elapsedMillis + (currentRequest?.interval ?: 0) < SystemClock.elapsedRealtime()) {
reportLocationToSystem(it)
}
}
}

override fun reportLocationToSystem(location: Location) {
location.provider = "network"
handler.removeCallbacks(reportAgainRunnable)
location.provider = LocationManager.NETWORK_PROVIDER
location.extras?.remove(LOCATION_EXTRA_PRECISION)
lastReportedLocation = location
super.reportLocation(location)
val repeatInterval = max(MIN_REPORT_MILLIS, currentRequest?.interval ?: Long.MAX_VALUE)
if (repeatInterval < MIN_INTERVAL_MILLIS) {
handler.postDelayed(reportAgainRunnable, repeatInterval)
}
}

companion object {
private const val MIN_INTERVAL_MILLIS = 20000L
private const val MIN_REPORT_MILLIS = 1000L
private val properties = ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package org.microg.gms.location.manager
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Build.VERSION.SDK_INT
import android.os.SystemClock
import android.util.Log
import androidx.core.content.getSystemService
Expand All @@ -24,6 +23,7 @@ import org.microg.safeparcel.AutoSafeParcelable
import java.io.File
import java.lang.Long.max
import java.util.concurrent.TimeUnit
import kotlin.math.min

class LastLocationCapsule(private val context: Context) {
private var lastFineLocation: Location? = null
Expand All @@ -44,15 +44,13 @@ class LastLocationCapsule(private val context: Context) {
else -> return null
} ?: return null
val cliff = if (effectiveGranularity == GRANULARITY_COARSE) max(maxUpdateAgeMillis, TIME_COARSE_CLIFF) else maxUpdateAgeMillis
val elapsedRealtimeDiff = SystemClock.elapsedRealtime() - LocationCompat.getElapsedRealtimeMillis(location)
val elapsedRealtimeDiff = SystemClock.elapsedRealtime() - location.elapsedMillis
if (elapsedRealtimeDiff > cliff) return null
if (elapsedRealtimeDiff <= maxUpdateAgeMillis) return location
// Location is too old according to maxUpdateAgeMillis, but still in scope due to time coarsing. Adjust time
val locationUpdated = Location(location)
val timeAdjustment = elapsedRealtimeDiff - maxUpdateAgeMillis
if (SDK_INT >= 17) {
locationUpdated.elapsedRealtimeNanos = location.elapsedRealtimeNanos + TimeUnit.MILLISECONDS.toNanos(timeAdjustment)
}
locationUpdated.elapsedRealtimeNanos = location.elapsedRealtimeNanos + TimeUnit.MILLISECONDS.toNanos(timeAdjustment)
locationUpdated.time = location.time + timeAdjustment
return locationUpdated
}
Expand All @@ -66,6 +64,8 @@ class LastLocationCapsule(private val context: Context) {
}

fun updateCoarseLocation(location: Location) {
location.elapsedRealtimeNanos = min(location.elapsedRealtimeNanos, SystemClock.elapsedRealtimeNanos())
location.time = min(location.time, System.currentTimeMillis())
if (lastCoarseLocation != null && lastCoarseLocation!!.elapsedMillis + EXTENSION_CLIFF > location.elapsedMillis) {
if (!location.hasSpeed()) {
location.speed = lastCoarseLocation!!.distanceTo(location) / ((location.elapsedMillis - lastCoarseLocation!!.elapsedMillis) / 1000)
Expand All @@ -81,6 +81,8 @@ class LastLocationCapsule(private val context: Context) {
}

fun updateFineLocation(location: Location) {
location.elapsedRealtimeNanos = min(location.elapsedRealtimeNanos, SystemClock.elapsedRealtimeNanos())
location.time = min(location.time, System.currentTimeMillis())
lastFineLocation = newest(lastFineLocation, location)
lastFineLocationTimeCoarsed = newest(lastFineLocationTimeCoarsed, location, TIME_COARSE_CLIFF)
updateCoarseLocation(location)
Expand All @@ -89,15 +91,19 @@ class LastLocationCapsule(private val context: Context) {
private fun newest(oldLocation: Location?, newLocation: Location, cliff: Long = 0): Location {
if (oldLocation == null) return newLocation
if (LocationCompat.isMock(oldLocation) && !LocationCompat.isMock(newLocation)) return newLocation
if (LocationCompat.getElapsedRealtimeNanos(newLocation) >= LocationCompat.getElapsedRealtimeNanos(oldLocation) + TimeUnit.MILLISECONDS.toNanos(cliff)) return newLocation
oldLocation.elapsedRealtimeNanos = min(oldLocation.elapsedRealtimeNanos, SystemClock.elapsedRealtimeNanos())
oldLocation.time = min(oldLocation.time, System.currentTimeMillis())
if (newLocation.elapsedRealtimeNanos >= oldLocation.elapsedRealtimeNanos + TimeUnit.MILLISECONDS.toNanos(cliff)) return newLocation
return oldLocation
}

fun start() {
fun Location.adjustRealtime() = apply {
if (SDK_INT >= 17) {
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() - TimeUnit.MILLISECONDS.toNanos((System.currentTimeMillis() - time))
}
time = min(time, System.currentTimeMillis())
elapsedRealtimeNanos = min(
SystemClock.elapsedRealtimeNanos() - TimeUnit.MILLISECONDS.toNanos((System.currentTimeMillis() - time)),
SystemClock.elapsedRealtimeNanos()
)
}
try {
if (file.exists()) {
Expand Down

0 comments on commit 827a8b9

Please sign in to comment.