use async_trait::async_trait;
use std::path::Path;
use tokio::sync::Mutex;
use smix_driver::Platform;
use smix_simctl::{DeviceControlError, RecordingHandle, SimctlClient};
use crate::PermissionAction;
use crate::device_control::{DeviceControl, Permission};
pub struct IosDeviceControl {
client: SimctlClient,
recording: Mutex<Option<RecordingHandle>>,
}
impl Default for IosDeviceControl {
fn default() -> Self {
Self::new()
}
}
impl IosDeviceControl {
#[must_use]
pub fn new() -> Self {
IosDeviceControl {
client: SimctlClient::new(),
recording: Mutex::new(None),
}
}
#[must_use]
pub fn with_client(client: SimctlClient) -> Self {
IosDeviceControl {
client,
recording: Mutex::new(None),
}
}
#[must_use]
pub fn simctl(&self) -> &SimctlClient {
&self.client
}
}
#[async_trait]
impl DeviceControl for IosDeviceControl {
fn platform(&self) -> Platform {
Platform::Ios
}
fn as_ios_simctl(&self) -> Option<&SimctlClient> {
Some(&self.client)
}
async fn launch(&self, udid: &str, bundle_id: &str) -> Result<u32, DeviceControlError> {
self.client.launch(udid, bundle_id).await.map(|res| res.pid)
}
async fn launch_with_args(
&self,
udid: &str,
bundle_id: &str,
args: &[String],
_activity: Option<&str>,
) -> Result<u32, DeviceControlError> {
self.client
.launch_with_args(udid, bundle_id, args)
.await
.map(|res| res.pid)
}
async fn terminate(&self, udid: &str, bundle_id: &str) -> Result<(), DeviceControlError> {
self.client.terminate(udid, bundle_id).await
}
async fn install(&self, udid: &str, app_path: &str) -> Result<(), DeviceControlError> {
self.client.install(udid, app_path).await
}
async fn uninstall(&self, udid: &str, bundle_id: &str) -> Result<(), DeviceControlError> {
self.client.uninstall(udid, bundle_id).await
}
async fn set_animations_quiet(
&self,
_udid: &str,
_quiet: bool,
) -> Result<(), DeviceControlError> {
Ok(())
}
async fn keychain_reset(&self, udid: &str) -> Result<(), DeviceControlError> {
self.client.keychain_reset(udid).await
}
async fn privacy_reset_all(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), DeviceControlError> {
self.client.privacy_reset_all(udid, bundle_id).await
}
async fn clear_app_sandbox(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), DeviceControlError> {
self.client.clear_app_sandbox(udid, bundle_id).await
}
async fn user_defaults_delete(
&self,
udid: &str,
bundle_id: &str,
key: &str,
) -> Result<bool, DeviceControlError> {
self.client.user_defaults_delete(udid, bundle_id, key).await
}
async fn open_url(&self, udid: &str, url: &str) -> Result<(), DeviceControlError> {
self.client.open_url(udid, url).await
}
async fn send_push(
&self,
udid: &str,
bundle_id: &str,
apns_json_path: &str,
) -> Result<(), DeviceControlError> {
self.client.send_push(udid, bundle_id, apns_json_path).await
}
async fn screenshot(&self, udid: &str) -> Result<Vec<u8>, DeviceControlError> {
self.client.screenshot(udid).await
}
async fn capture_bgra(
&self,
udid: &str,
) -> Result<smix_simctl::surface_capture::CapturedFrame, DeviceControlError> {
self.client.capture_bgra(udid).await
}
async fn pasteboard_set(&self, udid: &str, text: &str) -> Result<(), DeviceControlError> {
self.client.pasteboard_set(udid, text).await
}
async fn pasteboard_get(&self, udid: &str) -> Result<String, DeviceControlError> {
self.client.pasteboard_get(udid).await
}
async fn add_media(&self, udid: &str, paths: &[String]) -> Result<(), DeviceControlError> {
self.client.add_media(udid, paths).await
}
async fn location_set(&self, udid: &str, lat: f64, lon: f64) -> Result<(), DeviceControlError> {
self.client.location_set(udid, lat, lon).await
}
async fn location_start(
&self,
udid: &str,
points: &[(f64, f64)],
speed_mps: Option<f64>,
) -> Result<(), DeviceControlError> {
self.client.location_start(udid, points, speed_mps).await
}
async fn set_permission(
&self,
udid: &str,
bundle_id: &str,
permission: Permission,
action: PermissionAction,
) -> Result<(), DeviceControlError> {
let Some(simctl_perm) = permission.to_simctl() else {
eprintln!(
"setPermissions: {permission:?} has no iOS mapping — skipped ({action:?} not applied)"
);
return Ok(());
};
match action {
PermissionAction::Grant => {
self.client
.grant_permission(udid, simctl_perm, bundle_id)
.await
}
PermissionAction::Revoke => {
self.client
.revoke_permission(udid, simctl_perm, bundle_id)
.await
}
PermissionAction::Reset => {
self.client
.reset_permission(udid, simctl_perm, bundle_id)
.await
}
}
}
async fn start_recording(
&self,
udid: &str,
output_path: &Path,
) -> Result<(), DeviceControlError> {
let mut guard = self.recording.lock().await;
if guard.is_some() {
return Err(DeviceControlError::non_zero_exit(
"io recordVideo",
-1,
"a recording is already in progress (call stop_recording first)",
));
}
let path_str = output_path.to_string_lossy();
let handle = self.client.record_video_start(udid, &path_str).await?;
*guard = Some(handle);
Ok(())
}
async fn stop_recording(&self) -> Result<(), DeviceControlError> {
let mut guard = self.recording.lock().await;
let handle = guard.take().ok_or_else(|| {
DeviceControlError::non_zero_exit(
"io recordVideo",
-1,
"no recording in progress (call start_recording first)",
)
})?;
self.client.record_video_stop(handle).await
}
}