From faab5ce9fc93c2a57e52647acd6aa97bbb1ce646 Mon Sep 17 00:00:00 2001 From: Nils Loewen Date: Wed, 31 Dec 2025 22:02:57 -0800 Subject: [PATCH 1/2] Geo works on android --- examples/geolocation/AndroidManifest.xml | 36 ++ examples/geolocation/Cargo.toml | 3 +- examples/geolocation/Dioxus.toml | 2 + packages/geolocation/Cargo.toml | 4 + packages/geolocation/src/core.rs | 2 +- packages/geolocation/src/platform/android.rs | 439 +++++++++++++++++++ packages/geolocation/src/platform/mod.rs | 9 +- 7 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 examples/geolocation/AndroidManifest.xml create mode 100644 examples/geolocation/Dioxus.toml create mode 100644 packages/geolocation/src/platform/android.rs diff --git a/examples/geolocation/AndroidManifest.xml b/examples/geolocation/AndroidManifest.xml new file mode 100644 index 0000000..17952d8 --- /dev/null +++ b/examples/geolocation/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/examples/geolocation/Cargo.toml b/examples/geolocation/Cargo.toml index 19b5377..b7f85db 100644 --- a/examples/geolocation/Cargo.toml +++ b/examples/geolocation/Cargo.toml @@ -5,11 +5,12 @@ edition = "2021" publish = false [dependencies] -dioxus = { workspace = true } +dioxus = "0.7.0-alpha0" dioxus-sdk-geolocation = { workspace = true } [features] default = ["desktop"] web = ["dioxus/web"] desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] diff --git a/examples/geolocation/Dioxus.toml b/examples/geolocation/Dioxus.toml new file mode 100644 index 0000000..462b019 --- /dev/null +++ b/examples/geolocation/Dioxus.toml @@ -0,0 +1,2 @@ +[application] +android_manifest = "./AndroidManifest.xml" \ No newline at end of file diff --git a/packages/geolocation/Cargo.toml b/packages/geolocation/Cargo.toml index 2585c12..a5dfdc0 100644 --- a/packages/geolocation/Cargo.toml +++ b/packages/geolocation/Cargo.toml @@ -34,3 +34,7 @@ windows = { version = "0.48.0", features = [ "Foundation", "Devices_Geolocation", ] } + +[target.'cfg(target_os = "android")'.dependencies] +dioxus = { workspace = true, features = ["mobile"] } +jni = "0.21.1" diff --git a/packages/geolocation/src/core.rs b/packages/geolocation/src/core.rs index e6f6779..cfc26e1 100644 --- a/packages/geolocation/src/core.rs +++ b/packages/geolocation/src/core.rs @@ -13,7 +13,7 @@ pub struct Geocoordinates { } /// To conserve battery, some devices allow setting a desired accuracy based on your use-case. -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum PowerMode { /// Will generally enable the on-board GPS for precise coordinates. High, diff --git a/packages/geolocation/src/platform/android.rs b/packages/geolocation/src/platform/android.rs new file mode 100644 index 0000000..74050ea --- /dev/null +++ b/packages/geolocation/src/platform/android.rs @@ -0,0 +1,439 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; +use std::time::Duration; + +use jni::JNIEnv; +use jni::objects::{JObject, JValue}; + +use crate::core::{Error, Event, Geocoordinates, PowerMode, Status}; + +const FINE_LOCATION_PERMISSION: &str = "android.permission.ACCESS_FINE_LOCATION"; +const COARSE_LOCATION_PERMISSION: &str = "android.permission.ACCESS_COARSE_LOCATION"; + +fn permission_for_power_mode(power_mode: PowerMode) -> &'static str { + match power_mode { + PowerMode::High => FINE_LOCATION_PERMISSION, + PowerMode::Low => COARSE_LOCATION_PERMISSION, + } +} + +/// Represents the geolocator for Android. +pub struct Geolocator { + power_mode: PowerMode, + stop_listening: Arc, +} + +impl Geolocator { + /// Create a new Geolocator for the device. + /// This will request location permissions if not already granted and wait for user response. + pub fn new() -> Result { + use std::sync::mpsc::channel; + + let permission = FINE_LOCATION_PERMISSION; + let already_granted = check_location_permission(permission)?; + + if !already_granted { + let (tx, rx) = channel(); + + dioxus::mobile::wry::prelude::dispatch( + move |env: &mut JNIEnv, activity: &JObject, _webview| { + let permission_str = env.new_string(permission).unwrap(); + let permissions = env + .new_object_array(1, "java/lang/String", &permission_str) + .unwrap(); + + let result = env.call_method( + activity, + "requestPermissions", + "([Ljava/lang/String;I)V", + &[JValue::Object(&permissions.into()), JValue::Int(1)], + ); + + tx.send(result.is_ok()).unwrap(); + }, + ); + + rx.recv().map_err(|e| Error::DeviceError(e.to_string()))?; + + // Poll for permission result with timeout + // The permission dialog is shown asynchronously, so we poll until + // the user responds or we timeout (30 seconds) + let poll_interval = Duration::from_millis(250); + let timeout = Duration::from_secs(30); + let start = std::time::Instant::now(); + + loop { + thread::sleep(poll_interval); + + match check_location_permission(permission) { + Ok(true) => break, + Ok(false) if start.elapsed() >= timeout => { + return Err(Error::AccessDenied); + } + Ok(false) => continue, + Err(e) => return Err(e), + } + } + } + + Ok(Self { + power_mode: PowerMode::High, + stop_listening: Arc::new(AtomicBool::new(false)), + }) + } +} + +fn check_location_permission(permission: &'static str) -> Result { + use std::sync::mpsc::channel; + + let (tx, rx) = channel(); + + dioxus::mobile::wry::prelude::dispatch( + move |env: &mut JNIEnv, activity: &JObject, _webview| { + let permission_str = env.new_string(permission).unwrap(); + + let check_result = env + .call_method( + activity, + "checkSelfPermission", + "(Ljava/lang/String;)I", + &[JValue::Object(&permission_str.into())], + ) + .and_then(|v| v.i()); + + match check_result { + Ok(0) => tx.send(Ok(true)).unwrap(), + Ok(_) => tx.send(Ok(false)).unwrap(), + Err(e) => tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(), + } + }, + ); + + rx.recv().map_err(|e| Error::DeviceError(e.to_string()))? +} + +pub async fn get_coordinates(geolocator: &Geolocator) -> Result { + use std::sync::mpsc::channel; + + let power_mode = geolocator.power_mode; + let permission = permission_for_power_mode(power_mode); + + if !check_location_permission(permission)? { + return Err(Error::AccessDenied); + } + let (tx, rx) = channel(); + + dioxus::mobile::wry::prelude::dispatch( + move |env: &mut JNIEnv, activity: &JObject, _webview| { + // Get LocationManager + let location_service = env.new_string("location").unwrap(); + let location_manager = env + .call_method( + activity, + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + &[JValue::Object(&location_service.into())], + ) + .and_then(|v| v.l()); + + let location_manager = match location_manager { + Ok(lm) => lm, + Err(e) => { + tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + return; + } + }; + + // Determine provider based on power mode + let provider = match power_mode { + PowerMode::High => "gps", + PowerMode::Low => "network", + }; + let provider_str = env.new_string(provider).unwrap(); + + // Get last known location + let location = env + .call_method( + &location_manager, + "getLastKnownLocation", + "(Ljava/lang/String;)Landroid/location/Location;", + &[JValue::Object(&provider_str.into())], + ) + .and_then(|v| v.l()); + + match location { + Ok(loc) if !loc.is_null() => { + let latitude = env + .call_method(&loc, "getLatitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + let longitude = env + .call_method(&loc, "getLongitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + tx.send(Ok(Geocoordinates { + latitude, + longitude, + })) + .unwrap(); + } + Ok(_) => { + tx.send(Err(Error::DeviceError( + "No last known location available".to_string(), + ))) + .unwrap(); + } + Err(e) => { + tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + } + } + }, + ); + + rx.recv().map_err(|e| Error::DeviceError(e.to_string()))? +} + +/// This spawns a background thread that continuously polls for location updates. +pub fn listen( + geolocator: &Geolocator, + callback: Arc, +) -> Result<(), Error> { + use std::sync::Mutex; + use std::sync::mpsc::channel; + + let power_mode = geolocator.power_mode; + let permission = permission_for_power_mode(power_mode); + + if !check_location_permission(permission)? { + return Err(Error::AccessDenied); + } + let stop_flag = geolocator.stop_listening.clone(); + + stop_flag.store(false, Ordering::SeqCst); + + let (tx, rx) = channel(); + let callback_init = callback.clone(); + + dioxus::mobile::wry::prelude::dispatch( + move |env: &mut JNIEnv, activity: &JObject, _webview| { + // Get LocationManager + let location_service = env.new_string("location").unwrap(); + let location_manager = env + .call_method( + activity, + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + &[JValue::Object(&location_service.into())], + ) + .and_then(|v| v.l()); + + let location_manager = match location_manager { + Ok(lm) => lm, + Err(e) => { + tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + return; + } + }; + + let provider = match power_mode { + PowerMode::High => "gps", + PowerMode::Low => "network", + }; + let provider_str = env.new_string(provider).unwrap(); + + let is_enabled = env + .call_method( + &location_manager, + "isProviderEnabled", + "(Ljava/lang/String;)Z", + &[JValue::Object(&provider_str.into())], + ) + .and_then(|v| v.z()) + .unwrap_or(false); + + if is_enabled { + callback_init(Event::StatusChanged(Status::Ready)); + } else { + callback_init(Event::StatusChanged(Status::Disabled)); + } + + // Get initial location + let provider_str = env.new_string(provider).unwrap(); + let location = env + .call_method( + &location_manager, + "getLastKnownLocation", + "(Ljava/lang/String;)Landroid/location/Location;", + &[JValue::Object(&provider_str.into())], + ) + .and_then(|v| v.l()); + + let initial_coords = if let Ok(loc) = location { + if !loc.is_null() { + let latitude = env + .call_method(&loc, "getLatitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + let longitude = env + .call_method(&loc, "getLongitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + Some(Geocoordinates { + latitude, + longitude, + }) + } else { + None + } + } else { + None + }; + + if let Some(coords) = initial_coords.clone() { + callback_init(Event::NewGeocoordinates(coords)); + } + + tx.send(Ok(initial_coords)).unwrap(); + }, + ); + + let initial_coords = rx.recv().map_err(|e| Error::DeviceError(e.to_string()))??; + + // Spawn a background thread for continuous updates + let last_coords: Arc>> = Arc::new(Mutex::new(initial_coords)); + + thread::spawn(move || { + // Polling interval: 1 second for High accuracy, 5 seconds for Low + let poll_interval = match power_mode { + PowerMode::High => Duration::from_secs(1), + PowerMode::Low => Duration::from_secs(5), + }; + + loop { + if stop_flag.load(Ordering::SeqCst) { + break; + } + + thread::sleep(poll_interval); + + if stop_flag.load(Ordering::SeqCst) { + break; + } + + let (tx, rx) = channel(); + let callback_poll = callback.clone(); + let last_coords_poll = last_coords.clone(); + + let permission_poll = permission_for_power_mode(power_mode); + dioxus::mobile::wry::prelude::dispatch( + move |env: &mut JNIEnv, activity: &JObject, _webview| { + // Check if permission is still granted + let permission_str = env.new_string(permission_poll).unwrap(); + + let has_permission = env + .call_method( + activity, + "checkSelfPermission", + "(Ljava/lang/String;)I", + &[JValue::Object(&permission_str.into())], + ) + .and_then(|v| v.i()) + .map(|v| v == 0) + .unwrap_or(false); + + if !has_permission { + callback_poll(Event::StatusChanged(Status::NotAvailable)); + tx.send(false).unwrap(); + return; + } + + let location_service = env.new_string("location").unwrap(); + let location_manager = env + .call_method( + activity, + "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + &[JValue::Object(&location_service.into())], + ) + .and_then(|v| v.l()); + + let location_manager = match location_manager { + Ok(lm) => lm, + Err(_) => { + tx.send(true).unwrap(); + return; + } + }; + + let provider = match power_mode { + PowerMode::High => "gps", + PowerMode::Low => "network", + }; + let provider_str = env.new_string(provider).unwrap(); + + let location = env + .call_method( + &location_manager, + "getLastKnownLocation", + "(Ljava/lang/String;)Landroid/location/Location;", + &[JValue::Object(&provider_str.into())], + ) + .and_then(|v| v.l()); + + if let Ok(loc) = location { + if !loc.is_null() { + let latitude = env + .call_method(&loc, "getLatitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + let longitude = env + .call_method(&loc, "getLongitude", "()D", &[]) + .and_then(|v| v.d()) + .unwrap_or(0.0); + + let new_coords = Geocoordinates { + latitude, + longitude, + }; + + let mut last = last_coords_poll.lock().unwrap(); + let should_notify = match &*last { + Some(prev) => { + (prev.latitude - latitude).abs() > 0.00001 + || (prev.longitude - longitude).abs() > 0.00001 + } + None => true, + }; + + if should_notify { + *last = Some(new_coords.clone()); + callback_poll(Event::NewGeocoordinates(new_coords)); + } + } + } + + tx.send(true).unwrap(); + }, + ); + + // Wait for the dispatch to complete before next iteration + // If permission was revoked, stop the loop + if let Ok(false) = rx.recv() { + break; + } + } + }); + + Ok(()) +} + +pub fn set_power_mode(geolocator: &mut Geolocator, power_mode: PowerMode) -> Result<(), Error> { + geolocator.power_mode = power_mode; + Ok(()) +} diff --git a/packages/geolocation/src/platform/mod.rs b/packages/geolocation/src/platform/mod.rs index 59a88ef..964c5c3 100644 --- a/packages/geolocation/src/platform/mod.rs +++ b/packages/geolocation/src/platform/mod.rs @@ -8,7 +8,12 @@ mod wasm; #[cfg(target_family = "wasm")] pub use self::wasm::*; -#[cfg(not(any(target_family = "wasm", windows)))] +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "android")] +pub use self::android::*; + +#[cfg(not(any(target_family = "wasm", windows, target_os = "android")))] mod unsupported { use std::sync::Arc; @@ -42,5 +47,5 @@ mod unsupported { } } -#[cfg(not(any(target_family = "wasm", windows)))] +#[cfg(not(any(target_family = "wasm", windows, target_os = "android")))] pub use self::unsupported::*; From 1d50fa0d715a318c8e75f6a95556089918a678bd Mon Sep 17 00:00:00 2001 From: Nils Loewen Date: Wed, 31 Dec 2025 23:03:46 -0800 Subject: [PATCH 2/2] Improve error handling --- packages/geolocation/README.md | 28 +++- packages/geolocation/src/platform/android.rs | 139 ++++++++++++++----- 2 files changed, 132 insertions(+), 35 deletions(-) diff --git a/packages/geolocation/README.md b/packages/geolocation/README.md index e2dd4a7..0149056 100644 --- a/packages/geolocation/README.md +++ b/packages/geolocation/README.md @@ -4,10 +4,10 @@ Geolocation utilities and hooks for Dioxus. ### Supports - [x] Web - [x] Windows +- [x] Android (draft) - [ ] Mac - [ ] Linux -- [ ] Android -- [ ] iOs +- [ ] iOS ## Usage Add `dioxus-sdk-geolocation` to your `Cargo.toml`: @@ -41,3 +41,27 @@ fn App() -> Element { } } ``` + +## Platform Notes + +### Android + +> **Note:** Android support requires `dioxus = "0.7.0-alpha0"` or later. + +The Android implementation provides robust geolocation support via JNI: + +**Features:** +- **Automatic permission handling** - Requests `ACCESS_FINE_LOCATION` (GPS) or `ACCESS_COARSE_LOCATION` (network) based on `PowerMode` +- **Power mode support** - `PowerMode::High` uses GPS provider, `PowerMode::Low` uses network provider +- **Continuous location updates** - Background polling with configurable intervals (1s for High, 5s for Low accuracy) +- **Change detection** - Only emits `NewGeocoordinates` events when position changes (0.00001 degree threshold) +- **Permission monitoring** - Detects permission revocation during active listening +- **Automatic cleanup** - Listener thread stops when `Geolocator` is dropped + +**Required Permissions:** + +Add to your `AndroidManifest.xml`: +```xml + + +``` diff --git a/packages/geolocation/src/platform/android.rs b/packages/geolocation/src/platform/android.rs index 74050ea..1dc3945 100644 --- a/packages/geolocation/src/platform/android.rs +++ b/packages/geolocation/src/platform/android.rs @@ -38,10 +38,21 @@ impl Geolocator { dioxus::mobile::wry::prelude::dispatch( move |env: &mut JNIEnv, activity: &JObject, _webview| { - let permission_str = env.new_string(permission).unwrap(); - let permissions = env - .new_object_array(1, "java/lang/String", &permission_str) - .unwrap(); + let permission_str = match env.new_string(permission) { + Ok(s) => s, + Err(_) => { + let _ = tx.send(false); + return; + } + }; + let permissions = + match env.new_object_array(1, "java/lang/String", &permission_str) { + Ok(arr) => arr, + Err(_) => { + let _ = tx.send(false); + return; + } + }; let result = env.call_method( activity, @@ -50,7 +61,7 @@ impl Geolocator { &[JValue::Object(&permissions.into()), JValue::Int(1)], ); - tx.send(result.is_ok()).unwrap(); + let _ = tx.send(result.is_ok()); }, ); @@ -84,6 +95,12 @@ impl Geolocator { } } +impl Drop for Geolocator { + fn drop(&mut self) { + self.stop_listening.store(true, Ordering::SeqCst); + } +} + fn check_location_permission(permission: &'static str) -> Result { use std::sync::mpsc::channel; @@ -91,7 +108,13 @@ fn check_location_permission(permission: &'static str) -> Result { dioxus::mobile::wry::prelude::dispatch( move |env: &mut JNIEnv, activity: &JObject, _webview| { - let permission_str = env.new_string(permission).unwrap(); + let permission_str = match env.new_string(permission) { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; let check_result = env .call_method( @@ -102,11 +125,12 @@ fn check_location_permission(permission: &'static str) -> Result { ) .and_then(|v| v.i()); - match check_result { - Ok(0) => tx.send(Ok(true)).unwrap(), - Ok(_) => tx.send(Ok(false)).unwrap(), - Err(e) => tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(), - } + let result = match check_result { + Ok(0) => Ok(true), + Ok(_) => Ok(false), + Err(e) => Err(Error::DeviceError(e.to_string())), + }; + let _ = tx.send(result); }, ); @@ -127,7 +151,13 @@ pub async fn get_coordinates(geolocator: &Geolocator) -> Result s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; let location_manager = env .call_method( activity, @@ -140,7 +170,7 @@ pub async fn get_coordinates(geolocator: &Geolocator) -> Result lm, Err(e) => { - tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); return; } }; @@ -150,7 +180,13 @@ pub async fn get_coordinates(geolocator: &Geolocator) -> Result "gps", PowerMode::Low => "network", }; - let provider_str = env.new_string(provider).unwrap(); + let provider_str = match env.new_string(provider) { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; // Get last known location let location = env @@ -174,20 +210,18 @@ pub async fn get_coordinates(geolocator: &Geolocator) -> Result { - tx.send(Err(Error::DeviceError( + let _ = tx.send(Err(Error::DeviceError( "No last known location available".to_string(), - ))) - .unwrap(); + ))); } Err(e) => { - tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); } } }, @@ -220,7 +254,13 @@ pub fn listen( dioxus::mobile::wry::prelude::dispatch( move |env: &mut JNIEnv, activity: &JObject, _webview| { // Get LocationManager - let location_service = env.new_string("location").unwrap(); + let location_service = match env.new_string("location") { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; let location_manager = env .call_method( activity, @@ -233,7 +273,7 @@ pub fn listen( let location_manager = match location_manager { Ok(lm) => lm, Err(e) => { - tx.send(Err(Error::DeviceError(e.to_string()))).unwrap(); + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); return; } }; @@ -242,7 +282,13 @@ pub fn listen( PowerMode::High => "gps", PowerMode::Low => "network", }; - let provider_str = env.new_string(provider).unwrap(); + let provider_str = match env.new_string(provider) { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; let is_enabled = env .call_method( @@ -261,7 +307,13 @@ pub fn listen( } // Get initial location - let provider_str = env.new_string(provider).unwrap(); + let provider_str = match env.new_string(provider) { + Ok(s) => s, + Err(e) => { + let _ = tx.send(Err(Error::DeviceError(e.to_string()))); + return; + } + }; let location = env .call_method( &location_manager, @@ -298,7 +350,7 @@ pub fn listen( callback_init(Event::NewGeocoordinates(coords)); } - tx.send(Ok(initial_coords)).unwrap(); + let _ = tx.send(Ok(initial_coords)); }, ); @@ -333,7 +385,13 @@ pub fn listen( dioxus::mobile::wry::prelude::dispatch( move |env: &mut JNIEnv, activity: &JObject, _webview| { // Check if permission is still granted - let permission_str = env.new_string(permission_poll).unwrap(); + let permission_str = match env.new_string(permission_poll) { + Ok(s) => s, + Err(_) => { + let _ = tx.send(true); + return; + } + }; let has_permission = env .call_method( @@ -348,11 +406,17 @@ pub fn listen( if !has_permission { callback_poll(Event::StatusChanged(Status::NotAvailable)); - tx.send(false).unwrap(); + let _ = tx.send(false); return; } - let location_service = env.new_string("location").unwrap(); + let location_service = match env.new_string("location") { + Ok(s) => s, + Err(_) => { + let _ = tx.send(true); + return; + } + }; let location_manager = env .call_method( activity, @@ -365,7 +429,7 @@ pub fn listen( let location_manager = match location_manager { Ok(lm) => lm, Err(_) => { - tx.send(true).unwrap(); + let _ = tx.send(true); return; } }; @@ -374,7 +438,13 @@ pub fn listen( PowerMode::High => "gps", PowerMode::Low => "network", }; - let provider_str = env.new_string(provider).unwrap(); + let provider_str = match env.new_string(provider) { + Ok(s) => s, + Err(_) => { + let _ = tx.send(true); + return; + } + }; let location = env .call_method( @@ -402,7 +472,10 @@ pub fn listen( longitude, }; - let mut last = last_coords_poll.lock().unwrap(); + let Ok(mut last) = last_coords_poll.lock() else { + let _ = tx.send(true); + return; + }; let should_notify = match &*last { Some(prev) => { (prev.latitude - latitude).abs() > 0.00001 @@ -418,7 +491,7 @@ pub fn listen( } } - tx.send(true).unwrap(); + let _ = tx.send(true); }, );