feat(roaming): port live GPS tracking and location-disabled hint
Completes the port of the QTH定位器 app's location logic (read from the decompiled MainActivity): 1. Live location updates: a LocationListener registers GPS+NETWORK providers (10s / 10m, matching the reference onResume) while the Roaming page is visible and removes itself on dispose. Every fix is pushed through settingsRepo.setStationPosition, so the shared stationPosition StateFlow updates the Settings page and the map in lockstep — the page now refreshes in real time instead of only showing stale cached coordinates. 2. Provider filtering: only gps/network fixes are accepted, mirroring the reference showLocation() guard that rejects passive fixes. 3. Location-disabled hint: when GPS is off or permission is missing, the header shows a tappable '定位未开启,点击前往系统设置' row that opens ACTION_LOCATION_SOURCE_SETTINGS — the port of the reference btnLocationSettings button. The GPS status dot now has three states: fresh fix (primary), provider on but stale (error), provider off (outline). 4. Periodic recovery: a 30s re-request loop (honoring the 漫游位置实时更新 toggle) re-arms the location request after the chip falls idle. Not ported (conflict, deliberate): the reference app's Play-store ad banner, 'New! Grid Square with map' promo and GP_IN preferences — commercial advertising does not belong in a GPL satellite tracker.
This commit is contained in:
1 parent
f9250ec1f1
commit
44c442ddd3
1 file changed
+104
-11
+104
-11
@@ -17,7 +17,18 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.roaming
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -36,8 +47,12 @@ import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -48,6 +63,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||
@@ -67,24 +83,78 @@ import java.util.Locale
|
||||
* and row are wider/taller, matching the reference app). Styled with the
|
||||
* Look4Sat Material 3 theme.
|
||||
*
|
||||
* Coordinates come straight from the shared settingsRepo.stationPosition
|
||||
* StateFlow — the exact same source the Settings page shows — so the two
|
||||
* pages can never disagree. The red marker is placed at the fractional
|
||||
* position derived from the 3rd character pair of the locator (ported from
|
||||
* the QTH定位器 app), scaled to the actual cell size, so it stays accurate
|
||||
* on any screen (phones, tablets, wide/narrow).
|
||||
* Live GPS: while the screen is shown (and the "漫游位置实时更新" setting is on)
|
||||
* a LocationManager listener requests gps+network fixes every 10s / 10m,
|
||||
* exactly like the reference app; each fix is pushed through the shared
|
||||
* settingsRepo.stationPosition StateFlow, so the Settings page and every
|
||||
* other consumer update in lockstep. The red marker is placed at the
|
||||
* fractional position derived from the 3rd character pair of the locator
|
||||
* (ported from the QTH定位器 app), scaled to the actual cell size, so it
|
||||
* stays accurate on any screen (phones, tablets, wide/narrow).
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun RoamingScreen() {
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val stationPos by container.settingsRepo.stationPosition.collectAsStateWithLifecycle()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
val settingsRepo = container.settingsRepo
|
||||
val stationPos by settingsRepo.stationPosition.collectAsStateWithLifecycle()
|
||||
val otherSettings by settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
|
||||
val state = remember(stationPos, otherSettings.stateOfRoamingLive) {
|
||||
RoamingState.fromPosition(stationPos)
|
||||
}
|
||||
|
||||
// Real-time location updates, ported from the reference app's onResume/onPause:
|
||||
// register gps+network listeners while visible, remove them when leaving.
|
||||
var gpsAvailable by remember { mutableStateOf(false) }
|
||||
DisposableEffect(Unit) {
|
||||
val locationManager = context.getSystemService(LocationManager::class.java)
|
||||
val hasPermission = ContextCompat.checkSelfPermission(
|
||||
context, Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
gpsAvailable = hasPermission && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
// Accept only real gps/network fixes, like the reference app
|
||||
if (location.provider != LocationManager.GPS_PROVIDER &&
|
||||
location.provider != LocationManager.NETWORK_PROVIDER
|
||||
) return
|
||||
settingsRepo.setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||
}
|
||||
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
||||
override fun onProviderEnabled(provider: String) {
|
||||
if (provider == LocationManager.GPS_PROVIDER) gpsAvailable = true
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
if (provider == LocationManager.GPS_PROVIDER) gpsAvailable = false
|
||||
}
|
||||
}
|
||||
if (hasPermission) {
|
||||
// Listen continuously while the page is visible (10s / 10m, like the reference)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER, 10_000L, 10f, listener
|
||||
)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER, 10_000L, 10f, listener
|
||||
)
|
||||
}
|
||||
onDispose {
|
||||
if (hasPermission) locationManager.removeUpdates(listener)
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic refresh when live updates are enabled: re-request a fix every 30s
|
||||
// so the page also recovers after the GPS chip falls idle.
|
||||
LaunchedEffect(otherSettings.stateOfRoamingLive) {
|
||||
while (otherSettings.stateOfRoamingLive) {
|
||||
settingsRepo.setStationPosition()
|
||||
kotlinx.coroutines.delay(30_000L)
|
||||
}
|
||||
}
|
||||
|
||||
ScreenColumn(
|
||||
topBar = {
|
||||
TopBar { Text(text = "漫游", fontSize = 20.sp, fontWeight = FontWeight.Bold) }
|
||||
@@ -95,15 +165,16 @@ fun RoamingScreen() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing.medium)
|
||||
) {
|
||||
InfoCard(state = state)
|
||||
InfoCard(state = state, gpsAvailable = gpsAvailable)
|
||||
GridCard(state = state, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(state: RoamingState) {
|
||||
private fun InfoCard(state: RoamingState, gpsAvailable: Boolean) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val context = LocalContext.current
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -124,7 +195,13 @@ private fun InfoCard(state: RoamingState) {
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (state.gpsEnabled) colorScheme.primary else colorScheme.outline)
|
||||
.background(
|
||||
when {
|
||||
state.gpsEnabled -> colorScheme.primary
|
||||
gpsAvailable -> colorScheme.error
|
||||
else -> colorScheme.outline
|
||||
}
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
@@ -140,6 +217,22 @@ private fun InfoCard(state: RoamingState) {
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
// GPS disabled hint, ported from the reference app's btnLocationSettings:
|
||||
// opens the system location settings so the user can re-enable GPS.
|
||||
if (!gpsAvailable) {
|
||||
Text(
|
||||
text = "定位未开启,点击前往系统设置",
|
||||
fontSize = 13.sp,
|
||||
color = colorScheme.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable {
|
||||
context.startActivity(Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
}
|
||||
.padding(vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
// Coordinates: DMS + decimal
|
||||
CoordinateRow(label = "纬度", value = state.latitude, isLat = true)
|
||||
CoordinateRow(label = "经度", value = state.longitude, isLat = false)
|
||||
|
||||
Reference in new issue
Block a user