Skip to content

Commit c6fbeaf

Browse files
committed
wip: get storage directory with JNI
1 parent b8dc259 commit c6fbeaf

8 files changed

Lines changed: 164 additions & 43 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sandpolis-mobile/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ sandpolis = { path = "../sandpolis", version = "8.0.0", default-features = false
2626
"layer-shell",
2727
] }
2828
sandpolis-database = { path = "../sandpolis-database", version = "0.0.1" }
29+
30+
# Android-specific dependencies
31+
jni = "0.21"
32+
ndk-context = "0.1"

sandpolis-mobile/android/app/build.gradle

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,19 @@ android {
1212
targetSdk 33
1313
versionCode 1
1414
versionName "1.0"
15-
// need this otherwise it won't insert libc++_shared.so
16-
externalNativeBuild {
17-
cmake {
18-
arguments "-DANDROID_STL=c++_shared"
19-
}
20-
}
15+
// Rust libraries are built with cargo-ndk, not CMake
2116
// set up targets
2217
ndk {
2318
abiFilters 'arm64-v8a'
2419
}
2520
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2621
}
27-
externalNativeBuild {
28-
cmake {
29-
path "CMakeLists.txt"
30-
}
31-
}
22+
// CMake is not needed since we build with cargo-ndk
23+
// externalNativeBuild {
24+
// cmake {
25+
// path "CMakeLists.txt"
26+
// }
27+
// }
3228
buildTypes {
3329
release {
3430
minifyEnabled false

sandpolis-mobile/android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
<activity
1313
android:name=".MainActivity"
1414
android:exported="true"
15-
android:theme="@style/Theme.AppCompat.NoActionBar">
15+
android:theme="@style/Theme.AppCompat.NoActionBar"
16+
android:configChanges="orientation|screenSize|screenLayout|uiMode">
1617
<meta-data
1718
android:name="android.app.lib_name"
1819
android:value="sandpolis_mobile" />

sandpolis-mobile/shell.nix

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ EOF
7070
echo ""
7171
echo "To build APK:"
7272
echo " # Debug build:"
73-
echo " cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build"
73+
echo " cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --link-libcxx-shared"
7474
echo " cd android && ./gradlew assembleDebug"
7575
echo ""
7676
echo " # Release build:"
77-
echo " cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --release"
77+
echo " cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --release --link-libcxx-shared"
7878
echo " cd android && ./gradlew assembleRelease"
7979
echo ""
8080
echo "APK output: android/app/build/outputs/apk/"
@@ -85,8 +85,8 @@ EOF
8585
# nix-shell Enter development environment
8686
#
8787
# Build APK:
88-
# Debug: cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build && cd android && ./gradlew assembleDebug
89-
# Release: cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --release && cd android && ./gradlew assembleRelease
88+
# Debug: cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --link-libcxx-shared && cd android && ./gradlew assembleDebug
89+
# Release: cargo ndk -t arm64-v8a -o android/app/src/main/jniLibs build --release --link-libcxx-shared && cd android && ./gradlew assembleRelease
9090
#
9191
# To add debugging tools, include in buildInputs:
9292
# - platform-tools (for adb, fastboot)

sandpolis-mobile/src/lib.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,66 @@
11
use bevy::prelude::bevy_main;
22
use sandpolis::{InstanceState, config::Configuration};
33
use sandpolis_database::DatabaseLayer;
4+
use std::path::PathBuf;
5+
6+
/// Get Android app's files directory using JNI
7+
fn get_android_files_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
8+
use jni::JavaVM;
9+
use ndk_context::android_context;
10+
11+
let ctx = android_context();
12+
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }?;
13+
let mut env = vm.attach_current_thread()?;
14+
15+
// Get the Context object
16+
let context = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
17+
18+
// Call getFilesDir() on the context
19+
let files_dir = env.call_method(
20+
context,
21+
"getFilesDir",
22+
"()Ljava/io/File;",
23+
&[]
24+
)?;
25+
26+
// Call getAbsolutePath() on the File object
27+
let path = env.call_method(
28+
files_dir.l()?,
29+
"getAbsolutePath",
30+
"()Ljava/lang/String;",
31+
&[]
32+
)?;
33+
34+
// Convert Java String to Rust String
35+
let path_string: String = env.get_string(&path.l()?.into())?.into();
36+
37+
Ok(PathBuf::from(path_string))
38+
}
439

540
#[bevy_main]
641
pub fn main() {
7-
// Get ready to do some cryptography
8-
rustls::crypto::aws_lc_rs::default_provider()
9-
.install_default()
10-
.expect("crypto provider is available");
42+
// Get ready to do some cryptography (ignore error if already installed)
43+
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
1144

1245
tokio::runtime::Builder::new_multi_thread()
1346
.enable_all()
1447
.build()
1548
.unwrap()
1649
.block_on(async {
17-
// Set initial configuration
18-
let config = Configuration::default();
50+
// Set initial configuration with Android app data directory
51+
let mut config = Configuration::default();
52+
53+
// Use Android's app-specific data directory for database storage
54+
match get_android_files_dir() {
55+
Ok(files_dir) => {
56+
config.database.storage = Some(files_dir);
57+
}
58+
Err(e) => {
59+
eprintln!("Failed to get Android files directory: {}", e);
60+
// Fallback to ephemeral database
61+
config.database.ephemeral = true;
62+
}
63+
}
1964

2065
// Load state
2166
let state = InstanceState::new(

sandpolis/src/client/gui/input.rs

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,38 +65,104 @@ impl Default for LoginDialogState {
6565
}
6666
}
6767

68-
// #[cfg(target_os = "android")]
69-
pub fn touch_camera(
68+
/// Handle touch input for panning on mobile devices
69+
#[cfg(target_os = "android")]
70+
pub fn handle_touch_camera(
71+
mut contexts: EguiContexts,
7072
windows: Query<&Window>,
7173
mut touches: EventReader<TouchInput>,
72-
mut camera: Query<&mut Transform, With<Camera3d>>,
74+
mut camera: Query<&mut Transform, With<Camera2d>>,
7375
mut last_position: Local<Option<Vec2>>,
74-
mut rotations: EventReader<RotationGesture>,
7576
) {
76-
let window = windows.single().unwrap();
77+
// Don't handle touch if egui wants the input
78+
let Ok(ctx) = contexts.ctx_mut() else {
79+
return;
80+
};
81+
if ctx.wants_pointer_input() || ctx.is_pointer_over_area() {
82+
touches.clear();
83+
*last_position = None;
84+
return;
85+
}
86+
87+
let Ok(window) = windows.single() else {
88+
return;
89+
};
7790

7891
for touch in touches.read() {
7992
if touch.phase == TouchPhase::Started {
8093
*last_position = None;
8194
}
82-
if let Some(last_position) = *last_position {
83-
let mut transform = camera.single_mut().unwrap();
84-
*transform = Transform::from_xyz(
85-
transform.translation.x
86-
+ (touch.position.x - last_position.x) / window.width() * 5.0,
87-
transform.translation.y,
88-
transform.translation.z
89-
+ (touch.position.y - last_position.y) / window.height() * 5.0,
90-
)
91-
.looking_at(Vec3::ZERO, Vec3::Y);
95+
if let Some(last_pos) = *last_position {
96+
if let Ok(mut transform) = camera.single_mut() {
97+
// Calculate displacement in screen space
98+
let displacement = touch.position - last_pos;
99+
// Apply panning (negative Y because screen coords are flipped)
100+
transform.translation -= Vec3::new(displacement.x, -displacement.y, 0.0);
101+
}
92102
}
93103
*last_position = Some(touch.position);
94104
}
95-
// Rotation gestures only work on iOS
96-
for rotation in rotations.read() {
97-
let mut transform = camera.single_mut().unwrap();
98-
let forward = transform.forward();
99-
transform.rotate_axis(forward, rotation.0 / 10.0);
105+
}
106+
107+
/// Handle pinch-to-zoom gestures on mobile devices using two-finger touch
108+
#[cfg(target_os = "android")]
109+
pub fn handle_touch_zoom(
110+
mut contexts: EguiContexts,
111+
mut touches: EventReader<TouchInput>,
112+
mut zoom_level: ResMut<ZoomLevel>,
113+
mut camera_query: Query<&mut Projection, With<Camera2d>>,
114+
mut touch_positions: Local<std::collections::HashMap<u64, Vec2>>,
115+
mut last_distance: Local<Option<f32>>,
116+
) {
117+
// Don't handle zoom if egui wants the input
118+
let Ok(ctx) = contexts.ctx_mut() else {
119+
return;
120+
};
121+
if ctx.wants_pointer_input() || ctx.is_pointer_over_area() {
122+
touches.clear();
123+
touch_positions.clear();
124+
*last_distance = None;
125+
return;
126+
}
127+
128+
// Update touch positions
129+
for touch in touches.read() {
130+
match touch.phase {
131+
TouchPhase::Started | TouchPhase::Moved => {
132+
touch_positions.insert(touch.id, touch.position);
133+
}
134+
TouchPhase::Ended | TouchPhase::Canceled => {
135+
touch_positions.remove(&touch.id);
136+
if touch_positions.len() < 2 {
137+
*last_distance = None;
138+
}
139+
}
140+
}
141+
}
142+
143+
// Only process zoom if we have exactly 2 touches
144+
if touch_positions.len() == 2 {
145+
let positions: Vec<Vec2> = touch_positions.values().copied().collect();
146+
let current_distance = positions[0].distance(positions[1]);
147+
148+
if let Some(prev_distance) = *last_distance {
149+
// Calculate zoom factor based on distance change
150+
let distance_ratio = current_distance / prev_distance;
151+
let zoom_factor = 1.0 / distance_ratio;
152+
let new_zoom = (zoom_level.0 * zoom_factor).clamp(CAMERA_ZOOM_RANGE.start, CAMERA_ZOOM_RANGE.end);
153+
154+
if let Ok(mut projection) = camera_query.single_mut() {
155+
if let Projection::Orthographic(ortho) = projection.as_mut() {
156+
ortho.scale = new_zoom;
157+
}
158+
}
159+
160+
**zoom_level = new_zoom;
161+
}
162+
163+
*last_distance = Some(current_distance);
164+
} else {
165+
*last_distance = None;
100166
}
101167
}
102168

sandpolis/src/client/gui/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,16 @@ pub async fn main(config: Configuration, state: InstanceState) -> Result<()> {
125125
(
126126
// Theme system (runs first to ensure theme is applied)
127127
theme::apply_theme_to_egui,
128-
// Input handling
128+
// Input handling (desktop)
129+
#[cfg(not(target_os = "android"))]
129130
self::input::handle_zoom,
131+
#[cfg(not(target_os = "android"))]
130132
self::input::handle_camera,
133+
// Input handling (mobile)
134+
#[cfg(target_os = "android")]
135+
self::input::handle_touch_camera,
136+
#[cfg(target_os = "android")]
137+
self::input::handle_touch_zoom,
131138
layer_switcher::handle_layer_switcher_toggle,
132139
node_picker::handle_node_picker_toggle,
133140
button_handler,

0 commit comments

Comments
 (0)