fix(gps,data): official-code fixes - real GPS result callback, no fake update success (4.5.2, release pending)

Official code issues found during review (user-reported):
1. GPS "success" was shown instantly even when no fix was obtained:
   - setStationPosition() always returned true (permissions exception
     swallowed, async requestLocationUpdates without waiting)
   - now: suspend + LocationManagerCompat.getCurrentLocation (GPS
     first, network fallback), permission check upfront, 15s timeout,
     success only when onLocationChanged fires; SettingsRepo takes
     Context for the permission check; ViewModel waits for the real
     result and shows "Unable to get location - check permission and
     GPS/network signal" on failure (4 locales)
2. Data update faked success on total failure:
   - updateFromRemote now counts successful sources; 0 success throws
     IOException -> timestamp NOT refreshed, Toast "Update failed -
     check your network" (4 locales, new IShowToast resId overload)
   - OkHttp timeouts widened: connect 15s / read 20s / write 20s

Verified: check_strings OK; all modules compile.
Release intentionally NOT triggered (user: fix everything first, then
one release).
This commit is contained in:
mckero committed 2026-08-04 14:08:38 +00:00
1 parent c09e87fa4e
commit cda6cf8583
11 files changed
+81 -26

No files matched your search

@@ -173,7 +173,14 @@ class MainContainer(private val context: Context) : IMainContainer {
} }
private fun provideRemoteSource(): IRemoteSource { private fun provideRemoteSource(): IRemoteSource {
return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build()) return RemoteSource(
Dispatchers.IO, context.contentResolver,
OkHttpClient.Builder()
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.build()
)
} }
private fun provideSatelliteRepo(): ISatelliteRepo { private fun provideSatelliteRepo(): ISatelliteRepo {
@@ -189,6 +196,6 @@ class MainContainer(private val context: Context) : IMainContainer {
val appPrefsFileName = "${context.packageName}_preferences" val appPrefsFileName = "${context.packageName}_preferences"
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE) val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4" val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
return SettingsRepo(manager, appPreferences, appVersionName) return SettingsRepo(context, manager, appPreferences, appVersionName)
} }
} }
@@ -81,14 +81,21 @@ class DatabaseRepo(
// launch all network requests concurrently // launch all network requests concurrently
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
// 统计成功源数: 0 成功视为更新失败(不刷时间戳, 抛异常让 UI 提示)
val tleResults = tleJobs.awaitAll()
val radioResults = radioJobs.awaitAll()
val successCount = tleResults.count { it.second != null } + radioResults.count { it.second != null }
if (successCount == 0) {
throw java.io.IOException("All data sources failed to download")
}
// parse fetched data concurrently and associate with types // parse fetched data concurrently and associate with types
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) -> val importedEntries = tleResults.flatMap { (url, stream) ->
val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries -> stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum }) settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum })
} }
} }
val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) -> val importedRadios = radioResults.flatMap { (url, stream) ->
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty() stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
} }
// insert parsed data into the database // insert parsed data into the database
@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class SettingsRepo( class SettingsRepo(
private val context: android.content.Context,
private val locationManager: LocationManager, private val locationManager: LocationManager,
private val preferences: SharedPreferences, private val preferences: SharedPreferences,
override val appVersionName: String override val appVersionName: String
@@ -186,23 +187,41 @@ class SettingsRepo(
return true return true
} }
override fun setStationPosition(): Boolean { /** GPS 定位: 一次性 getCurrentLocation(GPS 优先, 15 秒超时), 拿到位置才返回 true */
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false override suspend fun setStationPosition(): Boolean {
try { // 权限前置: 无定位权限直接失败(不吞异常)
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps) if (androidx.core.content.ContextCompat.checkSelfPermission(
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet) context, android.Manifest.permission.ACCESS_FINE_LOCATION
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef ) != android.content.pm.PackageManager.PERMISSION_GRANTED
val location = locationManager.getLastKnownLocation(providerDef) ) {
if (location == null || System.currentTimeMillis() - location.time > 600_000L) { println("GPS: no fine location permission")
println("Requesting location for $provider provider") return false
locationManager.requestLocationUpdates(provider, 0L, 0f, this) }
} else { if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
setStationPosition(location.latitude, location.longitude, location.altitude) return kotlinx.coroutines.suspendCancellableCoroutine { cont ->
} val signal = android.os.CancellationSignal()
} catch (exception: SecurityException) { val handler = android.os.Handler(android.os.Looper.getMainLooper())
println("No permissions were given - $exception") val executor = java.util.concurrent.Executor { handler.post(it) }
// 15 秒超时
val timeout = handler.postDelayed({
signal.cancel()
if (cont.isActive) cont.resume(false) { }
}, 15_000L)
val listener = androidx.core.util.Consumer<Location> { location ->
handler.removeCallbacksAndMessages(null)
setStationPosition(location.latitude, location.longitude, location.altitude)
if (cont.isActive) cont.resume(true) { }
}
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
val provider = if (hasGps) providerGps else providerNet
try {
LocationManagerCompat.getCurrentLocation(locationManager, provider, signal, executor, listener)
} catch (exception: SecurityException) {
handler.removeCallbacksAndMessages(null)
if (cont.isActive) cont.resume(false) { }
}
cont.invokeOnCancellation { signal.cancel(); handler.removeCallbacksAndMessages(null) }
} }
return true
} }
override fun setStationPosition(locator: String): Boolean { override fun setStationPosition(locator: String): Boolean {
@@ -25,4 +25,8 @@ class ShowToast(private val context: Context) : IShowToast {
override fun invoke(message: String) { override fun invoke(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} }
override fun invoke(resId: Int) {
invoke(context.getString(resId))
}
} }
@@ -45,7 +45,8 @@ interface ISettingsRepo {
//region # Station position settings //region # Station position settings
val stationPosition: StateFlow<GeoPos> val stationPosition: StateFlow<GeoPos>
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPosition(): Boolean /** GPS 定位(挂起): 拿到位置才返回 true; 权限缺失/超时/无信号返回 false */
suspend fun setStationPosition(): Boolean
fun setStationPosition(locator: String): Boolean fun setStationPosition(locator: String): Boolean
//endregion //endregion
@@ -19,4 +19,7 @@ package com.rtbishop.look4sat.core.domain.usecase
interface IShowToast { interface IShowToast {
operator fun invoke(message: String) operator fun invoke(message: String)
/** 按资源 ID 显示(四语文案) */
operator fun invoke(resId: Int)
} }
@@ -116,7 +116,7 @@
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string> <string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
<string name="prefs_loc_title">Posisi stasiun</string> <string name="prefs_loc_title">Posisi stasiun</string>
<string name="prefs_loc_gps_title">GPS</string> <string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Periksa izin lokasi Anda</string> <string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
<string name="prefs_loc_input_title">Input</string> <string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string> <string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
<string name="prefs_loc_qth_title">QTH</string> <string name="prefs_loc_qth_title">QTH</string>
@@ -135,6 +135,7 @@
<string name="prefs_data_clear">Bersihkan</string> <string name="prefs_data_clear">Bersihkan</string>
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string> <string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string> <string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string> <string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string> <string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
<string name="prefs_data_sources_title">Sumber data kustom</string> <string name="prefs_data_sources_title">Sumber data kustom</string>
@@ -120,7 +120,7 @@
<string name="prefs_loc_title">İstasyon konumu</string> <string name="prefs_loc_title">İstasyon konumu</string>
<string name="prefs_loc_gps_title">GPS</string> <string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Konum izinlerinizi kontrol edin</string> <string name="prefs_loc_gps_error">Konum alınamıyor - izinleri ve GPS/ağ sinyalini kontrol edin</string>
<string name="prefs_loc_input_title">Manuel giriş</string> <string name="prefs_loc_input_title">Manuel giriş</string>
<string name="prefs_loc_input_error">Geçersiz konum girildi</string> <string name="prefs_loc_input_error">Geçersiz konum girildi</string>
<string name="prefs_loc_qth_title">QTH</string> <string name="prefs_loc_qth_title">QTH</string>
@@ -140,6 +140,7 @@
<string name="prefs_data_clear">Temizle</string> <string name="prefs_data_clear">Temizle</string>
<string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string> <string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string>
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string> <string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
<string name="prefs_data_update_failed">Güncelleme başarısız - ağınızı kontrol edin</string>
<string name="prefs_data_sources_title">Özel veri kaynakları</string> <string name="prefs_data_sources_title">Özel veri kaynakları</string>
<string name="prefs_data_sources_tle_switch">Özel TLE URL</string> <string name="prefs_data_sources_tle_switch">Özel TLE URL</string>
@@ -102,7 +102,7 @@
<string name="prefs_loc_title">站位</string> <string name="prefs_loc_title">站位</string>
<string name="prefs_loc_gps_title">GPS 定位</string> <string name="prefs_loc_gps_title">GPS 定位</string>
<string name="prefs_loc_gps_error">检查您的位置权限</string> <string name="prefs_loc_gps_error">无法获取位置,请检查定位权限和GPS/网络信号</string>
<string name="prefs_loc_input_title">输入位置</string> <string name="prefs_loc_input_title">输入位置</string>
<string name="prefs_loc_input_error">输入的位置无效</string> <string name="prefs_loc_input_error">输入的位置无效</string>
<string name="prefs_loc_qth_title">QTH 网格</string> <string name="prefs_loc_qth_title">QTH 网格</string>
@@ -122,6 +122,7 @@
<string name="prefs_data_clear">清空</string> <string name="prefs_data_clear">清空</string>
<string name="prefs_data_clear_success">数据清空成功</string> <string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string> <string name="prefs_data_update_success">更新成功</string>
<string name="prefs_data_update_failed">更新失败,请检查网络</string>
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string> <string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string> <string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
@@ -132,7 +132,7 @@
<string name="prefs_loc_title">Station position</string> <string name="prefs_loc_title">Station position</string>
<string name="prefs_loc_gps_title">GPS</string> <string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Check your location permissions</string> <string name="prefs_loc_gps_error">Unable to get location - check permission and GPS/network signal</string>
<string name="prefs_loc_input_title">Input</string> <string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Invalid location entered</string> <string name="prefs_loc_input_error">Invalid location entered</string>
<string name="prefs_loc_qth_title">QTH</string> <string name="prefs_loc_qth_title">QTH</string>
@@ -152,6 +152,7 @@
<string name="prefs_data_clear">Clear</string> <string name="prefs_data_clear">Clear</string>
<string name="prefs_data_clear_success">Data was cleared successfully</string> <string name="prefs_data_clear_success">Data was cleared successfully</string>
<string name="prefs_data_update_success">Update completed successfully</string> <string name="prefs_data_update_success">Update completed successfully</string>
<string name="prefs_data_update_failed">Update failed - check your network</string>
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string> <string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string> <string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
@@ -164,7 +164,16 @@ class SettingsViewModel(
// region Position helpers — consolidated from 3 near-identical functions // region Position helpers — consolidated from 3 near-identical functions
private fun setGpsPosition() { private fun setGpsPosition() {
updatePosition(R.string.prefs_loc_gps_error) { settingsRepo.setStationPosition() } viewModelScope.launch {
_uiState.update { it.copy(positionSettings = it.positionSettings.copy(isUpdating = true)) }
val success = settingsRepo.setStationPosition()
_uiState.update {
it.copy(positionSettings = it.positionSettings.copy(
isUpdating = false,
messageResId = if (success) R.string.prefs_loc_success else R.string.prefs_loc_gps_error
))
}
}
} }
private fun setGeoPosition(latitude: Double, longitude: Double) { private fun setGeoPosition(latitude: Double, longitude: Double) {
@@ -212,6 +221,7 @@ class SettingsViewModel(
it.copy(dataSettings = it.dataSettings.copy(isUpdating = false)) it.copy(dataSettings = it.dataSettings.copy(isUpdating = false))
} }
println(exception) println(exception)
showToast(R.string.prefs_data_update_failed)
} }
} }