fix(android): drop geolocator for a native LocationManager channel
Removes the geolocator dependency, which transitively embedded com.google.android.gms.* (play-services-location) — proprietary classes that F-Droid's scanner rejects. The optional coarse-location button now talks to Android's own LocationManager over a small platform channel (org.comunes.tane/coarse_location): permission prompt, single coarse fix with a 12s timeout, last-known fallback, null on any failure. Behavior is unchanged and the CoarseLocationProvider interface is preserved, so the UI, i18n and widget tests need no changes. Also sets dependenciesInfo.includeInApk/Bundle = false so the AGP dependency-metadata block (also flagged by F-Droid) is not embedded. Neither change affects the Google Play build.
This commit is contained in:
parent
7f1118a243
commit
d0fabb80a9
9 changed files with 264 additions and 93 deletions
|
|
@ -1,5 +1,172 @@
|
|||
package org.comunes.tane
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
/**
|
||||
* Hosts the "coarse location" platform channel backed by Android's own
|
||||
* LocationManager. Deliberately no Google Play Services: the APK must stay
|
||||
* free of proprietary classes (F-Droid). Mirrors the old plugin's behavior:
|
||||
* ask permission if needed, try a single coarse fix with a timeout, fall
|
||||
* back to the last known position, and reply null on any failure — never
|
||||
* throw across the channel.
|
||||
*/
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var pendingPermissionResult: MethodChannel.Result? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getCoarseLatLon" -> getCoarseLatLon(result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCoarseLatLon(result: MethodChannel.Result) {
|
||||
if (hasLocationPermission()) {
|
||||
fetchCoarseLocation(result)
|
||||
return
|
||||
}
|
||||
if (pendingPermissionResult != null) {
|
||||
// A permission prompt is already on screen; don't stack requests.
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
pendingPermissionResult = result
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
|
||||
PERMISSION_REQUEST_CODE,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode != PERMISSION_REQUEST_CODE) return
|
||||
val result = pendingPermissionResult ?: return
|
||||
pendingPermissionResult = null
|
||||
if (hasLocationPermission()) {
|
||||
fetchCoarseLocation(result)
|
||||
} else {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasLocationPermission(): Boolean =
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
private fun fetchCoarseLocation(result: MethodChannel.Result) {
|
||||
val reply = SingleReply(result)
|
||||
try {
|
||||
val lm = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val provider = pickProvider(lm)
|
||||
if (provider == null) {
|
||||
reply.send(lastKnownLocation(lm))
|
||||
return
|
||||
}
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val cancel = CancellationSignal()
|
||||
handler.postDelayed({
|
||||
if (!reply.sent) {
|
||||
cancel.cancel()
|
||||
reply.send(lastKnownLocation(lm))
|
||||
}
|
||||
}, FIX_TIMEOUT_MS)
|
||||
lm.getCurrentLocation(provider, cancel, mainExecutor) { location ->
|
||||
reply.send(location ?: lastKnownLocation(lm))
|
||||
}
|
||||
} else {
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
reply.send(location)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onStatusChanged(
|
||||
provider: String?,
|
||||
status: Int,
|
||||
extras: android.os.Bundle?,
|
||||
) {}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
}
|
||||
handler.postDelayed({
|
||||
if (!reply.sent) {
|
||||
try {
|
||||
lm.removeUpdates(listener)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
reply.send(lastKnownLocation(lm))
|
||||
}
|
||||
}, FIX_TIMEOUT_MS)
|
||||
@Suppress("DEPRECATION")
|
||||
lm.requestSingleUpdate(provider, listener, Looper.getMainLooper())
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// SecurityException, disabled providers, anything: degrade quietly.
|
||||
reply.send(null)
|
||||
}
|
||||
}
|
||||
|
||||
/** Coarse first: the network provider is enough for a ~2 km geohash. */
|
||||
private fun pickProvider(lm: LocationManager): String? = when {
|
||||
lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ->
|
||||
LocationManager.NETWORK_PROVIDER
|
||||
lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ->
|
||||
LocationManager.GPS_PROVIDER
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun lastKnownLocation(lm: LocationManager): Location? = try {
|
||||
lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
|
||||
?: lm.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
?: lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
/** Answers a MethodChannel.Result exactly once (fix vs. timeout race). */
|
||||
private class SingleReply(private val result: MethodChannel.Result) {
|
||||
var sent = false
|
||||
private set
|
||||
|
||||
fun send(location: Location?) {
|
||||
if (sent) return
|
||||
sent = true
|
||||
result.success(
|
||||
location?.let { mapOf("lat" to it.latitude, "lon" to it.longitude) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CHANNEL = "org.comunes.tane/coarse_location"
|
||||
const val PERMISSION_REQUEST_CODE = 4711
|
||||
const val FIX_TIMEOUT_MS = 12_000L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue