diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt index 879e3f73..bf9b71b4 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt @@ -173,7 +173,14 @@ class MainContainer(private val context: Context) : IMainContainer { } 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 { @@ -189,6 +196,6 @@ class MainContainer(private val context: Context) : IMainContainer { val appPrefsFileName = "${context.packageName}_preferences" val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE) val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4" - return SettingsRepo(manager, appPreferences, appVersionName) + return SettingsRepo(context, manager, appPreferences, appVersionName) } } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt index 5b0a0998..9fa00f97 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt @@ -81,14 +81,21 @@ class DatabaseRepo( // launch all network requests concurrently val tleJobs = tleUrls.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 - val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) -> + val importedEntries = tleResults.flatMap { (url, stream) -> val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries -> 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() } // insert parsed data into the database diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 8d567a98..5d85007b 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update class SettingsRepo( + private val context: android.content.Context, private val locationManager: LocationManager, private val preferences: SharedPreferences, override val appVersionName: String @@ -186,23 +187,41 @@ class SettingsRepo( return true } - override fun setStationPosition(): Boolean { - if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false - try { - val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps) - val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet) - val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef - val location = locationManager.getLastKnownLocation(providerDef) - if (location == null || System.currentTimeMillis() - location.time > 600_000L) { - println("Requesting location for $provider provider") - locationManager.requestLocationUpdates(provider, 0L, 0f, this) - } else { - setStationPosition(location.latitude, location.longitude, location.altitude) - } - } catch (exception: SecurityException) { - println("No permissions were given - $exception") + /** GPS 定位: 一次性 getCurrentLocation(GPS 优先, 15 秒超时), 拿到位置才返回 true */ + override suspend fun setStationPosition(): Boolean { + // 权限前置: 无定位权限直接失败(不吞异常) + if (androidx.core.content.ContextCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_FINE_LOCATION + ) != android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + println("GPS: no fine location permission") + return false + } + if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false + return kotlinx.coroutines.suspendCancellableCoroutine { cont -> + val signal = android.os.CancellationSignal() + val handler = android.os.Handler(android.os.Looper.getMainLooper()) + 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 -> + 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 { diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/ShowToast.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/ShowToast.kt index b07b15ac..83f7e2ff 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/ShowToast.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/usecase/ShowToast.kt @@ -25,4 +25,8 @@ class ShowToast(private val context: Context) : IShowToast { override fun invoke(message: String) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } + + override fun invoke(resId: Int) { + invoke(context.getString(resId)) + } } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index 23b726e2..f2a5c3fe 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -45,7 +45,8 @@ interface ISettingsRepo { //region # Station position settings val stationPosition: StateFlow fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean - fun setStationPosition(): Boolean + /** GPS 定位(挂起): 拿到位置才返回 true; 权限缺失/超时/无信号返回 false */ + suspend fun setStationPosition(): Boolean fun setStationPosition(locator: String): Boolean //endregion diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt index 1bb99c75..7c20324b 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/usecase/IShowToast.kt @@ -19,4 +19,7 @@ package com.rtbishop.look4sat.core.domain.usecase interface IShowToast { operator fun invoke(message: String) + + /** 按资源 ID 显示(四语文案) */ + operator fun invoke(resId: Int) } diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index 9847463d..42aebd52 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -116,7 +116,7 @@ d MMM yyyy - HH:mm:ss Posisi stasiun GPS - Periksa izin lokasi Anda + Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan Input Lokasi yang dimasukkan tidak valid QTH @@ -135,6 +135,7 @@ Bersihkan Data berhasil dibersihkan Pembaruan berhasil diselesaikan + Pembaruan gagal - periksa jaringan Anda Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid. Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid. Sumber data kustom diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 6ce7a184..81d1360c 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -120,7 +120,7 @@ İstasyon konumu GPS - Konum izinlerinizi kontrol edin + Konum alınamıyor - izinleri ve GPS/ağ sinyalini kontrol edin Manuel giriş Geçersiz konum girildi QTH @@ -140,6 +140,7 @@ Temizle Veriler başarıyla temizlendi Güncelleme başarıyla tamamlandı + Güncelleme başarısız - ağınızı kontrol edin Özel veri kaynakları Özel TLE URL diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index b1eb5fbc..2c789b08 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -102,7 +102,7 @@ 站位 GPS 定位 - 检查您的位置权限 + 无法获取位置,请检查定位权限和GPS/网络信号 输入位置 输入的位置无效 QTH 网格 @@ -122,6 +122,7 @@ 清空 数据清空成功 更新成功 + 更新失败,请检查网络 未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。 未导入收发器。请选择有效的 SatNOGS (.json) 文件。 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index d3221a4f..14b19369 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -132,7 +132,7 @@ Station position GPS - Check your location permissions + Unable to get location - check permission and GPS/network signal Input Invalid location entered QTH @@ -152,6 +152,7 @@ Clear Data was cleared successfully Update completed successfully + Update failed - check your network No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file. No transceivers imported. Select a valid SatNOGS (.json) file. diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt index 04f014bd..8d835882 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt @@ -164,7 +164,16 @@ class SettingsViewModel( // region Position helpers — consolidated from 3 near-identical functions 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) { @@ -212,6 +221,7 @@ class SettingsViewModel( it.copy(dataSettings = it.dataSettings.copy(isUpdating = false)) } println(exception) + showToast(R.string.prefs_data_update_failed) } }