mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-20 23:11:24 +02:00
feat(geolocation): support for non-GMS devices (#3377)
* chore(geolocation): update gradle * feat(geolocation): add support for android devices without GMS * fix(geolocation): watchPosition does not resolve on android * feat(example): add watch support for geolocation * chore: add changeset * Update .changes/geolocation-non-gms.md Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> * fix(geolocation): only use Android Location API on API Level >= 31 * fix resolve * fix(geolocation): use LocationManagerCompat for non-GMS devices - support API 24+ instead of only API 31+ - pick the best enabled provider (fused, gps, network) instead of GPS only - honor enableHighAccuracy in getCurrentPosition - clear the previous listener before starting a new watch - report an error when the provider is disabled during a watch - share the provider/priority logic between both code paths - drop the geolocation-js bump, no JS changes * fix(examples): clear existing geolocation watch before starting a new one * chore(geolocation): add change file for watchPosition resolve fix * fix(geolocation): honor enableHighAccuracy in getCurrentPosition on Android 12+ without GMS * fix(geolocation): fall back to the next provider when one returns no fix on Android without GMS * fix(geolocation): honor timeout in getCurrentPosition on Android * fix(geolocation): keep multiple watchPosition watches active on Android * chore(geolocation): bump geolocation-js alongside geolocation in change files * refactor(geolocation): report provider errors from the callers of androidProviders * e2e tests --------- Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com> Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com> Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
15 files changed
+573
-64
No files matched your search
@@ -8,50 +8,140 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.location.LocationRequest
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import androidx.core.os.CancellationSignal as CompatCancellationSignal
|
||||
import app.tauri.Logger
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.location.CurrentLocationRequest
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationRequest as GmsLocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.Priority
|
||||
|
||||
|
||||
public class Geolocation(private val context: Context) {
|
||||
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private var fusedLocationClient: FusedLocationProviderClient? = null
|
||||
private var locationCallback: LocationCallback? = null
|
||||
|
||||
// Active watches, keyed by watch id.
|
||||
private val locationCallbacks = HashMap<Long, LocationCallback>() // For gms
|
||||
private val locationListeners = HashMap<Long, LocationListenerCompat>() // For android
|
||||
|
||||
fun isLocationServicesEnabled(): Boolean {
|
||||
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
return LocationManagerCompat.isLocationEnabled(lm)
|
||||
return LocationManagerCompat.isLocationEnabled(locationManager)
|
||||
}
|
||||
|
||||
@SuppressWarnings("MissingPermission")
|
||||
fun sendLocation(enableHighAccuracy: Boolean, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) {
|
||||
val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
|
||||
if (resultCode == ConnectionResult.SUCCESS) {
|
||||
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private fun isGmsAvailable(): Boolean {
|
||||
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
|
||||
}
|
||||
|
||||
if (this.isLocationServicesEnabled()) {
|
||||
var networkEnabled = false
|
||||
private fun gmsPriority(enableHighAccuracy: Boolean): Int {
|
||||
if (enableHighAccuracy) {
|
||||
return Priority.PRIORITY_HIGH_ACCURACY
|
||||
}
|
||||
|
||||
try {
|
||||
networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
} catch (_: Exception) {
|
||||
Logger.error("isProviderEnabled failed")
|
||||
var networkEnabled = false
|
||||
try {
|
||||
networkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
} catch (_: Exception) {
|
||||
Logger.error("isProviderEnabled failed")
|
||||
}
|
||||
|
||||
return if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER
|
||||
}
|
||||
|
||||
// Lists the enabled LocationManager providers for devices without Google Play Services, best first.
|
||||
private fun androidProviders(enableHighAccuracy: Boolean): List<String> {
|
||||
val fused = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) listOf(LocationManager.FUSED_PROVIDER) else emptyList()
|
||||
val candidates = if (enableHighAccuracy) {
|
||||
fused + LocationManager.GPS_PROVIDER + LocationManager.NETWORK_PROVIDER
|
||||
} else {
|
||||
fused + LocationManager.NETWORK_PROVIDER + LocationManager.GPS_PROVIDER
|
||||
}
|
||||
val enabled = locationManager.getProviders(true)
|
||||
return candidates.filter { it in enabled }
|
||||
}
|
||||
|
||||
// Asks each provider in turn for a fresh fix, so one that is enabled but can't produce a fix
|
||||
// (e.g. network without Wi-Fi or cell signal) falls back to the next.
|
||||
// The timeout covers all attempts; 0 or less means no timeout.
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun androidCurrentLocation(providers: List<String>, enableHighAccuracy: Boolean, timeout: Long, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) {
|
||||
// Everything below runs on the main thread, so no synchronization is needed.
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var done = false
|
||||
var cancelAttempt: (() -> Unit)? = null
|
||||
val onTimeout = Runnable {
|
||||
if (!done) {
|
||||
done = true
|
||||
cancelAttempt?.invoke()
|
||||
errorCallback("Timeout expired.")
|
||||
}
|
||||
}
|
||||
if (timeout > 0) {
|
||||
handler.postDelayed(onTimeout, timeout)
|
||||
}
|
||||
|
||||
fun attempt(remaining: List<String>) {
|
||||
val provider = remaining.firstOrNull()
|
||||
if (provider == null) {
|
||||
done = true
|
||||
handler.removeCallbacks(onTimeout)
|
||||
errorCallback("Location unavailable.")
|
||||
return
|
||||
}
|
||||
val onResult = { location: Location? ->
|
||||
if (!done) {
|
||||
if (location == null) {
|
||||
attempt(remaining.drop(1))
|
||||
} else {
|
||||
done = true
|
||||
handler.removeCallbacks(onTimeout)
|
||||
successCallback(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// On API 31+ the provider is usually fused, so the request quality is what honors enableHighAccuracy.
|
||||
val request = LocationRequest.Builder(0)
|
||||
.setQuality(if (enableHighAccuracy) LocationRequest.QUALITY_HIGH_ACCURACY else LocationRequest.QUALITY_BALANCED_POWER_ACCURACY)
|
||||
.build()
|
||||
val signal = CancellationSignal()
|
||||
cancelAttempt = { signal.cancel() }
|
||||
locationManager.getCurrentLocation(provider, request, signal, context.mainExecutor) { onResult(it) }
|
||||
} else {
|
||||
val signal = CompatCancellationSignal()
|
||||
cancelAttempt = { signal.cancel() }
|
||||
LocationManagerCompat.getCurrentLocation(locationManager, provider, signal, ContextCompat.getMainExecutor(context)) { onResult(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val lowPrio = if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER
|
||||
val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio
|
||||
attempt(providers)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun sendLocation(enableHighAccuracy: Boolean, timeout: Long, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) {
|
||||
if (isGmsAvailable()) {
|
||||
if (this.isLocationServicesEnabled()) {
|
||||
val currentLocationRequest = CurrentLocationRequest.Builder()
|
||||
.setPriority(gmsPriority(enableHighAccuracy))
|
||||
if (timeout > 0) {
|
||||
currentLocationRequest.setDurationMillis(timeout)
|
||||
}
|
||||
LocationServices
|
||||
.getFusedLocationProviderClient(context)
|
||||
.getCurrentLocation(prio, null)
|
||||
.getCurrentLocation(currentLocationRequest.build(), null)
|
||||
.addOnFailureListener { e -> e.message?.let { errorCallback(it) } }
|
||||
.addOnSuccessListener { location ->
|
||||
if (location == null) {
|
||||
@@ -63,39 +153,29 @@ public class Geolocation(private val context: Context) {
|
||||
} else {
|
||||
errorCallback("Location disabled.")
|
||||
}
|
||||
} else if (!this.isLocationServicesEnabled()) {
|
||||
errorCallback("Location disabled.")
|
||||
} else {
|
||||
errorCallback("Google Play Services unavailable.")
|
||||
// Reports "Location unavailable." itself when there is no provider.
|
||||
androidCurrentLocation(androidProviders(enableHighAccuracy), enableHighAccuracy, timeout, successCallback, errorCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun requestLocationUpdates(enableHighAccuracy: Boolean, timeout: Long, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) {
|
||||
val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
|
||||
if (resultCode == ConnectionResult.SUCCESS) {
|
||||
clearLocationUpdates()
|
||||
fun requestLocationUpdates(id: Long, enableHighAccuracy: Boolean, timeout: Long, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) {
|
||||
clearLocationUpdates(id)
|
||||
|
||||
if (isGmsAvailable()) {
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||
|
||||
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
if (this.isLocationServicesEnabled()) {
|
||||
var networkEnabled = false
|
||||
|
||||
try {
|
||||
networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
} catch (_: Exception) {
|
||||
Logger.error("isProviderEnabled failed")
|
||||
}
|
||||
|
||||
val lowPrio = if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER
|
||||
val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio
|
||||
|
||||
val locationRequest = LocationRequest.Builder(timeout)
|
||||
val locationRequest = GmsLocationRequest.Builder(timeout)
|
||||
.setMaxUpdateDelayMillis(timeout)
|
||||
.setMinUpdateIntervalMillis(timeout)
|
||||
.setPriority(prio)
|
||||
.setPriority(gmsPriority(enableHighAccuracy))
|
||||
.build()
|
||||
|
||||
locationCallback =
|
||||
val locationCallback =
|
||||
object : LocationCallback() {
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
val lastLocation = locationResult.lastLocation
|
||||
@@ -107,29 +187,53 @@ public class Geolocation(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback!!, null)
|
||||
locationCallbacks[id] = locationCallback
|
||||
fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback, null)
|
||||
} else {
|
||||
errorCallback("Location disabled.")
|
||||
}
|
||||
} else if (!this.isLocationServicesEnabled()) {
|
||||
errorCallback("Location disabled.")
|
||||
} else {
|
||||
errorCallback("Google Play Services not available.")
|
||||
val provider = androidProviders(enableHighAccuracy).firstOrNull()
|
||||
if (provider == null) {
|
||||
errorCallback("Location unavailable.")
|
||||
return
|
||||
}
|
||||
val request = LocationRequestCompat.Builder(timeout)
|
||||
.setQuality(if (enableHighAccuracy) LocationRequestCompat.QUALITY_HIGH_ACCURACY else LocationRequestCompat.QUALITY_LOW_POWER)
|
||||
.build()
|
||||
val listener = object : LocationListenerCompat {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
successCallback(location)
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
errorCallback("Location disabled.")
|
||||
}
|
||||
}
|
||||
locationListeners[id] = listener
|
||||
LocationManagerCompat.requestLocationUpdates(locationManager, provider, request, ContextCompat.getMainExecutor(context), listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLocationUpdates() {
|
||||
if (locationCallback != null) {
|
||||
fusedLocationClient?.removeLocationUpdates(locationCallback!!)
|
||||
locationCallback = null
|
||||
fun clearLocationUpdates(id: Long) {
|
||||
locationCallbacks.remove(id)?.let { fusedLocationClient?.removeLocationUpdates(it) }
|
||||
locationListeners.remove(id)?.let { LocationManagerCompat.removeUpdates(locationManager, it) }
|
||||
}
|
||||
|
||||
fun clearAllLocationUpdates() {
|
||||
for (id in locationCallbacks.keys + locationListeners.keys) {
|
||||
clearLocationUpdates(id)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun getLastLocation(maximumAge: Long): Location? {
|
||||
var lastLoc: Location? = null
|
||||
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
for (provider in lm.allProviders) {
|
||||
val tmpLoc = lm.getLastKnownLocation(provider)
|
||||
for (provider in locationManager.allProviders) {
|
||||
val tmpLoc = locationManager.getLastKnownLocation(provider)
|
||||
if (tmpLoc != null) {
|
||||
val locationAge = SystemClock.elapsedRealtimeNanos() - tmpLoc.elapsedRealtimeNanos
|
||||
val maxAgeNano = maximumAge * 1000000L
|
||||
|
||||
@@ -71,7 +71,7 @@ class GeolocationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Clear all location updates on pause to avoid possible background location calls
|
||||
implementation.clearLocationUpdates()
|
||||
implementation.clearAllLocationUpdates()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -108,7 +108,7 @@ class GeolocationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
if (location != null) {
|
||||
invoke.resolve(convertLocation(location))
|
||||
} else {
|
||||
implementation.sendLocation(args.enableHighAccuracy,
|
||||
implementation.sendLocation(args.enableHighAccuracy, args.timeout,
|
||||
{ loc -> invoke.resolve(convertLocation(loc)) },
|
||||
{ error -> invoke.reject(error) })
|
||||
}
|
||||
@@ -126,10 +126,12 @@ class GeolocationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
fun watchPosition(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(WatchArgs::class.java)
|
||||
startWatch(invoke, args)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
private fun startWatch(invoke: Invoke, args: WatchArgs) {
|
||||
implementation.requestLocationUpdates(
|
||||
args.channel.id,
|
||||
args.options.enableHighAccuracy,
|
||||
args.options.timeout,
|
||||
{ location -> args.channel.send(convertLocation(location)) },
|
||||
@@ -143,10 +145,7 @@ class GeolocationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
val args = invoke.parseArgs(ClearWatchArgs::class.java)
|
||||
|
||||
watchers.remove(args.channelId)
|
||||
|
||||
if (watchers.isEmpty()) {
|
||||
implementation.clearLocationUpdates()
|
||||
}
|
||||
implementation.clearLocationUpdates(args.channelId)
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user