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/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/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..1dc3945
--- /dev/null
+++ b/packages/geolocation/src/platform/android.rs
@@ -0,0 +1,512 @@
+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 = 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,
+ "requestPermissions",
+ "([Ljava/lang/String;I)V",
+ &[JValue::Object(&permissions.into()), JValue::Int(1)],
+ );
+
+ let _ = tx.send(result.is_ok());
+ },
+ );
+
+ 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)),
+ })
+ }
+}
+
+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;
+
+ let (tx, rx) = channel();
+
+ dioxus::mobile::wry::prelude::dispatch(
+ move |env: &mut JNIEnv, activity: &JObject, _webview| {
+ 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(
+ activity,
+ "checkSelfPermission",
+ "(Ljava/lang/String;)I",
+ &[JValue::Object(&permission_str.into())],
+ )
+ .and_then(|v| v.i());
+
+ let result = match check_result {
+ Ok(0) => Ok(true),
+ Ok(_) => Ok(false),
+ Err(e) => Err(Error::DeviceError(e.to_string())),
+ };
+ let _ = tx.send(result);
+ },
+ );
+
+ 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 = 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,
+ "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) => {
+ let _ = tx.send(Err(Error::DeviceError(e.to_string())));
+ return;
+ }
+ };
+
+ // Determine provider based on power mode
+ let provider = match power_mode {
+ PowerMode::High => "gps",
+ PowerMode::Low => "network",
+ };
+ 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
+ .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);
+
+ let _ = tx.send(Ok(Geocoordinates {
+ latitude,
+ longitude,
+ }));
+ }
+ Ok(_) => {
+ let _ = tx.send(Err(Error::DeviceError(
+ "No last known location available".to_string(),
+ )));
+ }
+ Err(e) => {
+ let _ = tx.send(Err(Error::DeviceError(e.to_string())));
+ }
+ }
+ },
+ );
+
+ 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 = 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,
+ "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) => {
+ let _ = tx.send(Err(Error::DeviceError(e.to_string())));
+ return;
+ }
+ };
+
+ let provider = match power_mode {
+ PowerMode::High => "gps",
+ PowerMode::Low => "network",
+ };
+ 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(
+ &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 = 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,
+ "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));
+ }
+
+ let _ = tx.send(Ok(initial_coords));
+ },
+ );
+
+ 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 = match env.new_string(permission_poll) {
+ Ok(s) => s,
+ Err(_) => {
+ let _ = tx.send(true);
+ return;
+ }
+ };
+
+ 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));
+ let _ = tx.send(false);
+ return;
+ }
+
+ let location_service = match env.new_string("location") {
+ Ok(s) => s,
+ Err(_) => {
+ let _ = tx.send(true);
+ return;
+ }
+ };
+ 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(_) => {
+ let _ = tx.send(true);
+ return;
+ }
+ };
+
+ let provider = match power_mode {
+ PowerMode::High => "gps",
+ PowerMode::Low => "network",
+ };
+ let provider_str = match env.new_string(provider) {
+ Ok(s) => s,
+ Err(_) => {
+ let _ = tx.send(true);
+ return;
+ }
+ };
+
+ 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 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
+ || (prev.longitude - longitude).abs() > 0.00001
+ }
+ None => true,
+ };
+
+ if should_notify {
+ *last = Some(new_coords.clone());
+ callback_poll(Event::NewGeocoordinates(new_coords));
+ }
+ }
+ }
+
+ let _ = tx.send(true);
+ },
+ );
+
+ // 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::*;