Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2023 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission
android:name="android.permission.LOCATION_HARDWARE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.INSTALL_LOCATION_PROVIDER"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.NETWORK_SCAN"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.MODIFY_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<application>
<uses-library android:name="com.android.location.provider" />
<service
android:name="org.microg.gms.location.network.NetworkLocationService"
android:exported="false">
<intent-filter>
<action android:name="org.microg.gms.location.network.ACTION_NETWORK_LOCATION_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="org.microg.gms.location.network.ACTION_NETWORK_IMPORT_EXPORT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<provider
android:name="org.microg.gms.location.network.DatabaseExportFileProvider"
android:authorities="${applicationId}.microg.location.export"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/location_exported_files" />
</provider>
<service
android:name="org.microg.gms.location.provider.NetworkLocationProviderService"
android:exported="true"
android:permission="android.permission.WRITE_SECURE_SETTINGS">
<intent-filter>
<action android:name="com.android.location.service.v2.NetworkLocationProvider" />
<action android:name="com.android.location.service.v3.NetworkLocationProvider" />
</intent-filter>
<meta-data
android:name="serviceVersion"
android:value="2" />
</service>
<service
android:name="org.microg.gms.location.provider.FusedLocationProviderService"
android:exported="true"
android:permission="android.permission.WRITE_SECURE_SETTINGS">
<intent-filter>
<action android:name="com.android.location.service.FusedLocationProvider" />
</intent-filter>
<meta-data
android:name="serviceVersion"
android:value="2" />
</service>
<service
android:name="org.microg.gms.location.provider.GeocodeProviderService"
android:exported="true"
android:permission="android.permission.WRITE_SECURE_SETTINGS">
<intent-filter>
<action android:name="com.android.location.service.GeocodeProvider" />
<action android:name="com.google.android.location.GeocodeProvider" />
</intent-filter>
<meta-data
android:name="serviceVersion"
android:value="2" />
</service>
</application>
</manifest>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
import android.net.Uri
import androidx.core.content.FileProvider
class DatabaseExportFileProvider : FileProvider() {
override fun getType(uri: Uri): String? {
try {
if (uri.lastPathSegment?.startsWith("cell-") == true) {
return "application/vnd.microg.location.cell+csv+gzip"
}
if (uri.lastPathSegment?.startsWith("wifi-") == true) {
return "application/vnd.microg.location.wifi+csv+gzip"
}
} catch (ignored: Exception) {}
return super.getType(uri)
}
}

View file

@ -0,0 +1,674 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import android.location.Location
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.content.contentValuesOf
import androidx.core.database.getDoubleOrNull
import androidx.core.database.getStringOrNull
import androidx.core.os.BundleCompat
import org.microg.gms.location.NAME_CELL
import org.microg.gms.location.NAME_WIFI
import org.microg.gms.location.network.cell.CellDetails
import org.microg.gms.location.network.cell.isValid
import org.microg.gms.location.network.wifi.WifiDetails
import org.microg.gms.location.network.wifi.isRequestable
import org.microg.gms.location.network.wifi.macBytes
import org.microg.gms.utils.toHexString
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintWriter
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
private const val CURRENT_VERSION = 8
internal class LocationDatabase(private val context: Context) : SQLiteOpenHelper(context, "geocache.db", null, CURRENT_VERSION) {
private data class Migration(val apply: String?, val revert: String?, val allowApplyFailure: Boolean, val allowRevertFailure: Boolean)
private val migrations: Map<Int, List<Migration>>
init {
val migrations = mutableMapOf<Int, MutableList<Migration>>()
fun declare(version: Int, apply: String, revert: String? = null, allowFailure: Boolean = false, allowApplyFailure: Boolean = allowFailure, allowRevertFailure: Boolean = allowFailure) {
if (!migrations.containsKey(version))
migrations[version] = arrayListOf()
migrations[version]!!.add(Migration(apply, revert, allowApplyFailure, allowRevertFailure))
}
declare(3, "CREATE TABLE $TABLE_CELLS($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);")
declare(3, "CREATE TABLE $TABLE_CELLS_PRE($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TIME INTEGER NOT NULL);")
declare(3, "CREATE TABLE $TABLE_WIFIS($FIELD_MAC BLOB, $FIELD_LATITUDE REAL NOT NULL, $FIELD_LONGITUDE REAL NOT NULL, $FIELD_ACCURACY REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_PRECISION REAL NOT NULL);")
declare(3, "CREATE TABLE $TABLE_CELLS_LEARN($FIELD_MCC INTEGER NOT NULL, $FIELD_MNC INTEGER NOT NULL, $FIELD_TYPE INTEGER NOT NULL, $FIELD_LAC_TAC INTEGER NOT NULL, $FIELD_CID INTEGER NOT NULL, $FIELD_PSC INTEGER NOT NULL, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);")
declare(3, "CREATE TABLE $TABLE_WIFI_LEARN($FIELD_MAC BLOB, $FIELD_LATITUDE_HIGH REAL NOT NULL, $FIELD_LATITUDE_LOW REAL NOT NULL, $FIELD_LONGITUDE_HIGH REAL NOT NULL, $FIELD_LONGITUDE_LOW REAL NOT NULL, $FIELD_TIME INTEGER NOT NULL, $FIELD_BAD_TIME INTEGER);")
declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS}_index ON $TABLE_CELLS($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);")
declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_PRE}_index ON $TABLE_CELLS_PRE($FIELD_MCC, $FIELD_MNC);")
declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFIS}_index ON $TABLE_WIFIS($FIELD_MAC);")
declare(3, "CREATE UNIQUE INDEX ${TABLE_CELLS_LEARN}_index ON $TABLE_CELLS_LEARN($FIELD_MCC, $FIELD_MNC, $FIELD_TYPE, $FIELD_LAC_TAC, $FIELD_CID, $FIELD_PSC);")
declare(3, "CREATE UNIQUE INDEX ${TABLE_WIFI_LEARN}_index ON $TABLE_WIFI_LEARN($FIELD_MAC);")
declare(3, "CREATE INDEX ${TABLE_CELLS}_time_index ON $TABLE_CELLS($FIELD_TIME);")
declare(3, "CREATE INDEX ${TABLE_CELLS_PRE}_time_index ON $TABLE_CELLS_PRE($FIELD_TIME);")
declare(3, "CREATE INDEX ${TABLE_WIFIS}_time_index ON $TABLE_WIFIS($FIELD_TIME);")
declare(3, "CREATE INDEX ${TABLE_CELLS_LEARN}_time_index ON $TABLE_CELLS_LEARN($FIELD_TIME);")
declare(3, "CREATE INDEX ${TABLE_WIFI_LEARN}_time_index ON $TABLE_WIFI_LEARN($FIELD_TIME);")
declare(3, "DROP TABLE IF EXISTS $TABLE_WIFI_SCANS;", allowFailure = true)
declare(4, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;")
declare(4, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_LEARN_RECORD_COUNT INTEGER;")
declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE REAL;")
declare(5, "ALTER TABLE $TABLE_CELLS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;")
declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE REAL;")
declare(5, "ALTER TABLE $TABLE_WIFIS ADD COLUMN $FIELD_ALTITUDE_ACCURACY REAL;")
declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;")
declare(6, "ALTER TABLE $TABLE_CELLS_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;")
declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_HIGH REAL;")
declare(6, "ALTER TABLE $TABLE_WIFI_LEARN ADD COLUMN $FIELD_ALTITUDE_LOW REAL;")
declare(8, "DELETE FROM $TABLE_WIFIS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true)
declare(8, "DELETE FROM $TABLE_CELLS WHERE $FIELD_ACCURACY = 0.0 AND ($FIELD_LATITUDE != 0.0 OR $FIELD_LONGITUDE != 0.0);", allowRevertFailure = true)
declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true)
declare(8, "UPDATE $TABLE_CELLS_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true)
declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_LOW = NULL WHERE $FIELD_ALTITUDE_LOW = 0.0;", allowRevertFailure = true)
declare(8, "UPDATE $TABLE_WIFI_LEARN SET $FIELD_ALTITUDE_HIGH = NULL WHERE $FIELD_ALTITUDE_HIGH = 0.0;", allowRevertFailure = true)
this.migrations = migrations
}
private fun migrate(db: SQLiteDatabase, oldVersion: Int, newVersion: Int, allowFailure: Boolean = false) {
var currentVersion = oldVersion
while (currentVersion < newVersion) {
val nextVersion = currentVersion + 1
val migrations = this.migrations[nextVersion].orEmpty()
for (migration in migrations) {
if (migration.apply == null && !migration.allowApplyFailure && !allowFailure)
throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion")
try {
db.execSQL(migration.apply)
Log.d(TAG, "Applied migration from version $currentVersion to $nextVersion: ${migration.apply}")
} catch (e: Exception) {
Log.w(TAG, "Error while applying migration from version $currentVersion to $nextVersion: ${migration.apply}", e)
if (!migration.allowApplyFailure && !allowFailure)
throw e
}
}
currentVersion = nextVersion
}
while (currentVersion > newVersion) {
val nextVersion = currentVersion - 1
val migrations = this.migrations[currentVersion].orEmpty()
for (migration in migrations.asReversed()) {
if (migration.revert == null && !migration.allowRevertFailure && !allowFailure)
throw SQLiteException("Incomplete migration from $currentVersion to $nextVersion")
try {
db.execSQL(migration.revert)
Log.d(TAG, "Reverted migration from version $currentVersion to $nextVersion: ${migration.revert}")
} catch (e: Exception) {
Log.w(TAG, "Error while reverting migration from version $currentVersion to $nextVersion: ${migration.revert}", e)
if (!migration.allowRevertFailure && !allowFailure)
throw e
}
}
currentVersion = nextVersion
}
Log.i(TAG, "Migrated from $oldVersion to $newVersion")
}
private fun SQLiteDatabase.query(table: String, columns: Array<String>, selection: String? = null, selectionArgs: Array<String>? = null) =
query(table, columns, selection, selectionArgs, null, null, null)
fun getCellLocation(cell: CellDetails, allowLearned: Boolean = true): Location? {
var cursor = readableDatabase.query(TABLE_CELLS, FIELDS_CACHE_LOCATION, CELLS_SELECTION, getCellSelectionArgs(cell))
val cellLocation = cursor.getSingleLocation(MAX_CACHE_AGE)
if (allowLearned) {
cursor = readableDatabase.query(TABLE_CELLS_LEARN, FIELDS_MID_LOCATION_GET_LEARN, CELLS_SELECTION, getCellSelectionArgs(cell))
try {
if (cursor.moveToNext()) {
val badTime = cursor.getLong(8)
val time = cursor.getLong(7)
if (badTime < time - LEARN_BAD_CUTOFF) {
cursor.getCellMidLocation()?.let {
if (cellLocation == null || cellLocation == NEGATIVE_CACHE_ENTRY || cellLocation.precision < it.precision) return it
}
}
}
} finally {
cursor.close()
}
}
if (cellLocation != null) return cellLocation
cursor = readableDatabase.query(TABLE_CELLS_PRE, arrayOf(FIELD_TIME), CELLS_PRE_SELECTION, getCellPreSelectionArgs(cell))
try {
if (cursor.moveToNext()) {
if (cursor.getLong(1) > System.currentTimeMillis() - MAX_CACHE_AGE) {
return NEGATIVE_CACHE_ENTRY
}
}
} finally {
cursor.close()
}
return null
}
fun getWifiLocation(wifi: WifiDetails, allowLearned: Boolean = true): Location? {
var cursor = readableDatabase.query(TABLE_WIFIS, FIELDS_CACHE_LOCATION, getWifiSelection(wifi))
val wifiLocation = cursor.getSingleLocation(MAX_CACHE_AGE)
if (allowLearned) {
cursor = readableDatabase.query(TABLE_WIFI_LEARN, FIELDS_MID_LOCATION_GET_LEARN, getWifiSelection(wifi))
try {
if (cursor.moveToNext()) {
val badTime = cursor.getLong(8)
val time = cursor.getLong(7)
if (badTime < time - LEARN_BAD_CUTOFF) {
cursor.getWifiMidLocation()?.let {
if (wifiLocation == null || wifiLocation == NEGATIVE_CACHE_ENTRY || wifiLocation.precision < it.precision) return it
}
}
}
} finally {
cursor.close()
}
}
return wifiLocation
}
fun putCellLocation(cell: CellDetails, location: Location) {
if (!cell.isValid) return
val cv = contentValuesOf(
FIELD_MCC to cell.mcc,
FIELD_MNC to cell.mnc,
FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0),
FIELD_TYPE to cell.type.ordinal,
FIELD_CID to cell.cid,
FIELD_PSC to (cell.pscOrPci ?: 0)
).apply { putLocation(location) }
writableDatabase.insertWithOnConflict(TABLE_CELLS, null, cv, SQLiteDatabase.CONFLICT_REPLACE)
}
fun putWifiLocation(wifi: WifiDetails, location: Location) {
if (!wifi.isRequestable) return
val cv = contentValuesOf(
FIELD_MAC to wifi.macBytes
).apply { putLocation(location) }
writableDatabase.insertWithOnConflict(TABLE_WIFIS, null, cv, SQLiteDatabase.CONFLICT_REPLACE)
}
fun learnCellLocation(cell: CellDetails, location: Location, import: Boolean = false): Boolean {
if (!cell.isValid) return false
val cursor = readableDatabase.query(TABLE_CELLS_LEARN, FIELDS_MID_LOCATION, CELLS_SELECTION, getCellSelectionArgs(cell))
var exists = false
var isBad = false
var midLocation: Location? = null
try {
if (cursor.moveToNext()) {
midLocation = cursor.getMidLocation()
exists = midLocation != null
isBad = midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_CELL } == true
}
} finally {
cursor.close()
}
if (exists && isBad) {
val values = ContentValues().apply { putLearnLocation(location, badTime = location.time, import = import) }
writableDatabase.update(TABLE_CELLS_LEARN, values, CELLS_SELECTION, getCellSelectionArgs(cell))
} else if (!exists) {
val values = contentValuesOf(
FIELD_MCC to cell.mcc,
FIELD_MNC to cell.mnc,
FIELD_LAC_TAC to (cell.lac ?: cell.tac ?: 0),
FIELD_TYPE to cell.type.ordinal,
FIELD_CID to cell.cid,
FIELD_PSC to (cell.pscOrPci ?: 0),
).apply { putLearnLocation(location, badTime = 0) }
writableDatabase.insertWithOnConflict(TABLE_CELLS_LEARN, null, values, SQLiteDatabase.CONFLICT_REPLACE)
} else {
val values = ContentValues().apply { putLearnLocation(location, midLocation) }
writableDatabase.update(TABLE_CELLS_LEARN, values, CELLS_SELECTION, getCellSelectionArgs(cell))
}
return true
}
fun learnWifiLocation(wifi: WifiDetails, location: Location, import: Boolean = false): Boolean {
if (!wifi.isRequestable) return false
val cursor = readableDatabase.query(TABLE_WIFI_LEARN, FIELDS_MID_LOCATION, getWifiSelection(wifi))
var exists = false
var isBad = false
var midLocation: Location? = null
try {
if (cursor.moveToNext()) {
midLocation = cursor.getMidLocation()
exists = midLocation != null
isBad = midLocation?.let { it.distanceTo(location) > LEARN_BAD_SIZE_WIFI } == true
}
} finally {
cursor.close()
}
if (exists && isBad) {
val values = ContentValues().apply { putLearnLocation(location, badTime = location.time, import = import) }
writableDatabase.update(TABLE_WIFI_LEARN, values, getWifiSelection(wifi), null)
} else if (!exists) {
val values = contentValuesOf(
FIELD_MAC to wifi.macBytes
).apply { putLearnLocation(location, badTime = 0) }
writableDatabase.insertWithOnConflict(TABLE_WIFI_LEARN, null, values, SQLiteDatabase.CONFLICT_REPLACE)
} else {
val values = ContentValues().apply { putLearnLocation(location, midLocation) }
writableDatabase.update(TABLE_WIFI_LEARN, values, getWifiSelection(wifi), null)
}
return true
}
fun exportLearned(name: String): Uri? {
try {
val wifi = when (name) {
NAME_WIFI -> true
NAME_CELL -> false
else -> throw IllegalArgumentException()
}
val fieldNames = if (wifi) FIELDS_WIFI else FIELDS_CELL
val tableName = if (wifi) TABLE_WIFI_LEARN else TABLE_CELLS_LEARN
val midLocationGetter: (Cursor) -> Location? = if (wifi) Cursor::getWifiMidLocation else Cursor::getCellMidLocation
val exportDir = File(context.cacheDir, "location")
exportDir.mkdir()
val exportFile = File(exportDir, "$name-${UUID.randomUUID()}.csv.gz")
val output = GZIPOutputStream(exportFile.outputStream()).bufferedWriter()
output.write("${fieldNames.joinToString(",")},${FIELDS_EXPORT_DATA.joinToString(",")}\n")
val cursor = readableDatabase.query(tableName, FIELDS_MID_LOCATION + fieldNames)
val indices = fieldNames.map { cursor.getColumnIndexOrThrow(it) }
while (cursor.moveToNext()) {
val midLocation = midLocationGetter(cursor)
if (midLocation != null) {
output.write(indices.joinToString(",") { index ->
if (cursor.getType(index) == Cursor.FIELD_TYPE_BLOB) {
cursor.getBlob(index).toHexString()
} else {
cursor.getStringOrNull(index) ?: ""
}
})
output.write(",${midLocation.latitude},${midLocation.longitude},${if (midLocation.hasAltitude()) midLocation.altitude else ""}\n")
}
}
output.close()
return FileProvider.getUriForFile(context,"${context.packageName}.microg.location.export", exportFile)
} catch (e: Exception) {
Log.w(TAG, e)
}
return null
}
fun importLearned(fileUri: Uri): Int {
var counter = 0
try {
val type = context.contentResolver.getType(fileUri)
val gzip = if (type == null || type !in SUPPORTED_TYPES) {
if (fileUri.path == null) throw IllegalArgumentException("Unsupported file extension")
if (fileUri.path!!.endsWith(".gz")) {
true
} else if (fileUri.path!!.endsWith(".csv")) {
false
} else {
throw IllegalArgumentException("Unsupported file extension")
}
} else {
type.endsWith("gzip")
}
val desc = context.contentResolver.openFileDescriptor(fileUri, "r") ?: throw FileNotFoundException()
ParcelFileDescriptor.AutoCloseInputStream(desc).use { source ->
val input = (if (gzip) GZIPInputStream(source) else source).bufferedReader()
val headers = input.readLine().split(",")
val name = when {
headers.containsAll(FIELDS_WIFI.toList()) && headers.containsAll(FIELDS_EXPORT_DATA.toList()) -> NAME_WIFI
headers.containsAll(FIELDS_CELL.toList()) && headers.containsAll(FIELDS_EXPORT_DATA.toList()) -> NAME_CELL
else -> null
}
if (name != null) {
while (true) {
val line = input.readLine().split(",")
if (line.size != headers.size) break // End of file reached
val location = Location(PROVIDER_CACHE)
location.latitude = line[headers.indexOf(FIELD_LATITUDE)].toDoubleOrNull() ?: continue
location.longitude = line[headers.indexOf(FIELD_LONGITUDE)].toDoubleOrNull() ?: continue
line[headers.indexOf(FIELD_ALTITUDE)].toDoubleOrNull()?.let { location.altitude = it }
location.time = headers.indexOf(FIELD_TIME).takeIf { it != -1 }?.let { line[it] }?.toLongOrNull()?.takeIf { it > 0 } ?: System.currentTimeMillis()
if (name == NAME_WIFI) {
val wifi = WifiDetails(
macAddress = line[headers.indexOf(FIELD_MAC)]
)
if (learnWifiLocation(wifi, location)) counter++
} else {
val cell = CellDetails(
type = line[headers.indexOf(FIELD_TYPE)].let {
it.toIntOrNull()?.let { CellDetails.Companion.Type.entries[it] } ?:
runCatching { CellDetails.Companion.Type.valueOf(it) }.getOrNull()
} ?: continue,
mcc = line[headers.indexOf(FIELD_MCC)].toIntOrNull() ?: continue,
mnc = line[headers.indexOf(FIELD_MNC)].toIntOrNull() ?: continue,
lac = line[headers.indexOf(FIELD_LAC_TAC)].toIntOrNull(),
tac = line[headers.indexOf(FIELD_LAC_TAC)].toIntOrNull(),
cid = line[headers.indexOf(FIELD_CID)].toLongOrNull() ?: continue,
pscOrPci = line[headers.indexOf(FIELD_PSC)].toIntOrNull(),
)
if (learnCellLocation(cell, location)) counter++
}
}
}
}
} catch (e: Exception) {
Log.w(TAG, e)
}
return counter
}
override fun onCreate(db: SQLiteDatabase) {
migrate(db, 0, CURRENT_VERSION)
}
fun cleanup(db: SQLiteDatabase) {
db.delete(TABLE_CELLS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString()))
db.delete(TABLE_CELLS_PRE, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString()))
db.delete(TABLE_WIFIS, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_CACHE_AGE).toString()))
db.delete(TABLE_CELLS_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString()))
db.delete(TABLE_WIFI_LEARN, "$FIELD_TIME < ?", arrayOf((System.currentTimeMillis() - MAX_LEARN_AGE).toString()))
}
override fun onOpen(db: SQLiteDatabase) {
super.onOpen(db)
if (!db.isReadOnly) {
cleanup(db)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
migrate(db, oldVersion, newVersion)
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
migrate(db, oldVersion, newVersion)
}
fun dump(writer: PrintWriter) {
writer.println("Database: cells(cached)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS)}, cells(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_CELLS_LEARN)}, wifis(cached)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFIS)}, wifis(learnt)=${DatabaseUtils.queryNumEntries(readableDatabase, TABLE_WIFI_LEARN)}")
}
}
const val EXTRA_HIGH_LOCATION = "high"
const val EXTRA_LOW_LOCATION = "low"
const val EXTRA_RECORD_COUNT = "recs"
private const val MAX_CACHE_AGE = 1000L * 60 * 60 * 24 * 14 // 14 days
private const val MAX_LEARN_AGE = 1000L * 60 * 60 * 24 * 365 // 1 year
private const val TABLE_CELLS = "cells"
private const val TABLE_CELLS_PRE = "cells_pre"
private const val TABLE_WIFIS = "wifis"
private const val TABLE_WIFI_SCANS = "wifi_scans"
private const val TABLE_CELLS_LEARN = "cells_learn"
private const val TABLE_WIFI_LEARN = "wifis_learn"
private const val FIELD_MCC = "mcc"
private const val FIELD_MNC = "mnc"
private const val FIELD_TYPE = "type"
private const val FIELD_LAC_TAC = "lac"
private const val FIELD_CID = "cid"
private const val FIELD_PSC = "psc"
private const val FIELD_LATITUDE = "lat"
private const val FIELD_LONGITUDE = "lon"
private const val FIELD_ACCURACY = "acc"
private const val FIELD_ALTITUDE = "alt"
private const val FIELD_ALTITUDE_ACCURACY = "alt_acc"
private const val FIELD_TIME = "time"
private const val FIELD_PRECISION = "prec"
private const val FIELD_MAC = "mac"
private const val FIELD_SCAN_HASH = "hash"
private const val FIELD_LATITUDE_HIGH = "lath"
private const val FIELD_LATITUDE_LOW = "latl"
private const val FIELD_LONGITUDE_HIGH = "lonh"
private const val FIELD_LONGITUDE_LOW = "lonl"
private const val FIELD_ALTITUDE_HIGH = "alth"
private const val FIELD_ALTITUDE_LOW = "altl"
private const val FIELD_BAD_TIME = "btime"
private const val FIELD_LEARN_RECORD_COUNT = "recs"
private const val LEARN_BASE_ACCURACY_CELL = 10_000.0
private const val LEARN_BASE_VERTICAL_ACCURACY_CELL = 5_000.0
private const val LEARN_BASE_PRECISION_CELL = 0.5
private const val LEARN_ACCURACY_FACTOR_CELL = 0.002
private const val LEARN_VERTICAL_ACCURACY_FACTOR_CELL = 0.002
private const val LEARN_PRECISION_FACTOR_CELL = 0.01
private const val LEARN_BAD_SIZE_CELL = 10_000
private const val LEARN_BASE_ACCURACY_WIFI = 200.0
private const val LEARN_BASE_VERTICAL_ACCURACY_WIFI = 100.0
private const val LEARN_BASE_PRECISION_WIFI = 0.2
private const val LEARN_ACCURACY_FACTOR_WIFI = 0.02
private const val LEARN_VERTICAL_ACCURACY_FACTOR_WIFI = 0.02
private const val LEARN_PRECISION_FACTOR_WIFI = 0.1
private const val LEARN_BAD_SIZE_WIFI = 200
private const val LEARN_BAD_CUTOFF = 1000L * 60 * 60 * 24 * 14
private const val CELLS_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ? AND $FIELD_TYPE = ? AND $FIELD_LAC_TAC = ? AND $FIELD_CID = ? AND $FIELD_PSC = ?"
private const val CELLS_PRE_SELECTION = "$FIELD_MCC = ? AND $FIELD_MNC = ?"
private val FIELDS_CACHE_LOCATION = arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ACCURACY, FIELD_ALTITUDE, FIELD_ALTITUDE_ACCURACY, FIELD_TIME, FIELD_PRECISION)
private val FIELDS_MID_LOCATION = arrayOf(FIELD_LATITUDE_HIGH, FIELD_LATITUDE_LOW, FIELD_LONGITUDE_HIGH, FIELD_LONGITUDE_LOW, FIELD_ALTITUDE_HIGH, FIELD_ALTITUDE_LOW, FIELD_LEARN_RECORD_COUNT, FIELD_TIME)
private val FIELDS_MID_LOCATION_GET_LEARN = FIELDS_MID_LOCATION + FIELD_BAD_TIME
private val FIELDS_CELL = arrayOf(FIELD_MCC, FIELD_MNC, FIELD_TYPE, FIELD_LAC_TAC, FIELD_CID, FIELD_PSC)
private val FIELDS_WIFI = arrayOf(FIELD_MAC)
private val FIELDS_EXPORT_DATA = arrayOf(FIELD_LATITUDE, FIELD_LONGITUDE, FIELD_ALTITUDE)
private val SUPPORTED_TYPES = listOf(
"application/vnd.microg.location.cell+csv+gzip",
"application/vnd.microg.location.cell+csv",
"application/vnd.microg.location.wifi+csv+gzip",
"application/vnd.microg.location.wifi+csv",
"application/gzip",
"application/x-gzip",
"text/csv",
)
private fun getCellSelectionArgs(cell: CellDetails): Array<String> {
return arrayOf(
cell.mcc.toString(),
cell.mnc.toString(),
cell.type.ordinal.toString(),
(cell.lac ?: cell.tac ?: 0).toString(),
cell.cid.toString(),
(cell.pscOrPci ?: 0).toString(),
)
}
private fun getWifiSelection(wifi: WifiDetails): String {
return "$FIELD_MAC = x'${wifi.macBytes.toHexString()}'"
}
private fun getCellPreSelectionArgs(cell: CellDetails): Array<String> {
return arrayOf(
cell.mcc.toString(),
cell.mnc.toString()
)
}
private fun Cursor.getSingleLocation(maxAge: Long): Location? {
return try {
if (moveToNext()) {
getLocation(maxAge)
} else {
null
}
} finally {
close()
}
}
private fun Cursor.getLocation(maxAge: Long): Location? {
if (getLong(5) > System.currentTimeMillis() - maxAge) {
if (getDouble(2) == 0.0) return NEGATIVE_CACHE_ENTRY
return Location(PROVIDER_CACHE).apply {
latitude = getDouble(0)
longitude = getDouble(1)
accuracy = getDouble(2).toFloat()
getDoubleOrNull(3)?.let { altitude = it }
verticalAccuracy = getDoubleOrNull(4)?.toFloat()
time = getLong(5)
precision = getDouble(6)
}
}
return null
}
private fun Cursor.getMidLocation(
maxAge: Long = Long.MAX_VALUE,
baseAccuracy: Double = 0.0,
accuracyFactor: Double = 0.0,
baseVerticalAccuracy: Double = baseAccuracy,
verticalAccuracyFactor: Double = accuracyFactor,
basePrecision: Double = 0.0,
precisionFactor: Double = 0.0
): Location? {
if (maxAge == Long.MAX_VALUE || getLong(7) > System.currentTimeMillis() - maxAge) {
val high = Location(PROVIDER_CACHE).apply { latitude = getDouble(0); longitude = getDouble(2) }
if (!isNull(4)) high.altitude = getDouble(4)
val low = Location(PROVIDER_CACHE).apply { latitude = getDouble(1); longitude = getDouble(3) }
if (!isNull(5)) low.altitude = getDouble(5)
val count = getInt(6)
val computedAccuracy = baseAccuracy / (1 + (accuracyFactor * (count - 1).toDouble()))
val computedVerticalAccuracy = baseVerticalAccuracy / (1 + (verticalAccuracyFactor * (count - 1).toDouble()))
return Location(PROVIDER_CACHE).apply {
latitude = (high.latitude + low.latitude) / 2.0
longitude = (high.longitude + low.longitude) / 2.0
accuracy = max(high.distanceTo(low) / 2.0, computedAccuracy).toFloat()
if (high.hasAltitude() && low.hasAltitude()) {
altitude = (high.altitude + low.altitude) / 2.0
verticalAccuracy = max((abs(high.altitude - low.altitude) / 2.0), computedVerticalAccuracy).toFloat()
} else if (high.hasAltitude()) {
altitude = high.altitude
verticalAccuracy = computedVerticalAccuracy.toFloat()
} else if (low.hasAltitude()) {
altitude = low.altitude
verticalAccuracy = computedVerticalAccuracy.toFloat()
}
precision = basePrecision * (1 + (precisionFactor * (count - 1)))
highLocation = high
lowLocation = low
extras += EXTRA_RECORD_COUNT to count
}
}
return null
}
private fun Cursor.getWifiMidLocation() = getMidLocation(
MAX_LEARN_AGE,
baseAccuracy = LEARN_BASE_ACCURACY_WIFI,
accuracyFactor = LEARN_ACCURACY_FACTOR_WIFI,
baseVerticalAccuracy = LEARN_BASE_VERTICAL_ACCURACY_WIFI,
verticalAccuracyFactor = LEARN_VERTICAL_ACCURACY_FACTOR_WIFI,
basePrecision = LEARN_BASE_PRECISION_WIFI,
precisionFactor = LEARN_PRECISION_FACTOR_WIFI
)
private fun Cursor.getCellMidLocation() = getMidLocation(
MAX_LEARN_AGE,
baseAccuracy = LEARN_BASE_ACCURACY_CELL,
accuracyFactor = LEARN_ACCURACY_FACTOR_CELL,
baseVerticalAccuracy = LEARN_BASE_VERTICAL_ACCURACY_CELL,
verticalAccuracyFactor = LEARN_VERTICAL_ACCURACY_FACTOR_CELL,
basePrecision = LEARN_BASE_PRECISION_CELL,
precisionFactor = LEARN_PRECISION_FACTOR_CELL
)
private var Location.highLocation: Location?
get() = extras?.let { BundleCompat.getParcelable(it, EXTRA_HIGH_LOCATION, Location::class.java) }
set(value) { extras += EXTRA_HIGH_LOCATION to value }
private val Location.highLatitude: Double
get() = highLocation?.latitude ?: latitude
private val Location.highLongitude: Double
get() = highLocation?.longitude ?: longitude
private val Location.highAltitude: Double?
get() = highLocation?.takeIf { it.hasAltitude() }?.altitude ?: altitude.takeIf { hasAltitude() }
private var Location.lowLocation: Location?
get() = extras?.let { BundleCompat.getParcelable(it, EXTRA_LOW_LOCATION, Location::class.java) }
set(value) { extras += EXTRA_LOW_LOCATION to value }
private val Location.lowLatitude: Double
get() = lowLocation?.latitude ?: latitude
private val Location.lowLongitude: Double
get() = lowLocation?.longitude ?: longitude
private val Location.lowAltitude: Double?
get() = lowLocation?.takeIf { it.hasAltitude() }?.altitude ?: altitude.takeIf { hasAltitude() }
private var Location?.recordCount: Int
get() = this?.extras?.getInt(EXTRA_RECORD_COUNT, 0) ?: 0
set(value) { this?.extras += EXTRA_RECORD_COUNT to value }
private fun max(first: Double, second: Double?): Double {
if (second == null) return first
return max(first, second)
}
private fun max(first: Long, second: Long?): Long {
if (second == null) return first
return max(first, second)
}
private fun min(first: Double, second: Double?): Double {
if (second == null) return first
return min(first, second)
}
private fun ContentValues.putLocation(location: Location) {
if (location != NEGATIVE_CACHE_ENTRY) {
put(FIELD_LATITUDE, location.latitude)
put(FIELD_LONGITUDE, location.longitude)
put(FIELD_ACCURACY, location.accuracy)
put(FIELD_TIME, location.time)
put(FIELD_PRECISION, location.precision)
if (location.hasAltitude()) {
put(FIELD_ALTITUDE, location.altitude)
put(FIELD_ALTITUDE_ACCURACY, location.verticalAccuracy)
}
} else {
put(FIELD_LATITUDE, 0.0)
put(FIELD_LONGITUDE, 0.0)
put(FIELD_ACCURACY, 0.0)
put(FIELD_TIME, System.currentTimeMillis())
put(FIELD_PRECISION, 0.0)
}
}
private fun ContentValues.putLearnLocation(location: Location, previous: Location? = null, badTime: Long? = null, import: Boolean = false) {
if (location != NEGATIVE_CACHE_ENTRY) {
put(FIELD_LATITUDE_HIGH, max(location.latitude, previous?.highLatitude))
put(FIELD_LATITUDE_LOW, min(location.latitude, previous?.lowLatitude))
put(FIELD_LONGITUDE_HIGH, max(location.longitude, previous?.highLongitude))
put(FIELD_LONGITUDE_LOW, min(location.longitude, previous?.lowLongitude))
put(FIELD_TIME, max(location.time, previous?.time ?: 0))
if (location.hasAltitude()) {
put(FIELD_ALTITUDE_HIGH, max(location.altitude, previous?.highAltitude))
put(FIELD_ALTITUDE_LOW, min(location.altitude, previous?.lowAltitude))
}
put(FIELD_LEARN_RECORD_COUNT, previous.recordCount + 1)
if (badTime != null && !import) {
put(FIELD_BAD_TIME, max(badTime, previous?.time))
}
}
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
interface NetworkDetails {
val timestamp: Long?
val signalStrength: Int?
}

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.location.Location
import android.os.SystemClock
import android.os.WorkSource
import org.microg.gms.location.EXTRA_LOCATION
import org.microg.gms.location.EXTRA_ELAPSED_REALTIME
class NetworkLocationRequest(
var pendingIntent: PendingIntent,
var intervalMillis: Long,
var lowPower: Boolean,
var bypass: Boolean,
var workSource: WorkSource
) {
var lastRealtime = 0L
private set
fun send(context: Context, location: Location) {
lastRealtime = SystemClock.elapsedRealtime()
pendingIntent.send(context, 0, Intent().apply {
putExtra(EXTRA_LOCATION, location)
putExtra(EXTRA_ELAPSED_REALTIME, lastRealtime)
})
}
}

View file

@ -0,0 +1,619 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.location.Location
import android.location.LocationManager
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.*
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.annotation.GuardedBy
import androidx.core.content.getSystemService
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.microg.gms.location.*
import org.microg.gms.location.network.cell.CellDetails
import org.microg.gms.location.network.cell.CellDetailsCallback
import org.microg.gms.location.network.cell.CellDetailsSource
import org.microg.gms.location.network.ichnaea.IchnaeaServiceClient
import org.microg.gms.location.network.wifi.*
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.*
import kotlin.math.*
class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDetailsCallback {
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
@GuardedBy("activeRequests")
private val activeRequests = HashSet<NetworkLocationRequest>()
private val highPowerScanRunnable = Runnable { this.scan(false) }
private val lowPowerScanRunnable = Runnable { this.scan(true) }
private var wifiDetailsSource: WifiDetailsSource? = null
private var cellDetailsSource: CellDetailsSource? = null
private val ichnaea by lazy { IchnaeaServiceClient(this) }
private val database by lazy { LocationDatabase(this) }
private val movingWifiHelper by lazy { MovingWifiHelper(this) }
private val settings by lazy { LocationSettings(this) }
private var lastHighPowerScanRealtime = 0L
private var lastLowPowerScanRealtime = 0L
private var highPowerIntervalMillis = Long.MAX_VALUE
private var lowPowerIntervalMillis = Long.MAX_VALUE
private var lastWifiDetailsRealtimeMillis = 0L
private var lastCellDetailsRealtimeMillis = 0L
private val locationLock = Any()
private var lastWifiLocation: Location? = null
private var lastCellLocation: Location? = null
private var lastLocation: Location? = null
private val passiveLocationListener by lazy { LocationListenerCompat { onNewPassiveLocation(it) } }
@GuardedBy("gpsLocationBuffer")
private val gpsLocationBuffer = LinkedList<Location>()
private var passiveListenerActive = false
private var currentLocalMovingWifi: WifiDetails? = null
private var lastLocalMovingWifiLocationCandidate: Location? = null
private val interval: Long
get() = min(highPowerIntervalMillis, lowPowerIntervalMillis)
override fun onCreate() {
super.onCreate()
handlerThread = HandlerThread(NetworkLocationService::class.java.simpleName)
handlerThread.start()
handler = Handler(handlerThread.looper)
wifiDetailsSource = WifiDetailsSource.create(this, this).apply { enable() }
cellDetailsSource = CellDetailsSource.create(this, this).apply { enable() }
if (settings.effectiveEndpoint == null && (settings.wifiIchnaea || settings.cellIchnaea)) {
sendBroadcast(Intent(ACTION_CONFIGURATION_REQUIRED).apply {
`package` = packageName
putExtra(EXTRA_CONFIGURATION, CONFIGURATION_FIELD_ONLINE_SOURCE)
})
}
}
private fun updatePassiveGpsListenerRegistration() {
try {
getSystemService<LocationManager>()?.let { locationManager ->
if ((settings.cellLearning || settings.wifiLearning) && (highPowerIntervalMillis != Long.MAX_VALUE)) {
if (!passiveListenerActive) {
LocationManagerCompat.requestLocationUpdates(
locationManager,
LocationManager.PASSIVE_PROVIDER,
LocationRequestCompat.Builder(LocationRequestCompat.PASSIVE_INTERVAL)
.setQuality(LocationRequestCompat.QUALITY_LOW_POWER)
.setMinUpdateIntervalMillis(GPS_PASSIVE_INTERVAL)
.build(),
passiveLocationListener,
handlerThread.looper
)
passiveListenerActive = true
}
} else {
if (passiveListenerActive) {
LocationManagerCompat.removeUpdates(locationManager, passiveLocationListener)
passiveListenerActive = false
}
}
}
} catch (e: SecurityException) {
Log.d(TAG, "GPS location retriever not initialized due to lack of permission")
} catch (e: Exception) {
Log.d(TAG, "GPS location retriever not initialized", e)
}
}
@SuppressLint("WrongConstant")
private fun scan(lowPower: Boolean) {
if (!lowPower) lastHighPowerScanRealtime = SystemClock.elapsedRealtime()
lastLowPowerScanRealtime = SystemClock.elapsedRealtime()
val currentLocalMovingWifi = currentLocalMovingWifi
val lastWifiScanIsSufficientlyNewForMoving = lastWifiDetailsRealtimeMillis > SystemClock.elapsedRealtime() - MAX_LOCAL_WIFI_SCAN_AGE_MS
val movingWifiWasProducingRecentResults = (lastLocalMovingWifiLocationCandidate?.elapsedMillis ?: 0L) > SystemClock.elapsedRealtime() - max(MAX_LOCAL_WIFI_AGE_MS, interval * 2)
val movingWifiLocationWasAccurate = (lastLocalMovingWifiLocationCandidate?.accuracy ?: Float.MAX_VALUE) <= MOVING_WIFI_HIGH_POWER_ACCURACY
if (currentLocalMovingWifi != null &&
movingWifiWasProducingRecentResults &&
lastWifiScanIsSufficientlyNewForMoving &&
(movingWifiLocationWasAccurate || lowPower) &&
getSystemService<WifiManager>()?.connectionInfo?.bssid == currentLocalMovingWifi.macAddress
) {
Log.d(TAG, "Skip network scan and use current local wifi instead. low=$lowPower accurate=$movingWifiLocationWasAccurate")
onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis())))
} else {
val workSource = synchronized(activeRequests) { activeRequests.minByOrNull { it.intervalMillis }?.workSource }
Log.d(TAG, "Start network scan for $workSource")
if (settings.wifiLearning || settings.wifiCaching || settings.wifiIchnaea) {
wifiDetailsSource?.startScan(workSource)
} else if (settings.wifiMoving) {
// No need to scan if only moving wifi enabled, instead simulate scan based on current connection info
val connectionInfo = getSystemService<WifiManager>()?.connectionInfo
if (SDK_INT >= 31 && connectionInfo != null) {
onWifiDetailsAvailable(listOf(connectionInfo.toWifiDetails()))
} else if (currentLocalMovingWifi != null && connectionInfo?.bssid == currentLocalMovingWifi.macAddress) {
onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis())))
} else {
// Can't simulate scan, so just scan
wifiDetailsSource?.startScan(workSource)
}
}
if (settings.cellLearning || settings.cellCaching || settings.cellIchnaea) {
cellDetailsSource?.startScan(workSource)
}
}
updateRequests()
}
private fun updateRequests(forceNow: Boolean = false, lowPower: Boolean = true) {
synchronized(activeRequests) {
lowPowerIntervalMillis = Long.MAX_VALUE
highPowerIntervalMillis = Long.MAX_VALUE
for (request in activeRequests) {
if (request.lowPower) lowPowerIntervalMillis = min(lowPowerIntervalMillis, request.intervalMillis)
else highPowerIntervalMillis = min(highPowerIntervalMillis, request.intervalMillis)
}
}
// Low power must be strictly less than high power
if (highPowerIntervalMillis <= lowPowerIntervalMillis) lowPowerIntervalMillis = Long.MAX_VALUE
val nextHighPowerRequestIn =
if (highPowerIntervalMillis == Long.MAX_VALUE) Long.MAX_VALUE else highPowerIntervalMillis - (SystemClock.elapsedRealtime() - lastHighPowerScanRealtime)
val nextLowPowerRequestIn =
if (lowPowerIntervalMillis == Long.MAX_VALUE) Long.MAX_VALUE else lowPowerIntervalMillis - (SystemClock.elapsedRealtime() - lastLowPowerScanRealtime)
handler.removeCallbacks(highPowerScanRunnable)
handler.removeCallbacks(lowPowerScanRunnable)
if ((forceNow && !lowPower) || nextHighPowerRequestIn <= 0) {
Log.d(TAG, "Schedule high-power scan now")
handler.post(highPowerScanRunnable)
} else if (forceNow || nextLowPowerRequestIn <= 0) {
Log.d(TAG, "Schedule low-power scan now")
handler.post(lowPowerScanRunnable)
} else {
// Reschedule next request
if (nextLowPowerRequestIn < nextHighPowerRequestIn) {
Log.d(TAG, "Schedule low-power scan in ${nextLowPowerRequestIn}ms")
handler.postDelayed(lowPowerScanRunnable, nextLowPowerRequestIn)
} else if (nextHighPowerRequestIn != Long.MAX_VALUE) {
Log.d(TAG, "Schedule high-power scan in ${nextHighPowerRequestIn}ms")
handler.postDelayed(highPowerScanRunnable, nextHighPowerRequestIn)
}
}
updatePassiveGpsListenerRegistration()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_NETWORK_LOCATION_SERVICE) {
handler.post {
val pendingIntent = intent.getParcelableExtra<PendingIntent>(EXTRA_PENDING_INTENT) ?: return@post
val enable = intent.getBooleanExtra(EXTRA_ENABLE, false)
if (enable) {
val intervalMillis = intent.getLongExtra(EXTRA_INTERVAL_MILLIS, -1L)
if (intervalMillis < 0) return@post
var forceNow = intent.getBooleanExtra(EXTRA_FORCE_NOW, false)
val lowPower = intent.getBooleanExtra(EXTRA_LOW_POWER, true)
val bypass = intent.getBooleanExtra(EXTRA_BYPASS, false)
val workSource = intent.getParcelableExtra(EXTRA_WORK_SOURCE) ?: WorkSource()
synchronized(activeRequests) {
if (activeRequests.any { it.pendingIntent == pendingIntent }) {
forceNow = false
activeRequests.removeAll { it.pendingIntent == pendingIntent }
}
activeRequests.add(NetworkLocationRequest(pendingIntent, intervalMillis, lowPower, bypass, workSource))
}
handler.post { updateRequests(forceNow, lowPower) }
} else {
synchronized(activeRequests) {
activeRequests.removeAll { it.pendingIntent == pendingIntent }
}
handler.post { updateRequests() }
}
}
} else if (intent?.action == ACTION_NETWORK_IMPORT_EXPORT) {
handler.post {
val callback = intent.getParcelableExtra<Messenger>(EXTRA_MESSENGER)
val replyWhat = intent.getIntExtra(EXTRA_REPLY_WHAT, 0)
when (intent.getStringExtra(EXTRA_DIRECTION)) {
DIRECTION_EXPORT -> {
val name = intent.getStringExtra(EXTRA_NAME)
val uri = name?.let { database.exportLearned(it) }
callback?.send(Message.obtain().apply {
what = replyWhat
data = bundleOf(
EXTRA_DIRECTION to DIRECTION_EXPORT,
EXTRA_NAME to name,
EXTRA_URI to uri,
)
})
}
DIRECTION_IMPORT -> {
val uri = intent.getParcelableExtra<Uri>(EXTRA_URI)
val counter = uri?.let { database.importLearned(it) } ?: 0
callback?.send(Message.obtain().apply {
what = replyWhat
arg1 = counter
data = bundleOf(
EXTRA_DIRECTION to DIRECTION_IMPORT,
EXTRA_URI to uri,
)
})
}
}
}
}
super.onStartCommand(intent, flags, startId)
return START_STICKY
}
override fun onDestroy() {
handlerThread.stop()
wifiDetailsSource?.disable()
wifiDetailsSource = null
cellDetailsSource?.disable()
cellDetailsSource = null
super.onDestroy()
}
fun Location.mayTakeAltitude(location: Location?) {
if (location != null && !hasAltitude() && location.hasAltitude()) {
altitude = location.altitude
verticalAccuracy = location.verticalAccuracy
}
}
suspend fun queryWifiLocation(wifis: List<WifiDetails>): Location? {
var candidate: Location? = queryCurrentLocalMovingWifiLocation()
if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f) return candidate
val databaseCandidate = queryWifiLocationFromDatabase(wifis)
if (databaseCandidate != null && (candidate == null || databaseCandidate.precision > candidate.precision)) {
databaseCandidate.mayTakeAltitude(candidate)
candidate = databaseCandidate
}
if ((candidate?.accuracy ?: Float.MAX_VALUE) <= 50f && (candidate?.precision ?: 0.0) > 1.0) return candidate
val ichnaeaCandidate = queryIchnaeaWifiLocation(wifis, minimumPrecision = (candidate?.precision ?: 0.0))
if (ichnaeaCandidate != null && ichnaeaCandidate.accuracy >= (candidate?.accuracy ?: 0f)) {
ichnaeaCandidate.mayTakeAltitude(candidate)
candidate = ichnaeaCandidate
}
return candidate
}
private suspend fun queryCurrentLocalMovingWifiLocation(): Location? {
var candidate: Location? = null
val currentLocalMovingWifi = currentLocalMovingWifi
if (currentLocalMovingWifi != null && settings.wifiMoving) {
try {
withTimeout(5000L) {
lastLocalMovingWifiLocationCandidate = movingWifiHelper.retrieveMovingLocation(currentLocalMovingWifi)
}
candidate = lastLocalMovingWifiLocationCandidate
} catch (e: Exception) {
lastLocalMovingWifiLocationCandidate = null
Log.w(TAG, "Failed retrieving location for current moving wifi ${currentLocalMovingWifi.ssid}", e)
}
}
return candidate
}
private suspend fun queryWifiLocationFromDatabase(wifis: List<WifiDetails>): Location? =
queryLocationFromRetriever(wifis, 1000.0) { database.getWifiLocation(it, settings.wifiLearning) }
private suspend fun queryCellLocationFromDatabase(cells: List<CellDetails>): Location? =
queryLocationFromRetriever(cells, 50000.0) { it.location ?: database.getCellLocation(it, settings.cellLearning) }
private val NetworkDetails.signalStrengthBounded: Int
get() = (signalStrength ?: -100).coerceIn(-100, -10)
private val NetworkDetails.ageBounded: Long
get() = (System.currentTimeMillis() - (timestamp ?: 0)).coerceIn(0, 60000)
private val NetworkDetails.weight: Double
get() = min(1.0, sqrt(2000.0 / ageBounded)) / signalStrengthBounded.toDouble().pow(2)
private fun <T: NetworkDetails> queryLocationFromRetriever(data: List<T>, maxClusterDistance: Double = 0.0, retriever: (T) -> Location?): Location? {
val locations = data.mapNotNull { detail -> retriever(detail)?.takeIf { it != NEGATIVE_CACHE_ENTRY }?.let { detail to it } }
if (locations.isNotEmpty()) {
val clusters = locations.map { mutableListOf(it) }
for (cellLocation in locations) {
for (cluster in clusters) {
if (cluster.first() == cellLocation) continue;
if (cluster.first().second.distanceTo(cellLocation.second) < max(cluster.first().second.accuracy * 2.0, maxClusterDistance)) {
cluster.add(cellLocation)
}
}
}
val cluster = clusters.maxBy { it.sumOf { it.second.precision } }
return Location(PROVIDER_CACHE).apply {
latitude = cluster.weightedAverage { it.second.latitude to it.first.weight }
longitude = cluster.weightedAverage { it.second.longitude to it.first.weight }
accuracy = min(
cluster.map { it.second.distanceTo(this) + it.second.accuracy }.average().toFloat() / cluster.size,
cluster.minOf { it.second.accuracy }
)
val altitudeCluster = cluster.filter { it.second.hasAltitude() }.takeIf { it.isNotEmpty() }
if (altitudeCluster != null) {
altitude = altitudeCluster.weightedAverage { it.second.altitude to it.first.weight }
verticalAccuracy = min(
altitudeCluster.map { abs(altitude - it.second.altitude) + (it.second.verticalAccuracy ?: it.second.accuracy) }.average().toFloat() / cluster.size,
altitudeCluster.minOf { it.second.verticalAccuracy ?: it.second.accuracy }
)
}
precision = cluster.sumOf { it.second.precision }
time = System.currentTimeMillis()
}
}
return null
}
private suspend fun queryIchnaeaWifiLocation(wifis: List<WifiDetails>, minimumPrecision: Double = 0.0): Location? {
if (settings.wifiIchnaea && wifis.size >= 3 && wifis.size / IchnaeaServiceClient.WIFI_BASE_PRECISION_COUNT >= minimumPrecision) {
try {
val ichnaeaCandidate = ichnaea.retrieveMultiWifiLocation(wifis) { wifi, location ->
if (settings.wifiCaching) database.putWifiLocation(wifi, location)
}!!
ichnaeaCandidate.time = System.currentTimeMillis()
return ichnaeaCandidate
} catch (e: Exception) {
Log.w(TAG, "Failed retrieving location for ${wifis.size} wifi networks", e)
}
}
return null
}
private fun <T> List<T>.weightedAverage(f: (T) -> Pair<Double, Double>): Double {
val valuesAndWeights = map { f(it) }
return valuesAndWeights.sumOf { it.first * it.second } / valuesAndWeights.sumOf { it.second }
}
override fun onWifiDetailsAvailable(wifis: List<WifiDetails>) {
if (wifis.isEmpty()) return
val scanResultTimestamp = min(wifis.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis())
val scanResultRealtimeMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp)
@Suppress("DEPRECATION")
currentLocalMovingWifi = getSystemService<WifiManager>()?.connectionInfo
?.let { wifiInfo -> wifis.filter { it.macAddress == wifiInfo.bssid && it.isMoving } }
?.filter { movingWifiHelper.isLocallyRetrievable(it) }
?.singleOrNull()
val requestableWifis = wifis.filter(WifiDetails::isRequestable)
if (requestableWifis.isEmpty() && currentLocalMovingWifi == null) return
updateWifiLocation(requestableWifis, scanResultRealtimeMillis, scanResultTimestamp)
}
private fun updateWifiLocation(requestableWifis: List<WifiDetails>, scanResultRealtimeMillis: Long = 0, scanResultTimestamp: Long = 0) {
if (settings.wifiLearning) {
for (wifi in requestableWifis.filter { it.timestamp != null }) {
val wifiElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - wifi.timestamp!!)
getGpsLocation(wifiElapsedMillis)?.let {
database.learnWifiLocation(wifi, it)
}
}
}
if (scanResultRealtimeMillis < lastWifiDetailsRealtimeMillis + interval / 2 && lastWifiDetailsRealtimeMillis != 0L && scanResultRealtimeMillis != 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 {
val location = queryWifiLocation(requestableWifis)
if (location == null) {
lastWifiDetailsRealtimeMillis = previousLastRealtimeMillis
return@launch
}
if (scanResultTimestamp != 0L) location.time = max(scanResultTimestamp, location.time)
if (scanResultRealtimeMillis != 0L) location.elapsedRealtimeNanos = max(location.elapsedRealtimeNanos, scanResultRealtimeMillis * 1_000_000L)
synchronized(locationLock) {
lastWifiLocation = location
}
sendLocationUpdate()
}
}
override fun onWifiSourceFailed() {
// Wifi source failed, create a new one
wifiDetailsSource?.disable()
wifiDetailsSource = WifiDetailsSource.create(this, this).apply { enable() }
}
private suspend fun queryCellLocation(cells: List<CellDetails>): Location? {
val candidate = queryCellLocationFromDatabase(cells)
if ((candidate?.precision ?: 0.0) > 1.0) return candidate
val cellsToUpdate = cells.filter { it.location == null && database.getCellLocation(it, settings.cellLearning) == null }
for (cell in cellsToUpdate) {
queryIchnaeaCellLocation(cell)
}
// Try again after fetching records from internet
return queryCellLocationFromDatabase(cells)
}
private suspend fun queryIchnaeaCellLocation(cell: CellDetails): Location? {
if (settings.cellIchnaea) {
try {
val ichnaeaCandidate = ichnaea.retrieveSingleCellLocation(cell) { cell, location ->
if (settings.cellCaching) database.putCellLocation(cell, location)
} ?: NEGATIVE_CACHE_ENTRY
if (settings.cellCaching) {
if (ichnaeaCandidate == NEGATIVE_CACHE_ENTRY) {
database.putCellLocation(cell, NEGATIVE_CACHE_ENTRY)
return null
} else {
ichnaeaCandidate.time = System.currentTimeMillis()
database.putCellLocation(cell, ichnaeaCandidate)
return ichnaeaCandidate
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed retrieving location for cell network", e)
}
}
return null
}
override fun onCellDetailsAvailable(cells: List<CellDetails>) {
if (settings.cellLearning) {
for (cell in cells.filter { it.timestamp != null && it.location == null }) {
val cellElapsedMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - cell.timestamp!!)
getGpsLocation(cellElapsedMillis)?.let {
database.learnCellLocation(cell, it)
}
}
}
val scanResultTimestamp = min(cells.maxOf { it.timestamp ?: Long.MAX_VALUE }, System.currentTimeMillis())
val scanResultRealtimeMillis = SystemClock.elapsedRealtime() - (System.currentTimeMillis() - scanResultTimestamp)
if (scanResultRealtimeMillis < lastCellDetailsRealtimeMillis + interval / 2 && lastCellDetailsRealtimeMillis != 0L) {
Log.d(TAG, "Ignoring cell details, similar age as last ($scanResultRealtimeMillis < $lastCellDetailsRealtimeMillis + $interval / 2)")
return
}
val previousLastRealtimeMillis = lastWifiDetailsRealtimeMillis
lastCellDetailsRealtimeMillis = scanResultRealtimeMillis
lifecycleScope.launch {
val location = queryCellLocation(cells)
if (location == null) {
lastCellDetailsRealtimeMillis = previousLastRealtimeMillis
return@launch
}
if (scanResultTimestamp != 0L) location.time = max(scanResultTimestamp, location.time)
if (scanResultRealtimeMillis != 0L) location.elapsedRealtimeNanos = max(location.elapsedRealtimeNanos, scanResultRealtimeMillis * 1_000_000L)
synchronized(locationLock) {
lastCellLocation = location
}
sendLocationUpdate()
}
}
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 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 && lastLocation.let { it != null && location.accuracy > it.accuracy } && !now) {
Log.d(TAG, "Debounce inaccurate location update")
handler.postDelayed({
sendLocationUpdate(true)
}, DEBOUNCE_DELAY_MS)
return
}
lastLocation = location
synchronized(activeRequests) {
for (request in activeRequests.toList()) {
try {
request.send(this@NetworkLocationService, location)
} catch (e: Exception) {
Log.w(TAG, "Pending intent error $request")
activeRequests.remove(request)
}
}
}
}
private fun onNewPassiveLocation(location: Location) {
if (location.provider != LocationManager.GPS_PROVIDER || location.accuracy > GPS_PASSIVE_MIN_ACCURACY) return
synchronized(gpsLocationBuffer) {
if (gpsLocationBuffer.isNotEmpty() && gpsLocationBuffer.last.elapsedMillis < SystemClock.elapsedRealtime() - GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL) {
gpsLocationBuffer.clear()
} else if (gpsLocationBuffer.size >= GPS_BUFFER_SIZE) {
gpsLocationBuffer.remove()
}
gpsLocationBuffer.offer(location)
}
}
private fun getGpsLocation(elapsedMillis: Long): Location? {
if (elapsedMillis + GPS_BUFFER_SIZE * GPS_PASSIVE_INTERVAL < SystemClock.elapsedRealtime()) return null
synchronized(gpsLocationBuffer) {
if (gpsLocationBuffer.isEmpty()) return null
for (location in gpsLocationBuffer.descendingIterator()) {
if (location.elapsedMillis in (elapsedMillis - GPS_PASSIVE_INTERVAL)..(elapsedMillis + GPS_PASSIVE_INTERVAL)) return location
if (location.elapsedMillis < elapsedMillis) return null
}
}
return null
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter, args: Array<out String>?) {
writer.println("Last scan elapsed realtime: high-power: ${lastHighPowerScanRealtime.formatRealtime()}, low-power: ${lastLowPowerScanRealtime.formatRealtime()}")
writer.println("Last scan result time: wifi: ${lastWifiDetailsRealtimeMillis.formatRealtime()}, cells: ${lastCellDetailsRealtimeMillis.formatRealtime()}")
writer.println("Interval: high-power: ${highPowerIntervalMillis.formatDuration()}, low-power: ${lowPowerIntervalMillis.formatDuration()}")
writer.println("Last wifi location: $lastWifiLocation${if (lastWifiLocation == lastLocation) " (active)" else ""}")
writer.println("Last cell location: $lastCellLocation${if (lastCellLocation == lastLocation) " (active)" else ""}")
writer.println("Wifi settings: ichnaea=${settings.wifiIchnaea} moving=${settings.wifiMoving} learn=${settings.wifiLearning}")
writer.println("Cell settings: ichnaea=${settings.cellIchnaea} learn=${settings.cellLearning}")
writer.println("Ichnaea settings: source=${settings.onlineSource?.id} endpoint=${settings.effectiveEndpoint} contribute=${settings.ichnaeaContribute}")
ichnaea.dump(writer)
writer.println("GPS location buffer size=${gpsLocationBuffer.size} first=${gpsLocationBuffer.firstOrNull()?.elapsedMillis?.formatRealtime()} last=${gpsLocationBuffer.lastOrNull()?.elapsedMillis?.formatRealtime()}")
database.dump(writer)
synchronized(activeRequests) {
if (activeRequests.isNotEmpty()) {
writer.println("Active requests:")
for (request in activeRequests) {
writer.println("- ${request.workSource} ${request.intervalMillis.formatDuration()} (low power: ${request.lowPower}, bypass: ${request.bypass}) reported ${request.lastRealtime.formatRealtime()}")
}
}
}
}
companion object {
const val GPS_BUFFER_SIZE = 60
const val GPS_PASSIVE_INTERVAL = 1000L
const val GPS_PASSIVE_MIN_ACCURACY = 25f
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_MS = 60_000_000L // 1 minute
const val MAX_LOCAL_WIFI_SCAN_AGE_MS = 600_000_000L // 10 minutes
const val MOVING_WIFI_HIGH_POWER_ACCURACY = 100f
}
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.cell
import android.location.Location
import org.microg.gms.location.network.NetworkDetails
data class CellDetails(
val type: Type,
val mcc: Int? = null,
val mnc: Int? = null,
val lac: Int? = null,
val tac: Int? = null,
val cid: Long? = null,
val sid: Int? = null,
val nid: Int? = null,
val bsid: Int? = null,
val pscOrPci: Int? = null,
override val timestamp: Long? = null,
override val signalStrength: Int? = null,
val location: Location? = null
) : NetworkDetails {
companion object {
enum class Type {
CDMA, GSM, WCDMA, LTE, TDSCDMA, NR
}
}
}

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.cell
interface CellDetailsCallback {
fun onCellDetailsAvailable(cells: List<CellDetails>)
}

View file

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.cell
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.WorkSource
import android.telephony.CellInfo
import android.telephony.TelephonyManager
import android.util.Log
import androidx.core.content.getSystemService
private const val TAG = "CellDetailsSource"
class CellDetailsSource(private val context: Context, private val callback: CellDetailsCallback) {
fun enable() = Unit
fun disable() = Unit
@SuppressLint("MissingPermission")
fun startScan(workSource: WorkSource?) {
val telephonyManager = context.getSystemService<TelephonyManager>() ?: return
if (SDK_INT >= 29) {
try {
telephonyManager.requestCellInfoUpdate(context.mainExecutor, object : TelephonyManager.CellInfoCallback() {
override fun onCellInfo(cells: MutableList<CellInfo>) {
val details = cells.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid)
if (details.isNotEmpty()) callback.onCellDetailsAvailable(details)
}
})
} catch (e: SecurityException) {
// It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted
Log.w(TAG, "requestCellInfoUpdate in startScan failed", e)
}
return
} else if (SDK_INT >= 17) {
val allCellInfo: List<CellInfo>? = try {
telephonyManager.allCellInfo
} catch (e: SecurityException) {
// It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted
Log.w(TAG, "allCellInfo in startScan failed", e)
null
}
if (allCellInfo != null) {
val details = allCellInfo.map(CellInfo::toCellDetails).map { it.repair(context) }.filter(CellDetails::isValid)
if (details.isNotEmpty()) {
callback.onCellDetailsAvailable(details)
return
}
}
}
val networkOperator = telephonyManager.networkOperator
if (networkOperator != null && networkOperator.length > 4) {
val mcc = networkOperator.substring(0, 3).toIntOrNull()
val mnc = networkOperator.substring(3).toIntOrNull()
val detail: CellDetails? = try {
telephonyManager.cellLocation?.toCellDetails(mcc, mnc)
} catch (e: SecurityException) {
// It may trigger a SecurityException if the ACCESS_FINE_LOCATION permission isn't granted
Log.w(TAG, "cellLocation in startScan failed", e)
null
}
if (detail?.isValid == true) callback.onCellDetailsAvailable(listOf(detail))
}
}
companion object {
fun create(context: Context, callback: CellDetailsCallback): CellDetailsSource = CellDetailsSource(context, callback)
}
}

View file

@ -0,0 +1,159 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.cell
import android.content.Context
import android.location.Location
import android.os.Build.VERSION.SDK_INT
import android.os.SystemClock
import android.telephony.CellIdentity
import android.telephony.CellIdentityCdma
import android.telephony.CellIdentityGsm
import android.telephony.CellIdentityLte
import android.telephony.CellIdentityNr
import android.telephony.CellIdentityTdscdma
import android.telephony.CellIdentityWcdma
import android.telephony.CellInfo
import android.telephony.CellInfoCdma
import android.telephony.CellInfoGsm
import android.telephony.CellInfoLte
import android.telephony.CellInfoTdscdma
import android.telephony.CellInfoWcdma
import android.telephony.CellLocation
import android.telephony.TelephonyManager
import android.telephony.cdma.CdmaCellLocation
import android.telephony.gsm.GsmCellLocation
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
private fun locationFromCdma(latitude: Int, longitude: Int) = if (latitude == Int.MAX_VALUE || longitude == Int.MAX_VALUE) null else Location("cdma").also {
it.latitude = latitude.toDouble() / 14400.0
it.longitude = longitude.toDouble() / 14400.0
it.accuracy = 30000f
}
private fun CdmaCellLocation.toCellDetails(timestamp: Long? = null) = CellDetails(
type = CellDetails.Companion.Type.CDMA,
sid = systemId,
nid = networkId,
bsid = baseStationId,
location = locationFromCdma(baseStationLatitude, baseStationLongitude),
timestamp = timestamp
)
private fun GsmCellLocation.toCellDetails(mcc: Int? = null, mnc: Int? = null, timestamp: Long? = null) = CellDetails(
type = CellDetails.Companion.Type.GSM,
mcc = mcc,
mnc = mnc,
lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong(),
pscOrPci = psc.takeIf { it != Int.MAX_VALUE && it != -1 },
timestamp = timestamp
)
internal fun CellLocation.toCellDetails(mcc: Int? = null, mnc: Int? = null, timestamp: Long? = null) = when (this) {
is CdmaCellLocation -> toCellDetails(timestamp)
is GsmCellLocation -> toCellDetails(mcc, mnc, timestamp)
else -> throw IllegalArgumentException("Unknown CellLocation type")
}
private fun CellIdentityCdma.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.CDMA,
sid = systemId,
nid = networkId,
bsid = basestationId,
location = locationFromCdma(latitude, longitude)
)
private fun CellIdentityGsm.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.GSM,
mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 },
mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 },
lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong()
)
private fun CellIdentityWcdma.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.WCDMA,
mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 },
mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 },
lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong(),
pscOrPci = psc.takeIf { it != Int.MAX_VALUE && it != -1 }
)
private fun CellIdentityLte.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.LTE,
mcc = if (SDK_INT >= 28) mccString?.toIntOrNull() else mcc.takeIf { it != Int.MAX_VALUE && it != -1 },
mnc = if (SDK_INT >= 28) mncString?.toIntOrNull() else mnc.takeIf { it != Int.MAX_VALUE && it != -1 },
tac = tac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = ci.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong(),
pscOrPci = pci.takeIf { it != Int.MAX_VALUE && it != -1 }
)
@RequiresApi(28)
private fun CellIdentityTdscdma.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.TDSCDMA,
mcc = mccString?.toIntOrNull(),
mnc = mncString?.toIntOrNull(),
lac = lac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = cid.takeIf { it != Int.MAX_VALUE && it != -1 }?.toLong()
)
@RequiresApi(29)
private fun CellIdentityNr.toCellDetails() = CellDetails(
type = CellDetails.Companion.Type.NR,
mcc = mccString?.toIntOrNull(),
mnc = mncString?.toIntOrNull(),
tac = tac.takeIf { it != Int.MAX_VALUE && it != -1 },
cid = nci.takeIf { it != Long.MAX_VALUE && it != -1L }
)
@RequiresApi(28)
internal fun CellIdentity.toCellDetails() = when {
this is CellIdentityCdma -> toCellDetails()
this is CellIdentityGsm -> toCellDetails()
this is CellIdentityWcdma -> toCellDetails()
this is CellIdentityLte -> toCellDetails()
this is CellIdentityTdscdma -> toCellDetails()
SDK_INT >= 29 && this is CellIdentityNr -> toCellDetails()
else -> throw IllegalArgumentException("Unknown CellIdentity type")
}
private val CellInfo.epochTimestamp: Long
@RequiresApi(17)
get() = if (SDK_INT >= 30) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - timestampMillis)
else System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() - timeStamp)
@RequiresApi(17)
internal fun CellInfo.toCellDetails() = when {
this is CellInfoCdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
this is CellInfoGsm -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
SDK_INT >= 18 && this is CellInfoWcdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
this is CellInfoLte -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
SDK_INT >= 29 && this is CellInfoTdscdma -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
SDK_INT >= 30 -> cellIdentity.toCellDetails().copy(timestamp = epochTimestamp, signalStrength = cellSignalStrength.dbm)
else -> throw IllegalArgumentException("Unknown CellInfo type")
}
/**
* Fix a few known issues in Android's parsing of MNCs
*/
internal fun CellDetails.repair(context: Context): CellDetails {
if (type == CellDetails.Companion.Type.CDMA) return this
val networkOperator = context.getSystemService<TelephonyManager>()?.networkOperator ?: return this
if (networkOperator.length < 5) return this
val networkOperatorMnc = networkOperator.substring(3).toInt()
if (networkOperator[3] == '0' && mnc == null || networkOperator.length == 5 && mnc == networkOperatorMnc * 10 + 15)
return copy(mnc = networkOperatorMnc)
return this
}
val CellDetails.isValid: Boolean
get() = when (type) {
CellDetails.Companion.Type.CDMA -> sid != null && nid != null && bsid != null
else -> mcc != null && mnc != null && cid != null && (lac != null || tac != null)
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network
import android.location.Location
import android.os.Bundle
import androidx.core.location.LocationCompat
import androidx.core.os.bundleOf
import java.security.MessageDigest
const val TAG = "NetworkLocation"
const val LOCATION_EXTRA_PRECISION = "precision"
const val PROVIDER_CACHE = "cache"
const val PROVIDER_CACHE_NEGATIVE = "cache-"
val NEGATIVE_CACHE_ENTRY = Location(PROVIDER_CACHE_NEGATIVE)
internal operator fun <T> Bundle?.plus(pair: Pair<String, T>): Bundle = this + bundleOf(pair)
internal operator fun Bundle?.plus(other: Bundle): Bundle = when {
this == null -> other
else -> Bundle(this).apply { putAll(other) }
}
internal var Location.precision: Double
get() = extras?.getDouble(LOCATION_EXTRA_PRECISION, 1.0) ?: 1.0
set(value) {
extras += LOCATION_EXTRA_PRECISION to value
}
internal var Location.verticalAccuracy: Float?
get() = if (LocationCompat.hasVerticalAccuracy(this)) LocationCompat.getVerticalAccuracyMeters(this) else null
set(value) = if (value == null) LocationCompat.removeVerticalAccuracy(this) else LocationCompat.setVerticalAccuracyMeters(this, value)
fun ByteArray.toHexString(separator: String = "") : String = joinToString(separator) { "%02x".format(it) }
fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this)

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class BluetoothBeacon(
/**
* The address of the Bluetooth Low Energy (BLE) beacon.
*/
val macAddress: String? = null,
/**
* The name of the BLE beacon.
*/
val name: String? = null,
/**
* The number of milliseconds since this BLE beacon was last seen.
*/
val age: Long? = null,
/**
* The measured signal strength of the BLE beacon in dBm.
*/
val signalStrength: Int? = null,
)

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class CellTower(
/**
* The type of radio network.
*/
val radioType: RadioType? = null,
/**
* The mobile country code.
*/
val mobileCountryCode: Int? = null,
/**
* The mobile network code.
*/
val mobileNetworkCode: Int? = null,
/**
* The location area code for GSM and WCDMA networks. The tracking area code for LTE networks.
*/
val locationAreaCode: Int? = null,
/**
* The cell id or cell identity.
*/
val cellId: Int? = null,
/**
* The number of milliseconds since this networks was last detected.
*/
val age: Long? = null,
/**
* The primary scrambling code for WCDMA and physical cell id for LTE.
*/
val psc: Int? = null,
/**
* The signal strength for this cell network, either the RSSI or RSCP.
*/
val signalStrength: Int? = null,
/**
* The timing advance value for this cell network.
*/
val timingAdvance: Int? = null,
)

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
/**
* By default, both a GeoIP based position fallback and a fallback based on cell location areas (lacs) are enabled. Omit the fallbacks section if you want to use the defaults. Change the values to false if you want to disable either of the fallbacks.
*/
data class Fallback(
/**
* If no exact cell match can be found, fall back from exact cell position estimates to more coarse grained cell location area estimates rather than going directly to an even worse GeoIP based estimate.
*/
val lacf: Boolean? = null,
/**
* If no position can be estimated based on any of the provided data points, fall back to an estimate based on a GeoIP database based on the senders IP address at the time of the query.
*/
val ipf: Boolean? = null
)

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class GeolocateRequest(
/**
* The clear text name of the cell carrier / operator.
*/
val carrier: String? = null,
/**
* Should the clients IP address be used to locate it; defaults to true.
*/
val considerIp: Boolean? = null,
/**
* The mobile country code stored on the SIM card.
*/
val homeMobileCountryCode: Int? = null,
/**
* The mobile network code stored on the SIM card.
*/
val homeMobileNetworkCode: Int? = null,
/**
* Same as the `radioType` entry in each cell record. If all the cell entries have the same `radioType`, it can be provided at the top level instead.
*/
val radioType: RadioType? = null,
val bluetoothBeacons: List<BluetoothBeacon>? = null,
val cellTowers: List<CellTower>? = null,
val wifiAccessPoints: List<WifiAccessPoint>? = null,
/**
* By default, both a GeoIP based position fallback and a fallback based on cell location areas (lacs) are enabled. Omit the fallbacks section if you want to use the defaults. Change the values to false if you want to disable either of the fallbacks.
*/
val fallbacks: Fallback? = null,
)

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class GeolocateResponse(
val location: ResponseLocation? = null,
val horizontalAccuracy: Double? = null,
val fallback: String? = null,
val error: ResponseError? = null,
// Custom
val verticalAccuracy: Double? = null,
val raw: List<RawGeolocateEntry> = emptyList(),
)

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class GeosubmitItem(
/**
* The time of observation of the data, measured in milliseconds since the UNIX epoch. Can be omitted if the observation time is very recent. The age values in each section are relative to this timestamp.
*/
val timestamp: Long? = null,
/**
* The position block contains information about where and when the data was observed.
*/
val position: GeosubmitPosition? = null,
val bluetoothBeacons: List<BluetoothBeacon>? = null,
val cellTowers: List<CellTower>? = null,
val wifiAccessPoints: List<WifiAccessPoint>? = null,
)

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class GeosubmitPosition(
/**
* The latitude of the observation (WSG 84).
*/
val latitude: Double? = null,
/**
* The longitude of the observation (WSG 84).
*/
val longitude: Double? = null,
/**
* The accuracy of the observed position in meters.
*/
val accuracy: Double? = null,
/**
* The altitude at which the data was observed in meters above sea-level.
*/
val altitude: Double? = null,
/**
* The accuracy of the altitude estimate in meters.
*/
val altitudeAccuracy: Double? = null,
/**
* The age of the position data (in milliseconds).
*/
val age: Long? = null,
/**
* The heading field denotes the direction of travel of the device and is specified in degrees, where 0° heading < 360°, counting clockwise relative to the true north.
*/
val heading: Double? = null,
/**
* The air pressure in hPa (millibar).
*/
val pressure: Double? = null,
/**
* The speed field denotes the magnitude of the horizontal component of the devices current velocity and is specified in meters per second.
*/
val speed: Double? = null,
/**
* The source of the position information. If the field is omitted, gps is assumed. The term gps is used to cover all types of satellite based positioning systems including Galileo and Glonass. Other possible values are manual for a position entered manually into the system and fused for a position obtained from a combination of other sensors or outside service queries.
*/
val source: GeosubmitSource? = null,
)

View file

@ -0,0 +1,10 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class GeosubmitRequest(
val items: List<GeosubmitItem>? = null
)

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
enum class GeosubmitSource {
GPS, MANUAL, FUSED;
override fun toString(): String {
return super.toString().lowercase()
}
}

View file

@ -0,0 +1,241 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
import android.content.Context
import android.location.Location
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.collection.LruCache
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.formatRealtime
import org.microg.gms.location.network.*
import org.microg.gms.location.network.cell.CellDetails
import org.microg.gms.location.network.precision
import org.microg.gms.location.network.verticalAccuracy
import org.microg.gms.location.network.wifi.*
import org.microg.gms.location.provider.BuildConfig
import org.microg.gms.utils.singleInstanceOf
import java.io.PrintWriter
import java.nio.ByteBuffer
import java.util.LinkedList
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.math.min
class IchnaeaServiceClient(private val context: Context) {
private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
private val settings = LocationSettings(context)
private val omitables = LinkedList<String>()
private val cache = LruCache<String, Location>(REQUEST_CACHE_SIZE)
private val start = SystemClock.elapsedRealtime()
private fun GeolocateRequest.hash(): ByteArray? {
if (cellTowers.isNullOrEmpty() && (wifiAccessPoints?.size ?: 0) < 3 || bluetoothBeacons?.isNotEmpty() == true) return null
val minAge = min(
cellTowers?.takeIf { it.isNotEmpty() }?.minOf { it.age?.takeIf { it > 0L } ?: 0L } ?: Long.MAX_VALUE,
wifiAccessPoints?.takeIf { it.isNotEmpty() }?.minOf { it.age?.takeIf { it > 0L } ?: 0L } ?: Long.MAX_VALUE
)
val buffer = ByteBuffer.allocate(8 + (cellTowers?.size ?: 0) * 23 + (wifiAccessPoints?.size ?: 0) * 8)
buffer.putInt(cellTowers?.size?: 0)
buffer.putInt(wifiAccessPoints?.size?: 0)
for (cell in cellTowers.orEmpty()) {
buffer.put(cell.radioType?.ordinal?.toByte() ?: -1)
buffer.putInt(cell.mobileCountryCode ?: -1)
buffer.putInt(cell.mobileNetworkCode ?: -1)
buffer.putInt(cell.locationAreaCode ?: -1)
buffer.putInt(cell.cellId ?: -1)
buffer.putInt(cell.psc ?: -1)
buffer.put(((cell.age?.let { it - minAge }?: 0L) / (60 * 1000)).toByte())
buffer.put(((cell.signalStrength ?: 0) / 20).toByte())
}
for (wifi in wifiAccessPoints.orEmpty()) {
buffer.put(wifi.macBytes)
buffer.put(((wifi.age?.let { it - minAge }?: 0L) / (60 * 1000)).toByte())
buffer.put(((wifi.signalStrength ?: 0) / 20).toByte())
}
return buffer.array().digest("SHA-256")
}
fun isRequestable(wifi: WifiDetails): Boolean {
return wifi.isRequestable && !omitables.contains(wifi.macClean)
}
suspend fun retrieveMultiWifiLocation(wifis: List<WifiDetails>, rawHandler: ((WifiDetails, Location) -> Unit)? = null): Location? = geoLocate(
GeolocateRequest(
considerIp = false,
wifiAccessPoints = wifis.filter { isRequestable(it) }.map(WifiDetails::toWifiAccessPoint),
fallbacks = Fallback(lacf = false, ipf = false)
),
rawWifiHandler = rawHandler
)?.apply {
precision = wifis.size.toDouble() / WIFI_BASE_PRECISION_COUNT
}
suspend fun retrieveSingleCellLocation(cell: CellDetails, rawHandler: ((CellDetails, Location) -> Unit)? = null): Location? = geoLocate(
GeolocateRequest(
considerIp = false,
radioType = cell.toCellTower().radioType,
homeMobileCountryCode = cell.toCellTower().mobileCountryCode,
homeMobileNetworkCode = cell.toCellTower().mobileNetworkCode,
cellTowers = listOf(cell.toCellTower()),
fallbacks = Fallback(
lacf = true,
ipf = false
)
),
rawCellHandler = rawHandler
)?.apply {
precision = if (extras?.getString(LOCATION_EXTRA_FALLBACK) != null) CELL_FALLBACK_PRECISION else CELL_DEFAULT_PRECISION
}
private suspend fun geoLocate(
request: GeolocateRequest,
rawWifiHandler: ((WifiDetails, Location) -> Unit)? = null,
rawCellHandler: ((CellDetails, Location) -> Unit)? = null
): Location? {
val requestHash = request.hash()
if (requestHash != null) {
val locationFromCache = cache[requestHash.toHexString()]
if (locationFromCache == NEGATIVE_CACHE_ENTRY) return null
if (locationFromCache != null) return Location(locationFromCache)
}
val response = rawGeoLocate(request)
Log.d(TAG, "$request -> $response")
for (entry in response.raw) {
if (entry.omit && entry.wifiAccessPoint?.macAddress != null) {
omitables.offer(entry.wifiAccessPoint.macAddress.lowercase().replace(":", ""))
if (omitables.size > OMITABLES_LIMIT) omitables.remove()
runCatching { rawWifiHandler?.invoke(entry.wifiAccessPoint.toWifiDetails(), NEGATIVE_CACHE_ENTRY) }
}
if (entry.omit && entry.cellTower?.radioType != null) {
runCatching { rawCellHandler?.invoke(entry.cellTower.toCellDetails(), NEGATIVE_CACHE_ENTRY) }
}
if (!entry.omit && entry.wifiAccessPoint?.macAddress != null && entry.location != null) {
val location = buildLocation(entry.location, entry.horizontalAccuracy, entry.verticalAccuracy).apply {
precision = 1.0
}
runCatching { rawWifiHandler?.invoke(entry.wifiAccessPoint.toWifiDetails(), location) }
}
if (!entry.omit && entry.cellTower?.radioType != null && entry.location != null) {
val location = buildLocation(entry.location, entry.horizontalAccuracy, entry.verticalAccuracy).apply {
precision = 1.0
}
runCatching { rawCellHandler?.invoke(entry.cellTower.toCellDetails(), location) }
}
}
val location = if (response.location != null) {
buildLocation(response.location, response.horizontalAccuracy, response.verticalAccuracy).apply {
if (response.fallback != null) extras = (extras ?: Bundle()).apply { putString(LOCATION_EXTRA_FALLBACK, response.fallback) }
}
} else if (response.error != null && response.error.code == 404) {
NEGATIVE_CACHE_ENTRY
} else if (response.error != null) {
throw ServiceException(response.error)
} else {
throw RuntimeException("Invalid response JSON")
}
if (requestHash != null) {
cache[requestHash.toHexString()] = if (location == NEGATIVE_CACHE_ENTRY) NEGATIVE_CACHE_ENTRY else Location(location)
}
if (location == NEGATIVE_CACHE_ENTRY) return null
return location
}
private fun buildLocation(location: ResponseLocation, defaultHorizontalAccuracy: Double? = null, defaultVerticalAccuracy: Double? = null): Location {
return Location(PROVIDER).apply {
latitude = location.latitude
longitude = location.longitude
if (location.altitude != null) altitude = location.altitude
if (defaultHorizontalAccuracy != null && defaultHorizontalAccuracy > 0.0) accuracy = defaultHorizontalAccuracy.toFloat()
if (hasAltitude() && defaultVerticalAccuracy != null && defaultVerticalAccuracy > 0.0) verticalAccuracy = defaultVerticalAccuracy.toFloat()
if (location.horizontalAccuracy != null && location.horizontalAccuracy > 0.0) accuracy = location.horizontalAccuracy.toFloat()
if (hasAltitude() && location.verticalAccuracy != null && location.verticalAccuracy > 0.0) verticalAccuracy = location.verticalAccuracy.toFloat()
time = System.currentTimeMillis()
}
}
private fun getRequestHeaders(): Map<String, String> = buildMap {
set("User-Agent", "${BuildConfig.ICHNAEA_USER_AGENT} (Linux; Android ${android.os.Build.VERSION.RELEASE}; ${context.packageName})")
if (settings.ichnaeaContribute) {
set("X-Ichnaea-Contribute-Opt-In", "1")
}
}
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?.latitude != 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.effectiveEndpoint).buildUpon().appendPath("v1").appendPath("geolocate").build().toString()
queue.add(object : JsonObjectRequest(Method.POST, url, request.toJson(), {
try {
continuation.resume(it.toGeolocateResponse())
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}, {
continueError(continuation, it)
}) {
override fun getHeaders(): Map<String, String> = getRequestHeaders()
})
}
private suspend fun rawGeoSubmit(request: GeosubmitRequest): Unit = suspendCoroutine { continuation ->
val url = Uri.parse(settings.effectiveEndpoint).buildUpon().appendPath("v2").appendPath("geosubmit").build().toString()
queue.add(object : JsonObjectRequest(Method.POST, url, request.toJson(), {
continuation.resume(Unit)
}, {
continuation.resumeWithException(it)
}) {
override fun getHeaders(): Map<String, String> = getRequestHeaders()
})
}
fun dump(writer: PrintWriter) {
writer.println("Ichnaea start=${start.formatRealtime()} omitables=${omitables.size}")
writer.println("Ichnaea request cache size=${cache.size()} hits=${cache.hitCount()} miss=${cache.missCount()} puts=${cache.putCount()} evicts=${cache.evictionCount()}")
}
private operator fun <K : Any, V : Any> LruCache<K, V>.set(key: K, value: V) {
put(key, value)
}
companion object {
private const val TAG = "IchnaeaLocation"
private const val PROVIDER = "ichnaea"
const val WIFI_BASE_PRECISION_COUNT = 4.0
const val CELL_DEFAULT_PRECISION = 1.0
const val CELL_FALLBACK_PRECISION = 0.5
private const val OMITABLES_LIMIT = 100
private const val REQUEST_CACHE_SIZE = 200
const val LOCATION_EXTRA_FALLBACK = "fallback"
}
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
enum class RadioType {
GSM, WCDMA, LTE;
override fun toString(): String {
return super.toString().lowercase()
}
}

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class RawGeolocateEntry(
val timestamp: Long? = null,
val bluetoothBeacon: BluetoothBeacon? = null,
val cellTower: CellTower? = null,
val wifiAccessPoint: WifiAccessPoint? = null,
val location: ResponseLocation? = null,
val horizontalAccuracy: Double? = null,
val verticalAccuracy: Double? = null,
val omit: Boolean = false,
)

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class ResponseError(
val code: Int? = null,
val message: String? = null
)

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class ResponseLocation(
val latitude: Double,
val longitude: Double,
// Custom
val horizontalAccuracy: Double? = null,
val altitude: Double? = null,
val verticalAccuracy: Double? = null
)

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
class ServiceException(val error: ResponseError) : Exception(error.message)

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
data class WifiAccessPoint(
/**
* The BSSID of the WiFi network.
*/
val macAddress: String,
/**
* The number of milliseconds since this network was last detected.
*/
val age: Long? = null,
/**
* The WiFi channel for networks in the 2.4GHz range. This often ranges from 1 to 13.
*/
val channel: Int? = null,
/**
* The frequency in MHz of the channel over which the client is communicating with the access point.
*/
val frequency: Int? = null,
/**
* The received signal strength (RSSI) in dBm.
*/
val signalStrength: Int? = null,
/**
* The current signal to noise ratio measured in dB.
*/
val signalToNoiseRatio: Int? = null,
/**
* The SSID of the Wifi network.
*/
val ssid: String? = null
) {
init {
if (ssid != null && ssid.endsWith("_nomap")) throw IllegalArgumentException("Wifi networks with a SSID ending in _nomap must not be collected.")
}
}

View file

@ -0,0 +1,260 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.ichnaea
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.microg.gms.location.network.cell.CellDetails
import org.microg.gms.location.network.wifi.WifiDetails
private fun JSONObject.getDouble(vararg names: String): Double {
for (name in names) {
if (has(name)) return getDouble(name)
}
throw JSONException("Values are all null: ${names.joinToString(", ")}")
}
private fun JSONObject.getInt(vararg names: String): Int {
for (name in names) {
if (has(name)) return getInt(name)
}
throw JSONException("Values are all null: ${names.joinToString(", ")}")
}
private fun JSONObject.getString(vararg names: String): String {
for (name in names) {
if (has(name)) return getString(name)
}
throw JSONException("Values are all null: ${names.joinToString(", ")}")
}
private fun JSONObject.optDouble(vararg names: String): Double? {
for (name in names) {
if (has(name)) optDouble(name).takeIf { it.isFinite() }?.let { return it }
}
return null
}
private fun JSONObject.optInt(vararg names: String): Int? {
for (name in names) {
runCatching { if (has(name)) return getInt(name) }
}
return null
}
private fun JSONObject.optLong(vararg names: String): Long? {
for (name in names) {
runCatching { if (has(name)) return getLong(name) }
}
return null
}
private fun JSONObject.optString(vararg names: String): String? {
for (name in names) {
if (has(name)) return optString(name)
}
return null
}
internal fun CellDetails.toCellTower() = CellTower(
radioType = when (type) {
CellDetails.Companion.Type.GSM -> RadioType.GSM
CellDetails.Companion.Type.WCDMA -> RadioType.WCDMA
CellDetails.Companion.Type.LTE -> RadioType.LTE
else -> throw IllegalArgumentException("Unsupported radio type")
},
mobileCountryCode = mcc,
mobileNetworkCode = mnc,
locationAreaCode = lac ?: tac,
cellId = cid?.toInt(),
age = timestamp?.let { System.currentTimeMillis() - it },
psc = pscOrPci,
signalStrength = signalStrength
)
internal fun CellTower.toCellDetails() = CellDetails(
type = when(radioType) {
RadioType.GSM -> CellDetails.Companion.Type.GSM
RadioType.WCDMA -> CellDetails.Companion.Type.WCDMA
RadioType.LTE -> CellDetails.Companion.Type.LTE
else -> throw IllegalArgumentException("Unsupported radio type")
},
mcc = mobileNetworkCode,
mnc = mobileNetworkCode,
lac = locationAreaCode,
tac = locationAreaCode,
cid = cellId?.toLong(),
pscOrPci = psc,
signalStrength = signalStrength
)
internal fun WifiDetails.toWifiAccessPoint() = WifiAccessPoint(
macAddress = macAddress,
age = timestamp?.let { System.currentTimeMillis() - it },
channel = channel,
frequency = frequency,
signalStrength = signalStrength,
ssid = ssid
)
internal fun WifiAccessPoint.toWifiDetails() = WifiDetails(
macAddress = macAddress,
channel = channel,
frequency = frequency,
signalStrength = signalStrength,
ssid = ssid,
)
internal fun JSONObject.toGeolocateResponse() = GeolocateResponse(
location = optJSONObject("location")?.toResponseLocation(),
horizontalAccuracy = optDouble("accuracy", "acc", "horizontalAccuracy"),
verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"),
fallback = optString("fallback").takeIf { it.isNotEmpty() },
raw = optJSONArray("raw")?.toRawGeolocateEntries().orEmpty(),
error = optJSONObject("error")?.toResponseError()
)
internal fun GeolocateRequest.toJson() = JSONObject().apply {
if (carrier != null) put("carrier", carrier)
if (considerIp != null) put("considerIp", considerIp)
if (homeMobileCountryCode != null) put("homeMobileCountryCode", homeMobileCountryCode)
if (homeMobileNetworkCode != null) put("homeMobileNetworkCode", homeMobileNetworkCode)
if (radioType != null) put("radioType", radioType.toString())
if (!bluetoothBeacons.isNullOrEmpty()) put("bluetoothBeacons", JSONArray(bluetoothBeacons.map(BluetoothBeacon::toJson)))
if (!cellTowers.isNullOrEmpty()) put("cellTowers", JSONArray(cellTowers.map(CellTower::toJson)))
if (!wifiAccessPoints.isNullOrEmpty()) put("wifiAccessPoints", JSONArray(wifiAccessPoints.map(WifiAccessPoint::toJson)))
if (fallbacks != null) put("fallbacks", fallbacks.toJson())
}
internal fun GeosubmitRequest.toJson() = JSONObject().apply {
if (items != null) put("items", JSONArray(items.map(GeosubmitItem::toJson)))
}
private fun GeosubmitItem.toJson() = JSONObject().apply {
if (timestamp != null) put("timestamp", timestamp)
if (position != null) put("position", position.toJson())
if (!bluetoothBeacons.isNullOrEmpty()) put("bluetoothBeacons", JSONArray(bluetoothBeacons.map(BluetoothBeacon::toJson)))
if (!cellTowers.isNullOrEmpty()) put("cellTowers", JSONArray(cellTowers.map(CellTower::toJson)))
if (!wifiAccessPoints.isNullOrEmpty()) put("wifiAccessPoints", JSONArray(wifiAccessPoints.map(WifiAccessPoint::toJson)))
}
private fun GeosubmitPosition.toJson() = JSONObject().apply {
if (latitude != null) put("latitude", latitude)
if (longitude != null) put("longitude", longitude)
if (accuracy != null) put("accuracy", accuracy)
if (altitude != null) put("altitude", altitude)
if (altitudeAccuracy != null) put("altitudeAccuracy", altitudeAccuracy)
if (age != null) put("age", age)
if (heading != null) put("heading", heading)
if (pressure != null) put("pressure", pressure)
if (speed != null) put("speed", speed)
if (source != null) put("source", source.toString())
}
private fun JSONObject.toResponseLocation() = ResponseLocation(
latitude = getDouble("lat", "latitude"),
longitude = getDouble("lng", "longitude"),
altitude = optDouble("alt", "altitude"),
horizontalAccuracy = optDouble("acc", "accuracy", "horizontalAccuracy"),
verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"),
)
private fun JSONObject.toResponseError() = ResponseError(
code = optInt("code", "errorCode", "statusCode"),
message = optString("message", "msg", "statusText")
)
private fun JSONArray.toRawGeolocateEntries(): List<RawGeolocateEntry> =
(0 until length()).mapNotNull { optJSONObject(it)?.toRawGeolocateEntry() }
private fun JSONObject.toRawGeolocateEntry() = RawGeolocateEntry(
timestamp = optLong("time", "timestamp"),
bluetoothBeacon = optJSONObject("bluetoothBeacon")?.toBluetoothBeacon(),
cellTower = optJSONObject("cellTower")?.toCellTower(),
wifiAccessPoint = optJSONObject("wifiAccessPoint")?.toWifiAccessPoint(),
location = optJSONObject("location")?.toResponseLocation(),
horizontalAccuracy = optDouble("acc", "accuracy", "horizontalAccuracy"),
verticalAccuracy = optDouble("altAccuracy", "altitudeAccuracy", "verticalAccuracy"),
omit = optBoolean("omit")
)
private fun JSONObject.toBluetoothBeacon() = BluetoothBeacon(
macAddress = getString("macAddress", "mac", "address"),
name = optString("name"),
)
private fun JSONObject.toCellTower() = CellTower(
radioType = optString("radioType", "radio", "type")?.let { runCatching { RadioType.valueOf(it.uppercase()) }.getOrNull() },
mobileCountryCode = optInt("mobileCountryCode", "mcc"),
mobileNetworkCode = optInt("mobileNetworkCode", "mnc"),
locationAreaCode = optInt("locationAreaCode", "lac", "trackingAreaCode", "tac"),
cellId = optInt("cellId", "cellIdentity", "cid"),
psc = optInt("psc", "primaryScramblingCode", "physicalCellId", "pci"),
)
private fun JSONObject.toWifiAccessPoint() = WifiAccessPoint(
macAddress = getString("macAddress", "mac", "bssid", "address"),
channel = optInt("channel", "chan"),
frequency = optInt("frequency", "freq"),
ssid = optString("ssid"),
)
private fun BluetoothBeacon.toJson() = JSONObject().apply {
if (macAddress != null) put("macAddress", macAddress)
if (name != null) put("name", name)
if (age != null) put("age", age)
if (signalStrength != null) put("signalStrength", signalStrength)
}
private fun CellTower.toJson() = JSONObject().apply {
if (radioType != null) put("radioType", radioType.toString())
if (mobileCountryCode != null) put("mobileCountryCode", mobileCountryCode)
if (mobileNetworkCode != null) put("mobileNetworkCode", mobileNetworkCode)
if (locationAreaCode != null) put("locationAreaCode", locationAreaCode)
if (cellId != null) put("cellId", cellId)
if (age != null) put("age", age)
if (psc != null) put("psc", psc)
if (signalStrength != null) put("signalStrength", signalStrength)
if (timingAdvance != null) put("timingAdvance", timingAdvance)
}
private fun WifiAccessPoint.toJson() = JSONObject().apply {
put("macAddress", macAddress)
if (age != null) put("age", age)
if (channel != null) put("channel", channel)
if (frequency != null) put("frequency", frequency)
if (signalStrength != null) put("signalStrength", signalStrength)
if (signalToNoiseRatio != null) put("signalToNoiseRatio", signalToNoiseRatio)
if (ssid != null) put("ssid", ssid)
}
val WifiAccessPoint.macClean: String
get() = macAddress.lowercase().replace(":", "")
val WifiAccessPoint.macBytes: ByteArray
get() {
val mac = macClean
return byteArrayOf(
mac.substring(0, 2).toInt(16).toByte(),
mac.substring(2, 4).toInt(16).toByte(),
mac.substring(4, 6).toInt(16).toByte(),
mac.substring(6, 8).toInt(16).toByte(),
mac.substring(8, 10).toInt(16).toByte(),
mac.substring(10, 12).toInt(16).toByte()
)
}
val GeolocateRequest.isWifiOnly: Boolean
get() = bluetoothBeacons.isNullOrEmpty() && cellTowers.isNullOrEmpty() && wifiAccessPoints?.isNotEmpty() == true
val GeolocateRequest.isCellOnly: Boolean
get() = bluetoothBeacons.isNullOrEmpty() && wifiAccessPoints.isNullOrEmpty() && cellTowers?.isNotEmpty() == true
private fun Fallback.toJson() = JSONObject().apply {
if (lacf != null) put("lacf", lacf)
if (ipf != null) put("ipf", ipf)
}

View file

@ -0,0 +1,515 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import android.content.Context
import android.location.Location
import android.net.ConnectivityManager
import android.net.ConnectivityManager.TYPE_WIFI
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.core.content.getSystemService
import androidx.core.location.LocationCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.location.network.TAG
import java.net.HttpURLConnection
import java.net.Proxy
import java.net.URL
import java.security.KeyStore
import java.security.cert.*
import java.text.SimpleDateFormat
import java.util.*
import javax.net.ssl.CertPathTrustManagerParameters
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
private val MOVING_WIFI_HOTSPOTS = setOf(
// Austria
"OEBB",
"Austrian FlyNet",
"svciob", // OEBB Service WIFI
// Belgium
"THALYSNET",
// Canada
"Air Canada",
"ACWiFi",
"ACWiFi.com",
// Czech Republic
"CDWiFi",
// France
"_SNCF_WIFI_INOUI",
"_SNCF_WIFI_INTERCITES",
"_WIFI_LYRIA",
"OUIFI",
"NormandieTrainConnecte",
// Germany
"WIFIonICE",
"WIFI@DB",
"WiFi@DB",
"RRX Hotspot",
"FlixBux",
"FlixBus Wi-Fi",
"FlixTrain Wi-Fi",
"FlyNet",
"Telekom_FlyNet",
"Vestische WLAN",
"agilis-Wifi",
"freeWIFIahead!",
// Greece
"AegeanWiFi",
// Hong Kong
"Cathay Pacific",
// Hungary
"MAVSTART-WIFI",
// Netherlands
"KEOLIS Nederland",
// New Zealand
"AirNZ_InflightWiFi",
"Bluebridge WiFi",
// Singapore
"KrisWorld",
// Sweden
"SJ",
"saswifi",
// Switzerland
"SBB-Free",
"SBB-FREE",
"SWISS Connect",
"Edelweiss Entertainment",
// United Kingdom
"Avanti_Free_WiFi",
"CrossCountryWiFi",
"GWR WiFi",
"LNR On Board Wi-Fi",
"LOOP on train WiFi",
"WMR On Board Wi-Fi",
"EurostarTrainsWiFi",
// United States
"Amtrak_WiFi",
)
private val PHONE_HOTSPOT_KEYWORDS = setOf(
"iPhone",
"Galaxy",
"AndroidAP"
)
/**
* A Wi-Fi hotspot that changes its location dynamically and thus is unsuitable for use with location services that assume stable locations.
*
* Some moving Wi-Fi hotspots allow to determine their location when connected or through a public network API.
*/
val WifiDetails.isMoving: Boolean
get() {
if (MOVING_WIFI_HOTSPOTS.contains(ssid)) {
return true
}
if (PHONE_HOTSPOT_KEYWORDS.any { ssid?.contains(it) == true }) {
return true
}
return false
}
const val FEET_TO_METERS = 0.3048
const val KNOTS_TO_METERS_PER_SECOND = 0.5144
const val MILES_PER_HOUR_TO_METERS_PER_SECOND = 0.447
class MovingWifiHelper(private val context: Context) {
suspend fun retrieveMovingLocation(current: WifiDetails): Location {
if (!isLocallyRetrievable(current)) throw IllegalArgumentException()
val connectivityManager = context.getSystemService<ConnectivityManager>() ?: throw IllegalStateException()
val sources = MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]!!
val exceptions = mutableListOf<Exception>()
for (source in sources) {
try {
val url = URL(source.url)
return withContext(Dispatchers.IO) {
val network = if (isLocallyRetrievable(current) && SDK_INT >= 23) {
@Suppress("DEPRECATION")
(connectivityManager.allNetworks.singleOrNull {
val networkInfo = connectivityManager.getNetworkInfo(it)
networkInfo?.type == TYPE_WIFI && networkInfo.isConnected
})
} else {
null
}
val connection = (if (SDK_INT >= 23) {
network?.openConnection(url, Proxy.NO_PROXY)
} else {
null
} ?: url.openConnection()) as HttpURLConnection
try {
connection.doInput = true
if (connection is HttpsURLConnection && SDK_INT >= 24) {
try {
val ctx = SSLContext.getInstance("TLS")
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
fun wrap(originalTrustManager: TrustManager): TrustManager {
if (originalTrustManager is X509TrustManager) {
return object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.d(TAG, "checkClientTrusted: $chain, $authType")
originalTrustManager.checkClientTrusted(chain, authType)
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.d(TAG, "checkServerTrusted: $chain, $authType")
originalTrustManager.checkServerTrusted(chain, authType)
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return originalTrustManager.acceptedIssuers
}
}
} else {
return originalTrustManager
}
}
val ks = KeyStore.getInstance("AndroidCAStore")
ks.load(null, null)
tmf.init(ks)
ctx.init(null, tmf.trustManagers.map(::wrap).toTypedArray(), null)
connection.sslSocketFactory = ctx.socketFactory
} catch (e: Exception) {
Log.w(TAG, "Failed to disable revocation", e)
}
}
if (connection.responseCode != 200) throw RuntimeException("Got error")
val location = Location(current.ssid ?: "wifi")
source.parse(location, connection.inputStream.readBytes())
} finally {
connection.inputStream.close()
connection.disconnect()
}
}
} catch (e: Exception) {
exceptions.add(e)
}
}
if (exceptions.size == 1) throw exceptions.single()
throw RuntimeException(exceptions.joinToString("\n"))
}
fun isLocallyRetrievable(wifi: WifiDetails): Boolean =
MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid)
companion object {
abstract class MovingWifiLocationSource(val url: String) {
abstract fun parse(location: Location, data: ByteArray): Location
}
private val SOURCE_WIFI_ON_ICE = object : MovingWifiLocationSource("https://iceportal.de/api1/rs/status") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid")
location.accuracy = 100f
location.time = json.getLong("serverTime") - 15000L
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = (it / 3.6).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
return location
}
}
private val SOURCE_OEBB_1 = object : MovingWifiLocationSource("https://railnet.oebb.at/assets/modules/fis/combined.json") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString()).getJSONObject("latestStatus")
location.accuracy = 100f
runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("dateTime"))?.time }.getOrNull()?.let { location.time = it }
location.latitude = json.getJSONObject("gpsPosition").getDouble("latitude")
location.longitude = json.getJSONObject("gpsPosition").getDouble("longitude")
json.getJSONObject("gpsPosition").optDouble("orientation").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = (it / 3.6).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
return location
}
}
private val SOURCE_OEBB_2 = object : MovingWifiLocationSource("https://railnet.oebb.at/api/gps") {
override fun parse(location: Location, data: ByteArray): Location {
val root = JSONObject(data.decodeToString())
if (root.has("JSON")) {
val json = root.getJSONObject("JSON")
if (!json.isNull("error")) throw RuntimeException("Error: ${json.get("error")}");
location.accuracy = 100f
location.latitude = json.getDouble("lat")
location.longitude = json.getDouble("lon")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = (it / 3.6).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
} else if (root.optDouble("Latitude").let { !it.isNaN() && it.isFinite() && it > 0.1 }) {
location.accuracy = 100f
location.latitude = root.getDouble("Latitude")
location.longitude = root.getDouble("Longitude")
} else {
throw RuntimeException("Unsupported: $root")
}
return location
}
}
private val SOURCE_FLIXBUS = object : MovingWifiLocationSource("https://media.flixbus.com/services/pis/v1/position") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
return location
}
}
class PassengeraLocationSource(base: String) : MovingWifiLocationSource("$base/portal/api/vehicle/realtime") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getDouble("gpsLat")
location.longitude = json.getDouble("gpsLng")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = (it / 3.6).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it }
return location
}
}
private val SOURCE_PASSENGERA_MAV = PassengeraLocationSource("http://portal.mav.hu")
private val SOURCE_PASSENGERA_CD = PassengeraLocationSource("http://cdwifi.cz")
private val SOURCE_DISPLAY_UGO = object : MovingWifiLocationSource("https://api.ife.ugo.aero/navigation/positions") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONArray(data.decodeToString()).getJSONObject(0)
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it }
json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let {
location.speed = (it / 3.6).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it }
json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
return location
}
}
private val SOURCE_INFLIGHT_PANASONIC = object : MovingWifiLocationSource("https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude")
location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude")
json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let {
location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS }
json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
return location
}
}
class BoardConnectLocationSource(base: String) : MovingWifiLocationSource("$base/map/api/flightData") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getDouble("lat")
location.longitude = json.getDouble("lon")
runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it }
json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let {
location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS }
json.optDouble("heading").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
return location
}
}
private val SOURCE_LUFTHANSA_FLYNET_EUROPE = BoardConnectLocationSource("https://www.lufthansa-flynet.com")
private val SOURCE_LUFTHANSA_FLYNET_EUROPE_2 = BoardConnectLocationSource("https://ww2.lufthansa-flynet.com")
private val SOURCE_AUSTRIAN_FLYNET_EUROPE = BoardConnectLocationSource("https://www.austrian-flynet.com")
class SncfLocationSource(base: String) : MovingWifiLocationSource("$base/router/api/train/gps") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
if(json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid")
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
location.time = json.getLong("timestamp")
json.optDouble("heading").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
return location
}
}
private val SOURCE_SNCF = SncfLocationSource("https://wifi.sncf")
private val SOURCE_SNCF_INTERCITES = SncfLocationSource("https://wifi.intercites.sncf")
private val SOURCE_NORMANDIE = SncfLocationSource("https://wifi.normandie.fr")
private val SOURCE_LYRIA = object : MovingWifiLocationSource("https://wifi.tgv-lyria.com/api/train/gps/position/") {
/* If there is no location available (e.g. in a tunnel), the API
endpoint returns HTTP 500, though it may reuse a previous
location for a few seconds. The returned JSON has a
"satellites" integer field, but this always seems to be 0, even
when there is a valid location available.
*/
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
// Speed is returned in m/s.
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it }
return location
}
}
private val SOURCE_OUIFI = object : MovingWifiLocationSource("https://ouifi.ouigo.com:8084/api/gps") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
if(json.has("fix") && json.getInt("fix") == -1) throw RuntimeException("GPS not valid")
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
location.time = json.getLong("timestamp")
json.optDouble("heading").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
return location
}
}
private val SOURCE_OMBORD = object : MovingWifiLocationSource("https://www.ombord.info/api/jsonp/position/") {
override fun parse(location: Location, data: ByteArray): Location {
// The API endpoint returns a JSONP object (even when no ?callback= is supplied), so strip the surrounding function call.
val json = JSONObject(data.decodeToString().trim().trim('(', ')', ';'))
// TODO: what happens in the Channel Tunnel? Does "satellites" go to zero? Does "mode" change?
if (json.has("satellites") && json.getInt("satellites") < 1) throw RuntimeException("Ombord has no GPS fix")
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
location.time = json.getLong("time")
// "cmg" means "course made good", i.e. the compass heading of the track over the ground.
// Sometimes gets stuck for a few minutes, so use a generous accuracy value.
json.optDouble("cmg").takeIf { !it.isNaN() }?.let {
location.bearing = it.toFloat()
LocationCompat.setBearingAccuracyDegrees(location, 90f)
}
json.optDouble("altitude").takeIf { !it.isNaN() }?.let {
location.altitude = it
}
return location
}
}
private val SOURCE_AIR_CANADA = object : MovingWifiLocationSource("https://airbornemedia.inflightinternet.com/asp/api/flight/info") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString()).getJSONObject("gpsData")
location.accuracy = 100f
location.latitude = json.getDouble("latitude")
location.longitude = json.getDouble("longitude")
json.optLong("utcTime").takeIf { it != 0L }?.let { location.time = it }
json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS }
json.optDouble("horizontalVelocity").takeIf { !it.isNaN() }?.let {
location.speed = (it * MILES_PER_HOUR_TO_METERS_PER_SECOND).toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
return location
}
}
private val SOURCE_HOTSPLOTS = object : MovingWifiLocationSource("http://hsp.hotsplots.net/status.json") {
override fun parse(location: Location, data: ByteArray): Location {
val json = JSONObject(data.decodeToString())
location.accuracy = 100f
location.latitude = json.getDouble("lat")
location.longitude = json.getDouble("lng")
json.optLong("ts").takeIf { it != 0L }?.let { location.time = it * 1000 }
json.optDouble("speed").takeIf { !it.isNaN() }?.let {
location.speed = it.toFloat()
LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f)
}
return location
}
}
private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE: Map<String, List<MovingWifiLocationSource>> = mapOf(
"WIFIonICE" to listOf(SOURCE_WIFI_ON_ICE),
"OEBB" to listOf(SOURCE_OEBB_2, SOURCE_OEBB_1),
"FlixBus" to listOf(SOURCE_FLIXBUS),
"FlixBus Wi-Fi" to listOf(SOURCE_FLIXBUS),
"FlixTrain Wi-Fi" to listOf(SOURCE_FLIXBUS),
"MAVSTART-WIFI" to listOf(SOURCE_PASSENGERA_MAV),
"AegeanWiFi" to listOf(SOURCE_DISPLAY_UGO),
"Telekom_FlyNet" to listOf(SOURCE_INFLIGHT_PANASONIC),
"Cathay Pacific" to listOf(SOURCE_INFLIGHT_PANASONIC),
"KrisWorld" to listOf(SOURCE_INFLIGHT_PANASONIC),
"SWISS Connect" to listOf(SOURCE_INFLIGHT_PANASONIC),
"Edelweiss Entertainment" to listOf(SOURCE_INFLIGHT_PANASONIC),
"FlyNet" to listOf(SOURCE_LUFTHANSA_FLYNET_EUROPE, SOURCE_LUFTHANSA_FLYNET_EUROPE_2),
"CDWiFi" to listOf(SOURCE_PASSENGERA_CD),
"Air Canada" to listOf(SOURCE_AIR_CANADA),
"ACWiFi" to listOf(SOURCE_AIR_CANADA),
"ACWiFi.com" to listOf(SOURCE_AIR_CANADA),
"OUIFI" to listOf(SOURCE_OUIFI),
"_SNCF_WIFI_INOUI" to listOf(SOURCE_SNCF),
"_SNCF_WIFI_INTERCITES" to listOf(SOURCE_SNCF_INTERCITES),
"_WIFI_LYRIA" to listOf(SOURCE_LYRIA),
"NormandieTrainConnecte" to listOf(SOURCE_NORMANDIE),
"agilis-Wifi" to listOf(SOURCE_HOTSPLOTS),
"Austrian FlyNet" to listOf(SOURCE_AUSTRIAN_FLYNET_EUROPE),
"EurostarTrainsWiFi" to listOf(SOURCE_OMBORD),
)
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import org.microg.gms.location.network.NetworkDetails
data class WifiDetails(
val macAddress: String,
val ssid: String? = null,
val frequency: Int? = null,
val channel: Int? = null,
override val timestamp: Long? = null,
override val signalStrength: Int? = null,
val open: Boolean = false
): NetworkDetails

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
interface WifiDetailsCallback {
fun onWifiDetailsAvailable(wifis: List<WifiDetails>)
fun onWifiSourceFailed()
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import android.content.Context
import android.os.WorkSource
interface WifiDetailsSource {
fun enable() = Unit
fun disable() = Unit
fun startScan(workSource: WorkSource?) = Unit
companion object {
fun create(context: Context, callback: WifiDetailsCallback) = when {
WifiScannerSource.isSupported(context) -> WifiScannerSource(context, callback)
else -> WifiManagerSource(context, callback)
}
}
}

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.ScanResult
import android.net.wifi.WifiManager
import android.os.WorkSource
import android.util.Log
import androidx.core.content.getSystemService
class WifiManagerSource(private val context: Context, private val callback: WifiDetailsCallback) : BroadcastReceiver(), WifiDetailsSource {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context, intent: Intent) {
try {
callback.onWifiDetailsAvailable(this.context.getSystemService<WifiManager>()?.scanResults.orEmpty().map(ScanResult::toWifiDetails))
} catch (e: Exception) {
Log.w(org.microg.gms.location.network.TAG, e)
}
}
override fun enable() {
context.registerReceiver(this, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
}
override fun disable() {
context.unregisterReceiver(this)
}
override fun startScan(workSource: WorkSource?) {
context.getSystemService<WifiManager>()?.startScan()
}
}

View file

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.wifi.ScanResult
import android.net.wifi.WifiScanner
import android.os.Build.VERSION.SDK_INT
import android.os.WorkSource
import android.util.Log
import androidx.core.content.ContextCompat
import org.microg.gms.location.network.TAG
@SuppressLint("WrongConstant")
class WifiScannerSource(private val context: Context, private val callback: WifiDetailsCallback) : WifiDetailsSource {
override fun startScan(workSource: WorkSource?) {
val scanner = context.getSystemService("wifiscanner") as WifiScanner
scanner.startScan(WifiScanner.ScanSettings().apply {
band = WifiScanner.WIFI_BAND_BOTH
}, object : WifiScanner.ScanListener {
override fun onSuccess() {
Log.d(TAG, "Not yet implemented: onSuccess")
failed = false
}
override fun onFailure(reason: Int, description: String?) {
Log.d(TAG, "Not yet implemented: onFailure $reason $description")
failed = true
callback.onWifiSourceFailed()
}
@Deprecated("Not supported on all devices")
override fun onPeriodChanged(periodInMs: Int) {
Log.d(TAG, "Not yet implemented: onPeriodChanged $periodInMs")
}
override fun onResults(results: Array<out WifiScanner.ScanData>) {
callback.onWifiDetailsAvailable(results.flatMap { it.results.toList() }.map(ScanResult::toWifiDetails))
}
override fun onFullResult(fullScanResult: ScanResult) {
Log.d(TAG, "Not yet implemented: onFullResult $fullScanResult")
}
}, workSource)
}
companion object {
private var failed = false
fun isSupported(context: Context): Boolean {
return SDK_INT >= 26 && !failed && (context.getSystemService("wifiscanner") as? WifiScanner) != null && ContextCompat.checkSelfPermission(context, Manifest.permission.LOCATION_HARDWARE) == PERMISSION_GRANTED
}
}
}

View file

@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.network.wifi
import android.net.wifi.ScanResult
import android.net.wifi.WifiInfo
import android.os.Build.VERSION.SDK_INT
import android.os.SystemClock
import androidx.annotation.RequiresApi
internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails(
macAddress = BSSID,
ssid = SSID,
timestamp = if (SDK_INT >= 19) System.currentTimeMillis() - (SystemClock.elapsedRealtime() - (timestamp / 1000)) else null,
frequency = frequency,
channel = frequencyToChannel(frequency),
signalStrength = level,
open = setOf("WEP", "WPA", "PSK", "EAP", "IEEE8021X", "PEAP", "TLS", "TTLS").none { capabilities.contains(it) }
)
@RequiresApi(31)
internal fun WifiInfo.toWifiDetails(): WifiDetails = WifiDetails(
macAddress = bssid,
ssid = ssid,
timestamp = System.currentTimeMillis(),
frequency = frequency,
signalStrength = rssi,
open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN
)
private const val BAND_24_GHZ_FIRST_CH_NUM = 1
private const val BAND_24_GHZ_LAST_CH_NUM = 14
private const val BAND_5_GHZ_FIRST_CH_NUM = 32
private const val BAND_5_GHZ_LAST_CH_NUM = 177
private const val BAND_6_GHZ_FIRST_CH_NUM = 1
private const val BAND_6_GHZ_LAST_CH_NUM = 233
private const val BAND_60_GHZ_FIRST_CH_NUM = 1
private const val BAND_60_GHZ_LAST_CH_NUM = 6
private const val BAND_24_GHZ_START_FREQ_MHZ = 2412
private const val BAND_24_GHZ_END_FREQ_MHZ = 2484
private const val BAND_5_GHZ_START_FREQ_MHZ = 5160
private const val BAND_5_GHZ_END_FREQ_MHZ = 5885
private const val BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ = 5935
private const val BAND_6_GHZ_START_FREQ_MHZ = 5955
private const val BAND_6_GHZ_END_FREQ_MHZ = 7115
private const val BAND_60_GHZ_START_FREQ_MHZ = 58320
private const val BAND_60_GHZ_END_FREQ_MHZ = 70200
internal fun frequencyToChannel(freq: Int): Int? {
return when (freq) {
// Special cases
BAND_24_GHZ_END_FREQ_MHZ -> BAND_24_GHZ_LAST_CH_NUM
BAND_6_GHZ_OP_CLASS_136_CH_2_FREQ_MHZ -> 2
in BAND_24_GHZ_START_FREQ_MHZ..BAND_24_GHZ_END_FREQ_MHZ ->
(freq - BAND_24_GHZ_START_FREQ_MHZ) / 5 + BAND_24_GHZ_FIRST_CH_NUM
in BAND_5_GHZ_START_FREQ_MHZ..BAND_5_GHZ_END_FREQ_MHZ ->
(freq - BAND_5_GHZ_START_FREQ_MHZ) / 5 + BAND_5_GHZ_FIRST_CH_NUM
in BAND_6_GHZ_START_FREQ_MHZ..BAND_6_GHZ_END_FREQ_MHZ ->
(freq - BAND_6_GHZ_START_FREQ_MHZ) / 5 + BAND_6_GHZ_FIRST_CH_NUM
in BAND_60_GHZ_START_FREQ_MHZ..BAND_60_GHZ_END_FREQ_MHZ ->
(freq - BAND_60_GHZ_START_FREQ_MHZ) / 2160 + BAND_60_GHZ_FIRST_CH_NUM
else -> null
}
}
val WifiDetails.isNomap: Boolean
get() = ssid?.endsWith("_nomap") == true
val WifiDetails.isHidden: Boolean
get() = ssid == ""
val WifiDetails.isRequestable: Boolean
get() = !isNomap && !isHidden && !isMoving
val WifiDetails.macBytes: ByteArray
get() {
val mac = macClean
return byteArrayOf(
mac.substring(0, 2).toInt(16).toByte(),
mac.substring(2, 4).toInt(16).toByte(),
mac.substring(4, 6).toInt(16).toByte(),
mac.substring(6, 8).toInt(16).toByte(),
mac.substring(8, 10).toInt(16).toByte(),
mac.substring(10, 12).toInt(16).toByte()
)
}
val WifiDetails.macClean: String
get() = macAddress.lowercase().replace(":", "")

View file

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.content.Context
import android.location.LocationProvider
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import com.android.location.provider.LocationProviderBase
import com.android.location.provider.ProviderPropertiesUnbundled
import java.io.FileDescriptor
import java.io.PrintWriter
abstract class AbstractLocationProviderPreTiramisu : LocationProviderBase, GenericLocationProvider {
@Deprecated("Use only with SDK < 31")
constructor(properties: ProviderPropertiesUnbundled) : super(TAG, properties)
@RequiresApi(31)
constructor(context: Context, properties: ProviderPropertiesUnbundled) : super(context, TAG, properties)
private var statusUpdateTime = SystemClock.elapsedRealtime()
override fun onDump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
dump(pw)
}
override fun dump(writer: PrintWriter) {
// Nothing by default
}
override fun onFlush(callback: OnFlushCompleteCallback?) {
Log.d(TAG, "onFlush")
callback!!.onFlushComplete()
}
override fun onSendExtraCommand(command: String?, extras: Bundle?): Boolean {
Log.d(TAG, "onSendExtraCommand $command $extras")
return false
}
@Deprecated("Overriding this is required pre-Q, but not used since Q")
override fun onEnable() {
Log.d(TAG, "onEnable")
statusUpdateTime = SystemClock.elapsedRealtime()
}
@Deprecated("Overriding this is required pre-Q, but not used since Q")
override fun onDisable() {
Log.d(TAG, "onDisable")
statusUpdateTime = SystemClock.elapsedRealtime()
}
@Deprecated("Overriding this is required pre-Q, but not used since Q")
override fun onGetStatus(extras: Bundle?): Int {
Log.d(TAG, "onGetStatus $extras")
return LocationProvider.AVAILABLE
}
@Deprecated("Overriding this is required pre-Q, but not used since Q")
override fun onGetStatusUpdateTime(): Long {
Log.d(TAG, "onGetStatusUpdateTime")
return statusUpdateTime
}
}

View file

@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.location.Criteria
import android.location.Location
import android.os.Binder
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.core.location.LocationRequestCompat
import com.android.location.provider.ProviderPropertiesUnbundled
import com.android.location.provider.ProviderRequestUnbundled
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlin.math.max
class FusedLocationProviderService : IntentLocationProviderService() {
override fun extractLocation(intent: Intent): Location? = LocationResult.extractResult(intent)?.lastLocation
@SuppressLint("MissingPermission")
override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) {
val intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, minIntervalMillis)
val request = LocationRequest.Builder(intervalMillis)
if (SDK_INT >= 31 && currentRequest != null) {
request.setPriority(when {
currentRequest.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE
currentRequest.isLowPower -> Priority.PRIORITY_LOW_POWER
currentRequest.quality == LocationRequestCompat.QUALITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER
currentRequest.quality == LocationRequestCompat.QUALITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY
else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
})
request.setMaxUpdateDelayMillis(currentRequest.maxUpdateDelayMillis)
request.setWorkSource(currentRequest.workSource)
} else {
request.setPriority(when {
currentRequest?.interval == LocationRequestCompat.PASSIVE_INTERVAL -> Priority.PRIORITY_PASSIVE
else -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
})
}
if (SDK_INT >= 29 && currentRequest != null) {
request.setBypass(currentRequest.isLocationSettingsIgnored)
}
val identity = Binder.clearCallingIdentity()
try {
LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(request.build(), pendingIntent)
} catch (e: SecurityException) {
Log.d(TAG, "Failed requesting location updated", e)
}
Binder.restoreCallingIdentity(identity)
}
override fun stopIntentUpdated(pendingIntent: PendingIntent) {
LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(pendingIntent)
}
override val minIntervalMillis: Long
get() = MIN_INTERVAL_MILLIS
override val minReportMillis: Long
get() = MIN_REPORT_MILLIS
override val properties: ProviderPropertiesUnbundled
get() = PROPERTIES
override val providerName: String
get() = "fused"
companion object {
private const val MIN_INTERVAL_MILLIS = 1000L
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)
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.location.Location
import android.os.IBinder
import java.io.PrintWriter
interface GenericLocationProvider {
fun getBinder(): IBinder
fun enable()
fun disable()
fun dump(writer: PrintWriter)
fun reportLocationToSystem(location: Location)
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.app.Service
import android.content.Intent
import android.os.IBinder
import java.io.FileDescriptor
import java.io.PrintWriter
class GeocodeProviderService : Service() {
private var bound: Boolean = false
private var provider: OpenStreetMapNominatimGeocodeProvider? = null
override fun onBind(intent: Intent?): IBinder? {
if (provider == null) {
provider = OpenStreetMapNominatimGeocodeProvider(this)
}
bound = true
return provider?.binder
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
provider?.dump(writer)
}
}

View file

@ -0,0 +1,124 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Intent
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.elapsedMillis
import org.microg.gms.location.formatRealtime
import java.io.PrintWriter
import kotlin.math.max
class IntentLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu {
@Deprecated("Use only with SDK < 31")
constructor(service: IntentLocationProviderService, properties: ProviderPropertiesUnbundled, legacy: Unit) : super(properties) {
this.service = service
}
@RequiresApi(31)
constructor(service: IntentLocationProviderService, properties: ProviderPropertiesUnbundled) : super(service, properties) {
this.service = service
}
private val service: IntentLocationProviderService
private var enabled = false
private var currentRequest: ProviderRequestUnbundled? = null
private var pendingIntent: PendingIntent? = null
private var lastReportedLocation: Location? = null
private var lastReportTime: Long = 0
private val handler = Handler(Looper.getMainLooper())
private val reportAgainRunnable = Runnable { reportAgain() }
private fun updateRequest() {
if (enabled && pendingIntent != null) {
service.requestIntentUpdated(currentRequest, pendingIntent!!)
reportAgain()
}
}
override fun dump(writer: PrintWriter) {
writer.println("Enabled: $enabled")
writer.println("Current request: $currentRequest")
if (SDK_INT >= 31) writer.println("Current work source: ${currentRequest?.workSource}")
writer.println("Last reported: $lastReportedLocation")
writer.println("Last report time: ${lastReportTime.formatRealtime()}")
}
override fun onSetRequest(request: ProviderRequestUnbundled, source: WorkSource) {
synchronized(this) {
currentRequest = request
updateRequest()
}
}
override fun enable() {
synchronized(this) {
if (enabled) throw IllegalStateException()
val intent = Intent(service, service.javaClass)
intent.action = ACTION_REPORT_LOCATION
pendingIntent = PendingIntentCompat.getService(service, 0, intent, FLAG_UPDATE_CURRENT, true)
currentRequest = null
enabled = true
when {
SDK_INT >= 30 -> isAllowed = true
SDK_INT >= 29 -> isEnabled = true
}
try {
if (lastReportedLocation == null) {
lastReportedLocation = service.getSystemService<LocationManager>()?.getLastKnownLocation(service.providerName)
}
} catch (_: SecurityException) {
} catch (_: Exception) {
}
}
}
override fun disable() {
synchronized(this) {
if (!enabled || pendingIntent == null) throw IllegalStateException()
service.stopIntentUpdated(pendingIntent!!)
pendingIntent?.cancel()
pendingIntent = null
currentRequest = null
enabled = false
handler.removeCallbacks(reportAgainRunnable)
}
}
private fun reportAgain() {
// Report location again if it's recent enough
lastReportedLocation?.let {
if (it.elapsedMillis + max(currentRequest?.interval ?: 0, service.minIntervalMillis) > SystemClock.elapsedRealtime()) {
reportLocationToSystem(it)
}
}
}
override fun reportLocationToSystem(location: Location) {
handler.removeCallbacks(reportAgainRunnable)
location.provider = service.providerName
lastReportedLocation = location
lastReportTime = SystemClock.elapsedRealtime()
super.reportLocation(location)
val repeatInterval = max(service.minReportMillis, currentRequest?.interval ?: Long.MAX_VALUE)
if (repeatInterval < service.minIntervalMillis) {
handler.postDelayed(reportAgainRunnable, repeatInterval)
}
}
}

View file

@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.location.Location
import android.os.Binder
import android.os.Build.VERSION.SDK_INT
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import android.os.Process
import android.util.Log
import com.android.location.provider.ProviderPropertiesUnbundled
import com.android.location.provider.ProviderRequestUnbundled
import java.io.FileDescriptor
import java.io.PrintWriter
abstract class IntentLocationProviderService : Service() {
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
private var bound: Boolean = false
private var provider: GenericLocationProvider? = null
override fun onCreate() {
super.onCreate()
handlerThread = HandlerThread(this.javaClass.simpleName)
handlerThread.start()
handler = Handler(handlerThread.looper)
}
abstract fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent)
abstract fun stopIntentUpdated(pendingIntent: PendingIntent)
abstract fun extractLocation(intent: Intent): Location?
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Binder.getCallingUid() == Process.myUid() && intent?.action == ACTION_REPORT_LOCATION) {
handler.post {
val location = extractLocation(intent)
if (location != null) {
provider?.reportLocationToSystem(location)
}
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
bound = true
if (provider == null) {
provider = when {
// TODO: Migrate to Tiramisu provider. Not yet required thanks to backwards compat
// SDK_INT >= 33 ->
SDK_INT >= 31 ->
IntentLocationProviderPreTiramisu(this, properties)
else ->
@Suppress("DEPRECATION")
(IntentLocationProviderPreTiramisu(this, properties, Unit))
}
provider?.enable()
}
return provider?.getBinder()
}
override fun dump(fd: FileDescriptor, writer: PrintWriter, args: Array<out String>) {
writer.println("Bound: $bound")
provider?.dump(writer)
}
override fun onDestroy() {
if (SDK_INT >= 18) handlerThread.looper.quitSafely()
else handlerThread.looper.quit()
provider?.disable()
provider = null
bound = false
super.onDestroy()
}
abstract val minIntervalMillis: Long
abstract val minReportMillis: Long
abstract val properties: ProviderPropertiesUnbundled
abstract val providerName: String
}

View file

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.app.PendingIntent
import android.content.Intent
import android.location.Criteria
import android.location.Location
import android.location.LocationManager
import android.os.Build.VERSION.SDK_INT
import com.android.location.provider.ProviderPropertiesUnbundled
import com.android.location.provider.ProviderRequestUnbundled
import org.microg.gms.location.*
import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION
import kotlin.math.max
class NetworkLocationProviderService : IntentLocationProviderService() {
override fun extractLocation(intent: Intent): Location? = intent.getParcelableExtra<Location?>(EXTRA_LOCATION)?.apply {
extras?.remove(LOCATION_EXTRA_PRECISION)
}
override fun requestIntentUpdated(currentRequest: ProviderRequestUnbundled?, pendingIntent: PendingIntent) {
val forceNow: Boolean
val intervalMillis: Long
if (currentRequest?.reportLocation == true) {
forceNow = true
intervalMillis = max(currentRequest.interval ?: Long.MAX_VALUE, minIntervalMillis)
} else {
forceNow = false
intervalMillis = Long.MAX_VALUE
}
val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE)
intent.`package` = packageName
intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent)
intent.putExtra(EXTRA_ENABLE, true)
intent.putExtra(EXTRA_INTERVAL_MILLIS, intervalMillis)
intent.putExtra(EXTRA_FORCE_NOW, forceNow)
if (SDK_INT >= 31) {
intent.putExtra(EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false)
intent.putExtra(EXTRA_WORK_SOURCE, currentRequest?.workSource)
}
if (SDK_INT >= 29) {
intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false)
}
startService(intent)
}
override fun stopIntentUpdated(pendingIntent: PendingIntent) {
val intent = Intent(ACTION_NETWORK_LOCATION_SERVICE)
intent.`package` = packageName
intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent)
intent.putExtra(EXTRA_ENABLE, false)
startService(intent)
}
override val minIntervalMillis: Long
get() = MIN_INTERVAL_MILLIS
override val minReportMillis: Long
get() = MIN_REPORT_MILLIS
override val properties: ProviderPropertiesUnbundled
get() = PROPERTIES
override val providerName: String
get() = LocationManager.NETWORK_PROVIDER
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)
}
}

View file

@ -0,0 +1,188 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.content.Context
import android.location.Address
import android.location.GeocoderParams
import android.net.Uri
import android.util.Log
import android.util.LruCache
import com.android.location.provider.GeocodeProvider
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import com.google.android.gms.location.internal.ClientIdentity
import org.json.JSONObject
import org.microg.address.Formatter
import org.microg.gms.location.LocationSettings
import org.microg.gms.utils.singleInstanceOf
import java.io.PrintWriter
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : GeocodeProvider() {
private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
private val formatter = runCatching { Formatter() }.getOrNull()
private val addressCache = LruCache<CacheKey, Address>(CACHE_SIZE)
private val settings by lazy { LocationSettings(context) }
override fun onGetFromLocation(latitude: Double, longitude: Double, maxResults: Int, params: GeocoderParams, addresses: MutableList<Address>): String? {
val clientIdentity = params.clientIdentity ?: return "null client package"
val locale = params.locale ?: return "null locale"
if (!settings.geocoderNominatim) return "disabled"
val cacheKey = CacheKey(clientIdentity, locale, latitude, longitude)
addressCache[cacheKey]?.let {address ->
addresses.add(address)
return null
}
val uri = Uri.Builder()
.scheme("https").authority(NOMINATIM_SERVER).path("/reverse")
.appendQueryParameter("format", "json")
.appendQueryParameter("accept-language", locale.language)
.appendQueryParameter("addressdetails", "1")
.appendQueryParameter("lat", latitude.toString())
.appendQueryParameter("lon", longitude.toString())
val result = AtomicReference<String?>("timeout reached")
val returnedAddress = AtomicReference<Address?>(null)
val latch = CountDownLatch(1)
queue.add(object : JsonObjectRequest(uri.build().toString(), {
parseResponse(locale, it)?.let(returnedAddress::set)
result.set(null)
latch.countDown()
}, {
result.set(it.message)
latch.countDown()
}) {
override fun getHeaders(): Map<String, String> = mapOf("User-Agent" to "microG/${context.versionName}")
})
latch.await(5, TimeUnit.SECONDS)
val address = returnedAddress.get()
if (address != null) {
Log.d(TAG, "Returned $address for $latitude,$longitude")
addresses.add(address)
addressCache.put(cacheKey, address)
}
return result.get()
}
override fun onGetFromLocationName(
locationName: String,
lowerLeftLatitude: Double,
lowerLeftLongitude: Double,
upperRightLatitude: Double,
upperRightLongitude: Double,
maxResults: Int,
params: GeocoderParams,
addresses: MutableList<Address>
): String? {
val clientIdentity = params.clientIdentity ?: return "null client package"
val locale = params.locale ?: return "null locale"
if (!settings.geocoderNominatim) return "disabled"
val uri = Uri.Builder()
.scheme("https").authority(NOMINATIM_SERVER).path("/search")
.appendQueryParameter("format", "json")
.appendQueryParameter("accept-language", locale.language)
.appendQueryParameter("addressdetails", "1")
.appendQueryParameter("bounded", "1")
.appendQueryParameter("q", locationName)
.appendQueryParameter("limit", maxResults.toString())
if (lowerLeftLatitude != upperRightLatitude && lowerLeftLongitude != upperRightLongitude) {
uri.appendQueryParameter("viewbox", "$lowerLeftLongitude,$upperRightLatitude,$upperRightLongitude,$lowerLeftLatitude")
}
val result = AtomicReference<String?>("timeout reached")
val latch = CountDownLatch(1)
queue.add(object : JsonArrayRequest(uri.build().toString(), {
for (i in 0 until it.length()) {
parseResponse(locale, it.getJSONObject(i))?.let(addresses::add)
}
result.set(null)
latch.countDown()
}, {
result.set(it.message)
latch.countDown()
}) {
override fun getHeaders(): Map<String, String> = mapOf("User-Agent" to "microG/${context.versionName}")
})
latch.await(5, TimeUnit.SECONDS)
return result.get()
}
private fun parseResponse(locale: Locale, result: JSONObject): Address? {
if (!result.has(WIRE_LATITUDE) || !result.has(WIRE_LONGITUDE) ||
!result.has(WIRE_ADDRESS)
) {
return null
}
Log.d(TAG, "Result: $result")
val address = Address(locale)
address.latitude = result.getDouble(WIRE_LATITUDE)
address.longitude = result.getDouble(WIRE_LONGITUDE)
val a = result.getJSONObject(WIRE_ADDRESS)
address.thoroughfare = a.optString(WIRE_THOROUGHFARE)
address.subLocality = a.optString(WIRE_SUBLOCALITY)
address.postalCode = a.optString(WIRE_POSTALCODE)
address.subAdminArea = a.optString(WIRE_SUBADMINAREA)
address.adminArea = a.optString(WIRE_ADMINAREA)
address.countryName = a.optString(WIRE_COUNTRYNAME)
address.countryCode = a.optString(WIRE_COUNTRYCODE)
if (a.has(WIRE_LOCALITY_CITY)) {
address.locality = a.getString(WIRE_LOCALITY_CITY)
} else if (a.has(WIRE_LOCALITY_TOWN)) {
address.locality = a.getString(WIRE_LOCALITY_TOWN)
} else if (a.has(WIRE_LOCALITY_VILLAGE)) {
address.locality = a.getString(WIRE_LOCALITY_VILLAGE)
}
if (formatter != null) {
val components = mutableMapOf<String, String>()
for (s in a.keys()) {
if (s !in WIRE_IGNORED) {
components[s] = a[s].toString()
}
}
val split = formatter.formatAddress(components).split("\n")
for (i in split.indices) {
address.setAddressLine(i, split[i])
}
address.featureName = formatter.guessName(components)
}
return address
}
fun dump(writer: PrintWriter?) {
writer?.println("Enabled: ${settings.geocoderNominatim}")
writer?.println("Address cache: size=${addressCache.size()} hits=${addressCache.hitCount()} miss=${addressCache.missCount()} puts=${addressCache.putCount()} evicts=${addressCache.evictionCount()}")
}
companion object {
private const val CACHE_SIZE = 200
private const val NOMINATIM_SERVER = "nominatim.openstreetmap.org"
private const val WIRE_LATITUDE = "lat"
private const val WIRE_LONGITUDE = "lon"
private const val WIRE_ADDRESS = "address"
private const val WIRE_THOROUGHFARE = "road"
private const val WIRE_SUBLOCALITY = "suburb"
private const val WIRE_POSTALCODE = "postcode"
private const val WIRE_LOCALITY_CITY = "city"
private const val WIRE_LOCALITY_TOWN = "town"
private const val WIRE_LOCALITY_VILLAGE = "village"
private const val WIRE_SUBADMINAREA = "county"
private const val WIRE_ADMINAREA = "state"
private const val WIRE_COUNTRYNAME = "country"
private const val WIRE_COUNTRYCODE = "country_code"
private val WIRE_IGNORED = setOf<String>("ISO3166-2-lvl4")
private data class CacheKey(val uid: Int, val packageName: String?, val locale: Locale, val latitude: Int, val longitude: Int) {
constructor(clientIdentity: ClientIdentity, locale: Locale, latitude: Double, longitude: Double) : this(clientIdentity.uid, clientIdentity.packageName.takeIf { clientIdentity.uid != 0 }, locale, (latitude * 100000.0).toInt(), (longitude * 100000.0).toInt())
}
}
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.location.provider
import android.content.Context
import android.location.GeocoderParams
import android.os.Build.VERSION.SDK_INT
import com.google.android.gms.location.internal.ClientIdentity
const val TAG = "LocationProvider"
const val ACTION_REPORT_LOCATION = "org.microg.gms.location.provider.ACTION_REPORT_LOCATION"
val GeocoderParams.clientIdentity: ClientIdentity?
get() = clientPackage?.let {
ClientIdentity(it).apply {
if (SDK_INT >= 33) {
uid = clientUid
attributionTag = clientAttributionTag
}
}
}
val Context.versionName: String?
get() = packageManager.getPackageInfo(packageName, 0).versionName

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2024 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<paths>
<cache-path name="location" path="location" />
</paths>