Skip to main content

appscale_core/
platform_android.rs

1//! Android Mobile Platform Bridge
2//!
3//! Implements PlatformBridge for Android using Android Views concepts.
4//! At this stage, this is a scaffold that records operations and will
5//! later connect to the actual JNI bridge for Android View creation.
6//!
7//! Architecture:
8//!   Rust PlatformBridge → JNI → Kotlin/Java Android Views wrapper
9//!
10//! The Android bridge handles ViewGroup hierarchy, Paint/Canvas text
11//! measurement, and Android-specific capabilities like Haptics,
12//! Biometrics, and BackgroundFetch.
13
14use crate::platform::*;
15use crate::tree::NodeId;
16use std::sync::Mutex;
17use std::collections::HashMap;
18
19/// Android platform bridge.
20///
21/// In production, each method will call through JNI to the Kotlin/Java layer.
22/// For now, we maintain an in-memory view registry for testing and development.
23pub struct AndroidPlatform {
24    next_handle: Mutex<u64>,
25    views: Mutex<HashMap<u64, ViewRecord>>,
26    screen: ScreenSize,
27    scale: f32,
28}
29
30struct ViewRecord {
31    view_type: ViewType,
32    node_id: NodeId,
33    children: Vec<NativeHandle>,
34}
35
36impl AndroidPlatform {
37    pub fn new() -> Self {
38        Self {
39            next_handle: Mutex::new(1),
40            views: Mutex::new(HashMap::new()),
41            // Default: Pixel 6 logical resolution (dp)
42            screen: ScreenSize { width: 412.0, height: 732.0 },
43            scale: 2.625, // xxhdpi
44        }
45    }
46
47    pub fn with_screen(mut self, width: f32, height: f32, scale: f32) -> Self {
48        self.screen = ScreenSize { width, height };
49        self.scale = scale;
50        self
51    }
52
53    pub fn view_count(&self) -> usize {
54        self.views.lock().unwrap().len()
55    }
56}
57
58impl Default for AndroidPlatform {
59    fn default() -> Self { Self::new() }
60}
61
62impl PlatformBridge for AndroidPlatform {
63    fn platform_id(&self) -> PlatformId {
64        PlatformId::Android
65    }
66
67    fn create_view(&self, view_type: ViewType, node_id: NodeId) -> NativeHandle {
68        let mut h = self.next_handle.lock().unwrap();
69        let handle = NativeHandle(*h);
70        *h += 1;
71
72        self.views.lock().unwrap().insert(handle.0, ViewRecord {
73            view_type,
74            node_id,
75            children: Vec::new(),
76        });
77
78        handle
79    }
80
81    fn update_view(&self, handle: NativeHandle, _props: &PropsDiff) -> Result<(), PlatformError> {
82        let views = self.views.lock().unwrap();
83        if !views.contains_key(&handle.0) {
84            return Err(PlatformError::ViewNotFound(handle));
85        }
86        // In production: forward props to Android View via JNI
87        Ok(())
88    }
89
90    fn remove_view(&self, handle: NativeHandle) {
91        self.views.lock().unwrap().remove(&handle.0);
92    }
93
94    fn insert_child(&self, parent: NativeHandle, child: NativeHandle, index: usize) {
95        let mut views = self.views.lock().unwrap();
96        if let Some(parent_record) = views.get_mut(&parent.0) {
97            let idx = index.min(parent_record.children.len());
98            parent_record.children.insert(idx, child);
99        }
100    }
101
102    fn remove_child(&self, parent: NativeHandle, child: NativeHandle) {
103        let mut views = self.views.lock().unwrap();
104        if let Some(parent_record) = views.get_mut(&parent.0) {
105            parent_record.children.retain(|c| *c != child);
106        }
107    }
108
109    fn measure_text(&self, text: &str, style: &TextStyle, max_width: f32) -> TextMetrics {
110        // Approximate measurement until connected to Android Paint.measureText via JNI.
111        // Uses Roboto metrics as baseline (Android system font).
112        let font_size = style.font_size.unwrap_or(14.0); // Android default (14sp)
113        let char_width = font_size * 0.54; // Roboto average
114        let line_height = style.line_height.unwrap_or(font_size * 1.25);
115        let total_width = char_width * text.len() as f32;
116
117        let lines = if max_width > 0.0 && total_width > max_width {
118            (total_width / max_width).ceil() as u32
119        } else {
120            1
121        };
122
123        TextMetrics {
124            width: if lines > 1 { max_width } else { total_width },
125            height: line_height * lines as f32,
126            baseline: font_size * 0.78,
127            line_count: lines,
128        }
129    }
130
131    fn screen_size(&self) -> ScreenSize {
132        self.screen
133    }
134
135    fn scale_factor(&self) -> f32 {
136        self.scale
137    }
138
139    fn supports(&self, capability: PlatformCapability) -> bool {
140        matches!(capability,
141            PlatformCapability::Haptics
142            | PlatformCapability::Biometrics
143            | PlatformCapability::PushNotifications
144            | PlatformCapability::BackgroundFetch
145            | PlatformCapability::NativeFilePicker
146            | PlatformCapability::NativeDatePicker
147            | PlatformCapability::ContextMenu
148            | PlatformCapability::NativeShare
149        )
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_android_create_and_remove() {
159        let platform = AndroidPlatform::new();
160        assert_eq!(platform.platform_id(), PlatformId::Android);
161
162        let h1 = platform.create_view(ViewType::Container, NodeId(1));
163        let h2 = platform.create_view(ViewType::Button, NodeId(2));
164        assert_eq!(platform.view_count(), 2);
165
166        platform.insert_child(h1, h2, 0);
167        platform.remove_child(h1, h2);
168        platform.remove_view(h2);
169        assert_eq!(platform.view_count(), 1);
170    }
171
172    #[test]
173    fn test_android_capabilities() {
174        let platform = AndroidPlatform::new();
175        assert!(platform.supports(PlatformCapability::Haptics));
176        assert!(platform.supports(PlatformCapability::Biometrics));
177        assert!(platform.supports(PlatformCapability::PushNotifications));
178        assert!(platform.supports(PlatformCapability::BackgroundFetch));
179        assert!(platform.supports(PlatformCapability::NativeShare));
180        assert!(!platform.supports(PlatformCapability::MenuBar));
181        assert!(!platform.supports(PlatformCapability::SystemTray));
182        assert!(!platform.supports(PlatformCapability::MultiWindow));
183    }
184
185    #[test]
186    fn test_android_screen_info() {
187        let platform = AndroidPlatform::new();
188        let screen = platform.screen_size();
189        assert_eq!(screen.width, 412.0);
190        assert_eq!(screen.height, 732.0);
191        assert_eq!(platform.scale_factor(), 2.625);
192    }
193
194    #[test]
195    fn test_android_custom_screen() {
196        // Samsung Galaxy Tab S9 Ultra
197        let platform = AndroidPlatform::new().with_screen(900.0, 1422.0, 1.5);
198        let screen = platform.screen_size();
199        assert_eq!(screen.width, 900.0);
200        assert_eq!(screen.height, 1422.0);
201        assert_eq!(platform.scale_factor(), 1.5);
202    }
203
204    #[test]
205    fn test_android_text_measurement() {
206        let platform = AndroidPlatform::new();
207        let style = TextStyle { font_size: Some(16.0), ..TextStyle::default() };
208        let metrics = platform.measure_text("Hello Android", &style, 200.0);
209        assert!(metrics.width > 0.0);
210        assert!(metrics.height > 0.0);
211        assert_eq!(metrics.line_count, 1);
212    }
213
214    #[test]
215    fn test_android_text_wrapping() {
216        let platform = AndroidPlatform::new();
217        let style = TextStyle { font_size: Some(16.0), ..TextStyle::default() };
218        let metrics = platform.measure_text("This is a longer text that should wrap", &style, 50.0);
219        assert!(metrics.line_count > 1);
220        assert_eq!(metrics.width, 50.0);
221    }
222
223    #[test]
224    fn test_android_update_missing_view() {
225        let platform = AndroidPlatform::new();
226        let result = platform.update_view(NativeHandle(999), &PropsDiff::new());
227        assert!(result.is_err());
228    }
229}