1use serde::{Deserialize, Serialize};
2
3use super::bootstrap::{ensure_plugins_installed, invoke_plugin};
4use super::types::{ClickResult, CommandOptions, Error, FillResult, Result};
5
6#[derive(Debug, Clone, Default)]
7pub struct MobileAndroidConnectOptions {
8 pub device: Option<String>,
9 pub adb_endpoint: Option<String>,
10 pub preserve_app_state: bool,
11 pub timeout_ms: Option<u32>,
12}
13
14#[derive(Debug, Clone, Default)]
15pub struct MobileAndroidLaunchOptions {
16 pub apk_path: Option<String>,
17 pub app_id: Option<String>,
18 pub launch_activity: Option<String>,
19 pub stop_before_launch: bool,
20 pub timeout_ms: Option<u32>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24struct MobilePluginEnvelope<T> {
25 ok: bool,
26 result: Option<T>,
27 error: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31struct MobileBrowserSessionHandle {
32 platform: String,
33 automation: MobileAutomationSessionInfo,
34 device: MobileDeviceTarget,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38struct MobileAutomationSessionInfo {
39 session_id: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43struct MobileDeviceTarget {
44 device_id: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48struct MobilePageSessionHandle {
49 page_id: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53struct MobilePageInfo {
54 page_session: MobilePageSessionHandle,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58struct MobileConnectInfo {
59 browser_session: MobileBrowserSessionHandle,
60 initial_page: MobilePageInfo,
61}
62
63#[derive(Debug, Clone)]
64pub struct AndroidPage {
65 browser_session: MobileBrowserSessionHandle,
66 page_session: MobilePageSessionHandle,
67}
68
69#[derive(Debug, Clone)]
70pub struct AndroidDevice {
71 connect_info: MobileConnectInfo,
72 page: AndroidPage,
73}
74
75pub mod android {
76 use super::*;
77
78 pub fn connect(options: MobileAndroidConnectOptions) -> Result<AndroidDevice> {
79 ensure_plugins_installed(&["mobile-android"])?;
80 let request = serde_json::json!({
81 "command": "connect",
82 "platform": "android",
83 "device": options.device,
84 "adb_endpoint": options.adb_endpoint,
85 "preserve_app_state": options.preserve_app_state,
86 "timeout_ms": options.timeout_ms,
87 });
88 let connect_info: MobileConnectInfo = invoke_android("connect", request)?;
89 Ok(AndroidDevice::new(connect_info))
90 }
91}
92
93impl AndroidDevice {
94 fn new(connect_info: MobileConnectInfo) -> Self {
95 let page = AndroidPage {
96 browser_session: connect_info.browser_session.clone(),
97 page_session: connect_info.initial_page.page_session.clone(),
98 };
99 Self { connect_info, page }
100 }
101
102 pub fn session_id(&self) -> &str {
103 &self.connect_info.browser_session.automation.session_id
104 }
105
106 pub fn page(&self) -> AndroidPage {
107 self.page.clone()
108 }
109
110 pub fn initial_page(&self) -> AndroidPage {
111 self.page()
112 }
113
114 pub fn launch(&mut self, options: MobileAndroidLaunchOptions) -> Result<AndroidPage> {
115 let request = serde_json::json!({
116 "command": "launch_app",
117 "browser_session": self.connect_info.browser_session,
118 "options": {
119 "apk_path": options.apk_path,
120 "app_id": options.app_id,
121 "launch_activity": options.launch_activity,
122 "stop_before_launch": options.stop_before_launch,
123 "timeout_ms": options.timeout_ms,
124 },
125 });
126 let page_info: MobilePageInfo = invoke_android("launch", request)?;
127 self.page = AndroidPage {
128 browser_session: self.connect_info.browser_session.clone(),
129 page_session: page_info.page_session,
130 };
131 Ok(self.page.clone())
132 }
133}
134
135impl AndroidPage {
136 pub fn session_id(&self) -> &str {
137 &self.page_session.page_id
138 }
139
140 pub fn click(&self, selector: &str, options: CommandOptions) -> Result<ClickResult> {
141 #[derive(Deserialize)]
142 struct ClickInfo {
143 selector: String,
144 note: String,
145 session_id: String,
146 }
147 let result: ClickInfo = invoke_android(
148 "click",
149 serde_json::json!({
150 "command": "click_element",
151 "browser_session": self.browser_session,
152 "page_session": self.page_session,
153 "selector": selector,
154 "timeout_ms": options.timeout_ms,
155 }),
156 )?;
157 Ok(ClickResult {
158 selector: result.selector,
159 note: result.note,
160 bidi_session_id: result.session_id,
161 })
162 }
163
164 pub fn fill(&self, selector: &str, value: &str, options: CommandOptions) -> Result<FillResult> {
165 #[derive(Deserialize)]
166 struct FillInfo {
167 selector: String,
168 value: String,
169 note: String,
170 }
171 let result: FillInfo = invoke_android(
172 "fill",
173 serde_json::json!({
174 "command": "fill_element",
175 "browser_session": self.browser_session,
176 "page_session": self.page_session,
177 "selector": selector,
178 "value": value,
179 "timeout_ms": options.timeout_ms,
180 }),
181 )?;
182 Ok(FillResult {
183 selector: result.selector,
184 value: result.value,
185 note: result.note,
186 })
187 }
188}
189
190fn invoke_android<T>(command_name: &str, request: serde_json::Value) -> Result<T>
191where
192 T: for<'de> Deserialize<'de>,
193{
194 let payload = invoke_plugin("mobile-android", &request.to_string())?;
195 let envelope: MobilePluginEnvelope<T> =
196 serde_json::from_str(payload.trim()).map_err(|error| {
197 Error::new(format!(
198 "failed to decode mobile-android plugin response for {command_name}: {error}"
199 ))
200 })?;
201 if !envelope.ok {
202 return Err(Error::new(envelope.error.unwrap_or_else(|| {
203 format!("mobile-android plugin {command_name} failed")
204 })));
205 }
206 envelope.result.ok_or_else(|| {
207 Error::new(format!(
208 "mobile-android plugin {command_name} returned success without a result payload"
209 ))
210 })
211}