Skip to content

Commit

Permalink
Location: Major rework of location provider
Browse files Browse the repository at this point in the history
- Add Import/Export
- Allow online source selection
- Improve local data handling
  • Loading branch information
mar-v-in committed Sep 25, 2024
1 parent 108bd88 commit 44a9a4d
Show file tree
Hide file tree
Showing 35 changed files with 1,876 additions and 655 deletions.
7 changes: 0 additions & 7 deletions play-services-base/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ android {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
if (localProperties.get("ichnaea.endpoint", "") != "") {
buildConfigField "String", "ICHNAEA_ENDPOINT_DEFAULT", "\"${localProperties.get("ichnaea.endpoint")}\""
} else if (localProperties.get("ichnaea.key", "") != "") {
buildConfigField "String", "ICHNAEA_ENDPOINT_DEFAULT", "\"https://location.services.mozilla.com/?key=${localProperties.get("ichnaea.key")}\""
} else {
buildConfigField "String", "ICHNAEA_ENDPOINT_DEFAULT", "\"\""
}
}

buildFeatures {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,19 +166,25 @@ object SettingsContract {
const val WIFI_ICHNAEA = "location_wifi_mls"
const val WIFI_MOVING = "location_wifi_moving"
const val WIFI_LEARNING = "location_wifi_learning"
const val WIFI_CACHING = "location_wifi_caching"
const val CELL_ICHNAEA = "location_cell_mls"
const val CELL_LEARNING = "location_cell_learning"
const val CELL_CACHING = "location_cell_caching"
const val GEOCODER_NOMINATIM = "location_geocoder_nominatim"
const val ICHNAEA_ENDPOINT = "location_ichnaea_endpoint"
const val ONLINE_SOURCE = "location_online_source"

val PROJECTION = arrayOf(
WIFI_ICHNAEA,
WIFI_MOVING,
WIFI_LEARNING,
WIFI_CACHING,
CELL_ICHNAEA,
CELL_LEARNING,
CELL_CACHING,
GEOCODER_NOMINATIM,
ICHNAEA_ENDPOINT,
ONLINE_SOURCE,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,14 @@ class SettingsProvider : ContentProvider() {
when (key) {
Location.WIFI_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea"))
Location.WIFI_MOVING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("de.sorunome.unifiednlp.trains"))
Location.WIFI_LEARNING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("helium314.localbackend", "org.fitchfamily.android.dejavu"))
Location.WIFI_LEARNING -> getSettingsBoolean(key, false)
Location.WIFI_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.WIFI_LEARNING, false) == 1)
Location.CELL_ICHNAEA -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("org.microg.nlp.backend.ichnaea"))
Location.CELL_LEARNING -> getSettingsBoolean(key, hasUnifiedNlpLocationBackend("helium314.localbackend", "org.fitchfamily.android.dejavu"))
Location.CELL_LEARNING -> getSettingsBoolean(key, true)
Location.CELL_CACHING -> getSettingsBoolean(key, getSettingsBoolean(Location.CELL_LEARNING, true) == 1)
Location.GEOCODER_NOMINATIM -> getSettingsBoolean(key, hasUnifiedNlpGeocoderBackend("org.microg.nlp.backend.nominatim") )
Location.ICHNAEA_ENDPOINT -> getSettingsString(key, BuildConfig.ICHNAEA_ENDPOINT_DEFAULT)
Location.ICHNAEA_ENDPOINT -> getSettingsString(key, null)
Location.ONLINE_SOURCE -> getSettingsString(key, null)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
Expand All @@ -337,6 +340,7 @@ class SettingsProvider : ContentProvider() {
Location.CELL_LEARNING -> editor.putBoolean(key, value as Boolean)
Location.GEOCODER_NOMINATIM -> editor.putBoolean(key, value as Boolean)
Location.ICHNAEA_ENDPOINT -> (value as String).let { if (it.isBlank()) editor.remove(key) else editor.putString(key, it) }
Location.ONLINE_SOURCE -> (value as? String?).let { if (it.isNullOrBlank()) editor.remove(key) else editor.putString(key, it) }
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
Expand Down
4 changes: 1 addition & 3 deletions play-services-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,7 @@
android:taskAffinity="org.microg.gms.settings">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data
android:host="exposure-notifications-rpis"
android:scheme="x-gms-settings" />
<data android:scheme="x-gms-settings" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ProvisionService : LifecycleService() {
intent?.extras?.getBooleanOrNull("wifi_ichnaea")?.let { wifiIchnaea = it }
intent?.extras?.getBooleanOrNull("cell_mls")?.let { cellIchnaea = it }
intent?.extras?.getBooleanOrNull("cell_ichnaea")?.let { cellIchnaea = it }
intent?.extras?.getString("ichnaea_endpoint")?.let { ichneaeEndpoint = it }
intent?.extras?.getString("ichnaea_endpoint")?.let { customEndpoint = it }
intent?.extras?.getBooleanOrNull("ichnaea_contribute")?.let { ichnaeaContribute = it }
intent?.extras?.getBooleanOrNull("wifi_learning")?.let { wifiLearning = it }
intent?.extras?.getBooleanOrNull("cell_learning")?.let { cellLearning = it }
Expand Down
13 changes: 13 additions & 0 deletions play-services-location/core/base/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,23 @@ android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"

buildFeatures {
buildConfig = true
}

defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
def onlineSourcesString = ""
if (localProperties.get("location.online-sources", "") != "") {
onlineSourcesString = localProperties.get("location.online-sources", "[]")
} else if (localProperties.get("ichnaea.endpoint", "") != "") {
onlineSourcesString = "[{\"id\": \"default\", \"url\": \"${localProperties.get("ichnaea.endpoint", "")}\"},{\"id\": \"custom\", \"import\": true}]"
} else {
onlineSourcesString = "[{\"id\": \"beacondb\", \"name\": \"BeaconDB\", \"url\": \"https://api.beacondb.net/\", \"host\": \"beacondb.net\", \"terms\": \"https://beacondb.net/privacy/\", \"import\": true, \"allowContribute\": true},{\"id\": \"custom\", \"import\": true}]"
}
buildConfigField "java.util.List<org.microg.gms.location.network.OnlineSource>", "ONLINE_SOURCES", "org.microg.gms.location.network.OnlineSourceKt.parseOnlineSources(\"${onlineSourcesString.replaceAll("\"", "\\\\\"")}\")"
}

sourceSets {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import android.content.Context
import android.database.Cursor
import org.microg.gms.settings.SettingsContract

private const val PATH_GEOLOCATE = "/v1/geolocate"
private const val PATH_GEOLOCATE_QUERY = "/v1/geolocate?"
private const val PATH_GEOSUBMIT = "/v2/geosubmit"
private const val PATH_GEOSUBMIT_QUERY = "/v2/geosubmit?"
private const val PATH_QUERY_ONLY = "/?"

class LocationSettings(private val context: Context) {
private fun <T> getSettings(vararg projection: String, f: (Cursor) -> T): T =
SettingsContract.getSettings(context, SettingsContract.Location.getContentUri(context), projection, f)
Expand All @@ -28,6 +34,10 @@ class LocationSettings(private val context: Context) {
get() = getSettings(SettingsContract.Location.WIFI_LEARNING) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.WIFI_LEARNING, value) }

var wifiCaching: Boolean
get() = getSettings(SettingsContract.Location.WIFI_CACHING) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.WIFI_CACHING, value) }

var cellIchnaea: Boolean
get() = getSettings(SettingsContract.Location.CELL_ICHNAEA) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.CELL_ICHNAEA, value) }
Expand All @@ -36,13 +46,54 @@ class LocationSettings(private val context: Context) {
get() = getSettings(SettingsContract.Location.CELL_LEARNING) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.CELL_LEARNING, value) }

var cellCaching: Boolean
get() = getSettings(SettingsContract.Location.CELL_CACHING) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.CELL_CACHING, value) }

var geocoderNominatim: Boolean
get() = getSettings(SettingsContract.Location.GEOCODER_NOMINATIM) { c -> c.getInt(0) != 0 }
set(value) = setSettings { put(SettingsContract.Location.GEOCODER_NOMINATIM, value) }

var ichneaeEndpoint: String
get() = getSettings(SettingsContract.Location.ICHNAEA_ENDPOINT) { c -> c.getString(0) }
set(value) = setSettings { put(SettingsContract.Location.ICHNAEA_ENDPOINT, value) }
var customEndpoint: String?
get() {
try {
var endpoint = getSettings(SettingsContract.Location.ICHNAEA_ENDPOINT) { c -> c.getString(0) }
// This is only temporary as users might have already broken configuration.
// Usually this would be corrected before storing it in settings, see below.
if (endpoint.endsWith(PATH_GEOLOCATE)) {
endpoint = endpoint.substring(0, endpoint.length - PATH_GEOLOCATE.length + 1)
} else if (endpoint.contains(PATH_GEOLOCATE_QUERY)) {
endpoint = endpoint.replace(PATH_GEOLOCATE_QUERY, PATH_QUERY_ONLY)
} else if (endpoint.endsWith(PATH_GEOSUBMIT)) {
endpoint = endpoint.substring(0, endpoint.length - PATH_GEOSUBMIT.length + 1)
} else if (endpoint.contains(PATH_GEOSUBMIT_QUERY)) {
endpoint = endpoint.replace(PATH_GEOSUBMIT_QUERY, PATH_QUERY_ONLY)
}
return endpoint
} catch (e: Exception) {
return null
}
}
set(value) {
val endpoint = if (value == null) {
null
} else if (value.endsWith(PATH_GEOLOCATE)) {
value.substring(0, value.length - PATH_GEOLOCATE.length + 1)
} else if (value.contains(PATH_GEOLOCATE_QUERY)) {
value.replace(PATH_GEOLOCATE_QUERY, PATH_QUERY_ONLY)
} else if (value.endsWith(PATH_GEOSUBMIT)) {
value.substring(0, value.length - PATH_GEOSUBMIT.length + 1)
} else if (value.contains(PATH_GEOSUBMIT_QUERY)) {
value.replace(PATH_GEOSUBMIT_QUERY, PATH_QUERY_ONLY)
} else {
value
}
setSettings { put(SettingsContract.Location.ICHNAEA_ENDPOINT, endpoint) }
}

var onlineSourceId: String?
get() = getSettings(SettingsContract.Location.ONLINE_SOURCE) { c -> c.getString(0) }
set(value) = setSettings { put(SettingsContract.Location.ONLINE_SOURCE, value) }

var ichnaeaContribute: Boolean
get() = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ const val EXTRA_LOW_POWER = "low_power"
const val EXTRA_WORK_SOURCE = "work_source"
const val EXTRA_BYPASS = "bypass"

const val ACTION_CONFIGURATION_REQUIRED = "org.microg.gms.location.network.ACTION_CONFIGURATION_REQUIRED"
const val EXTRA_CONFIGURATION = "config"
const val CONFIGURATION_FIELD_ONLINE_SOURCE = "online_source"

const val ACTION_NETWORK_IMPORT_EXPORT = "org.microg.gms.location.network.ACTION_NETWORK_IMPORT_EXPORT"
const val EXTRA_DIRECTION = "direction"
const val DIRECTION_IMPORT = "import"
const val DIRECTION_EXPORT = "export"
const val EXTRA_NAME = "name"
const val NAME_WIFI = "wifi"
const val NAME_CELL = "cell"
const val EXTRA_URI = "uri"
const val EXTRA_MESSENGER = "messenger"
const val EXTRA_REPLY_WHAT = "what"

val Location.elapsedMillis: Long
get() = LocationCompat.getElapsedRealtimeMillis(this)

Expand Down Expand Up @@ -51,11 +66,6 @@ fun Long.formatDuration(): CharSequence {
return ret
}

fun Context.hasIchnaeaLocationServiceSupport(): Boolean {
if (!hasNetworkLocationServiceBuiltIn()) return false
return LocationSettings(this).ichneaeEndpoint.isNotBlank()
}

private var hasNetworkLocationServiceBuiltInFlag: Boolean? = null
fun Context.hasNetworkLocationServiceBuiltIn(): Boolean {
var flag = hasNetworkLocationServiceBuiltInFlag
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.location.network

import android.net.Uri
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.location.LocationSettings
import org.microg.gms.location.base.BuildConfig

fun parseOnlineSources(string: String): List<OnlineSource> = JSONArray(string).let { array ->
(0 until array.length()).map { parseOnlineSource(array.getJSONObject(it)) }.also { Log.d("Location", "parseOnlineSources: ${it.joinToString()}") }
}

fun parseOnlineSource(json: JSONObject): OnlineSource {
val id = json.getString("id")
val url = json.optString("url").takeIf { it.isNotBlank() }
val host = json.optString("host").takeIf { it.isNotBlank() } ?: runCatching { Uri.parse(url).host }.getOrNull()
val name = json.optString("name").takeIf { it.isNotBlank() } ?: host
return OnlineSource(
id = id,
name = name,
url = url,
host = host,
terms = json.optString("terms").takeIf { it.isNotBlank() }?.let { runCatching { Uri.parse(it) }.getOrNull() },
suggested = json.optBoolean("suggested", false),
import = json.optBoolean("import", false),
allowContribute = json.optBoolean("allowContribute", false),
)
}

data class OnlineSource(
val id: String,
val name: String? = null,
val url: String? = null,
val host: String? = null,
val terms: Uri? = null,
/**
* Show suggested flag
*/
val suggested: Boolean = false,
/**
* If set, automatically import from custom URL if host matches (is the same domain suffix)
*/
val import: Boolean = false,
val allowContribute: Boolean = false,
) {
companion object {
/**
* Entry to allow configuring a custom URL
*/
val ID_CUSTOM = "custom"

/**
* Legacy compatibility
*/
val ID_DEFAULT = "default"

val ALL: List<OnlineSource> = BuildConfig.ONLINE_SOURCES
}
}

val LocationSettings.onlineSource: OnlineSource?
get() {
val id = onlineSourceId
if (id != null) {
val source = OnlineSource.ALL.firstOrNull { it.id == id }
if (source != null) return source
}
val endpoint = customEndpoint
if (endpoint != null) {
val endpointHostSuffix = runCatching { "." + Uri.parse(endpoint).host }.getOrNull()
if (endpointHostSuffix != null) {
for (source in OnlineSource.ALL) {
if (source.import && endpointHostSuffix.endsWith("." + source.host)) {
return source
}
}
}
val customSource = OnlineSource.ALL.firstOrNull { it.id == OnlineSource.ID_CUSTOM }
if (customSource != null && customSource.import) {
return customSource
}
}
if (OnlineSource.ALL.size == 1) return OnlineSource.ALL.single()
return OnlineSource.ALL.firstOrNull { it.id == OnlineSource.ID_DEFAULT }
}

val LocationSettings.effectiveEndpoint: String?
get() {
val source = onlineSource ?: return null
if (source.id == OnlineSource.ID_CUSTOM) return customEndpoint
return source.url
}
1 change: 1 addition & 0 deletions play-services-location/core/provider/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation project(':play-services-base-core')
implementation project(':play-services-location-core-base')

implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
Expand Down
13 changes: 13 additions & 0 deletions play-services-location/core/provider/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@
<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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

2 comments on commit 44a9a4d

@aer0nix
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't support import OpenCellID database.

@mar-v-in
Copy link
Member Author

@mar-v-in mar-v-in commented on 44a9a4d Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be documentation about the format elsewhere, but I put this here as a start:

The file needs to be a csv file, optionally gzip compressed. Optional columns must be present, but the value can be left empty. The column order does not matter.

For all:

  • lat: latitude as decimal number between -90 and 90.
  • lon: longitude as decimal number between -180 and 180.
  • alt: optional altitude in meters as decimal number.

For Wi-Fi:

  • mac: BSSID / MAC address in hexadecimal with leading zeros, in lower or upper case and with optional : byte delimiter

For cell towers:

  • mcc: MCC in decimal without leading zeros
  • mnc: MNC in decimal without leading zeros
  • type: Cell tower type
    • 0 or CDMA for CDMA
    • 1 or GSM for GSM
    • 2 or WCDMA for UMTS
    • 3 or LTE for LTE
    • 4 or TDSCDMA for TD-SCDMA
    • 5 or NR for 5G (New Radio 5G)
  • lac: LAC or TAC
    • For GSM, UMTS and TD-SCDMA: 16-bit Location Area Code as decimal number between 0 and 65535
    • For LTE: 16-bit Tracking Area Code as decimal number between 0 and 65535
    • For 5G: 24-bit Tracking Area Code as decimal number between 0 and 16777215
  • cid: CID, CI or NCI
    • For GSM: 16-bit GSM Cell Identity as decimal number between 0 and 65535
    • For UMTS and TD-SCDMA: 28-bit UMTS Cell Identity as decimal number between 0 and 268435455
    • For LTE: 28-bit Cell Identity as decimal number between 0 and 268435455
    • For 5G: 36-bit NR Cell Identity as decimal number between 0 and 68719476735
  • psc: optional PSC or PCI
    • For UMTS: 9-bit UMTS Primary Scrambling Code as decimal number between 0 and 511
    • For LTE: Physical Cell Id as decimal number between 0 and 503

It should be reasonably possible to convert all existing cell tower or wifi databases into this format. As you likely want to filter any existing database to only include data relevant for your region, conversion and filtering could happen at the same time.

Please sign in to comment.