1use async_trait::async_trait;
8use std::path::Path;
9use tokio::sync::Mutex;
10
11use smix_driver::Platform;
12use smix_simctl::{DeviceControlError, RecordingHandle, SimctlClient};
13
14use crate::PermissionAction;
15use crate::device_control::{DeviceControl, Permission};
16
17pub struct IosDeviceControl {
21 client: SimctlClient,
22 recording: Mutex<Option<RecordingHandle>>,
23}
24
25impl Default for IosDeviceControl {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl IosDeviceControl {
32 #[must_use]
33 pub fn new() -> Self {
34 IosDeviceControl {
35 client: SimctlClient::new(),
36 recording: Mutex::new(None),
37 }
38 }
39
40 #[must_use]
43 pub fn with_client(client: SimctlClient) -> Self {
44 IosDeviceControl {
45 client,
46 recording: Mutex::new(None),
47 }
48 }
49
50 #[must_use]
53 pub fn simctl(&self) -> &SimctlClient {
54 &self.client
55 }
56}
57
58#[async_trait]
59impl DeviceControl for IosDeviceControl {
60 fn platform(&self) -> Platform {
61 Platform::Ios
62 }
63
64 fn as_ios_simctl(&self) -> Option<&SimctlClient> {
65 Some(&self.client)
66 }
67
68 async fn launch(&self, udid: &str, bundle_id: &str) -> Result<u32, DeviceControlError> {
71 self.client.launch(udid, bundle_id).await.map(|res| res.pid)
72 }
73
74 async fn launch_with_args(
76 &self,
77 udid: &str,
78 bundle_id: &str,
79 args: &[String],
80 _activity: Option<&str>,
81 ) -> Result<u32, DeviceControlError> {
82 self.client
83 .launch_with_args(udid, bundle_id, args)
84 .await
85 .map(|res| res.pid)
86 }
87
88 async fn terminate(&self, udid: &str, bundle_id: &str) -> Result<(), DeviceControlError> {
89 self.client.terminate(udid, bundle_id).await
90 }
91
92 async fn install(&self, udid: &str, app_path: &str) -> Result<(), DeviceControlError> {
93 self.client.install(udid, app_path).await
94 }
95
96 async fn uninstall(&self, udid: &str, bundle_id: &str) -> Result<(), DeviceControlError> {
97 self.client.uninstall(udid, bundle_id).await
98 }
99
100 async fn set_animations_quiet(
126 &self,
127 _udid: &str,
128 _quiet: bool,
129 ) -> Result<(), DeviceControlError> {
130 Ok(())
131 }
132
133 async fn keychain_reset(&self, udid: &str) -> Result<(), DeviceControlError> {
134 self.client.keychain_reset(udid).await
135 }
136
137 async fn privacy_reset_all(
140 &self,
141 udid: &str,
142 bundle_id: &str,
143 ) -> Result<(), DeviceControlError> {
144 self.client.privacy_reset_all(udid, bundle_id).await
145 }
146
147 async fn clear_app_sandbox(
151 &self,
152 udid: &str,
153 bundle_id: &str,
154 ) -> Result<(), DeviceControlError> {
155 self.client.clear_app_sandbox(udid, bundle_id).await
156 }
157
158 async fn user_defaults_delete(
161 &self,
162 udid: &str,
163 bundle_id: &str,
164 key: &str,
165 ) -> Result<bool, DeviceControlError> {
166 self.client.user_defaults_delete(udid, bundle_id, key).await
167 }
168
169 async fn open_url(&self, udid: &str, url: &str) -> Result<(), DeviceControlError> {
172 self.client.open_url(udid, url).await
173 }
174
175 async fn send_push(
176 &self,
177 udid: &str,
178 bundle_id: &str,
179 apns_json_path: &str,
180 ) -> Result<(), DeviceControlError> {
181 self.client.send_push(udid, bundle_id, apns_json_path).await
182 }
183
184 async fn screenshot(&self, udid: &str) -> Result<Vec<u8>, DeviceControlError> {
185 self.client.screenshot(udid).await
186 }
187
188 async fn capture_bgra(
189 &self,
190 udid: &str,
191 ) -> Result<smix_simctl::surface_capture::CapturedFrame, DeviceControlError> {
192 self.client.capture_bgra(udid).await
193 }
194
195 async fn pasteboard_set(&self, udid: &str, text: &str) -> Result<(), DeviceControlError> {
198 self.client.pasteboard_set(udid, text).await
199 }
200
201 async fn pasteboard_get(&self, udid: &str) -> Result<String, DeviceControlError> {
202 self.client.pasteboard_get(udid).await
203 }
204
205 async fn add_media(&self, udid: &str, paths: &[String]) -> Result<(), DeviceControlError> {
206 self.client.add_media(udid, paths).await
207 }
208
209 async fn location_set(&self, udid: &str, lat: f64, lon: f64) -> Result<(), DeviceControlError> {
210 self.client.location_set(udid, lat, lon).await
211 }
212
213 async fn location_start(
214 &self,
215 udid: &str,
216 points: &[(f64, f64)],
217 speed_mps: Option<f64>,
218 ) -> Result<(), DeviceControlError> {
219 self.client.location_start(udid, points, speed_mps).await
220 }
221
222 async fn set_permission(
225 &self,
226 udid: &str,
227 bundle_id: &str,
228 permission: Permission,
229 action: PermissionAction,
230 ) -> Result<(), DeviceControlError> {
231 let Some(simctl_perm) = permission.to_simctl() else {
232 eprintln!(
236 "setPermissions: {permission:?} has no iOS mapping — skipped ({action:?} not applied)"
237 );
238 return Ok(());
239 };
240 match action {
241 PermissionAction::Grant => {
242 self.client
243 .grant_permission(udid, simctl_perm, bundle_id)
244 .await
245 }
246 PermissionAction::Revoke => {
247 self.client
248 .revoke_permission(udid, simctl_perm, bundle_id)
249 .await
250 }
251 PermissionAction::Reset => {
252 self.client
253 .reset_permission(udid, simctl_perm, bundle_id)
254 .await
255 }
256 }
257 }
258
259 async fn start_recording(
262 &self,
263 udid: &str,
264 output_path: &Path,
265 ) -> Result<(), DeviceControlError> {
266 let mut guard = self.recording.lock().await;
267 if guard.is_some() {
268 return Err(DeviceControlError::non_zero_exit(
271 "io recordVideo",
272 -1,
273 "a recording is already in progress (call stop_recording first)",
274 ));
275 }
276 let path_str = output_path.to_string_lossy();
277 let handle = self.client.record_video_start(udid, &path_str).await?;
278 *guard = Some(handle);
279 Ok(())
280 }
281
282 async fn stop_recording(&self) -> Result<(), DeviceControlError> {
283 let mut guard = self.recording.lock().await;
284 let handle = guard.take().ok_or_else(|| {
285 DeviceControlError::non_zero_exit(
286 "io recordVideo",
287 -1,
288 "no recording in progress (call start_recording first)",
289 )
290 })?;
291 self.client.record_video_stop(handle).await
292 }
293}