Files
idevice/idevice/src/services/springboardservices.rs
neo 54439b85dd feat(springboard): get homescreen icon metrics (#67)
* feat(springboard): get homescreen icon metrics

* chore: clippy and fmt
2026-02-13 13:00:47 -07:00

356 lines
12 KiB
Rust

//! SpringBoard Services Client
//!
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
//! which manages home screen and app icon related operations.
use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds};
/// Orientation of the device
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InterfaceOrientation {
/// Orientation is unknown or cannot be determined
Unknown = 0,
/// Portrait mode (normal vertical)
Portrait = 1,
/// Portrait mode upside down
PortraitUpsideDown = 2,
/// Landscape with home button on the right (notch to the left)
LandscapeRight = 3,
/// Landscape with home button on the left (notch to the right)
LandscapeLeft = 4,
}
/// Client for interacting with the iOS SpringBoard services
///
/// This service provides access to home screen and app icon functionality,
/// such as retrieving app icons.
#[derive(Debug)]
pub struct SpringBoardServicesClient {
/// The underlying device connection with established SpringBoard services
pub idevice: Idevice,
}
impl IdeviceService for SpringBoardServicesClient {
/// Returns the SpringBoard services name as registered with lockdownd
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.springboardservices")
}
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
Ok(Self::new(idevice))
}
}
impl SpringBoardServicesClient {
/// Creates a new SpringBoard services client from an existing device connection
///
/// # Arguments
/// * `idevice` - Pre-established device connection
pub fn new(idevice: Idevice) -> Self {
Self { idevice }
}
/// Retrieves the PNG icon data for a specified app
///
/// # Arguments
/// * `bundle_identifier` - The bundle identifier of the app (e.g., "com.apple.Maps")
///
/// # Returns
/// The raw PNG data of the app icon
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The app doesn't exist
/// - The response is malformed
///
/// # Example
/// ```rust
/// let icon_data = client.get_icon_pngdata("com.apple.Maps".to_string()).await?;
/// std::fs::write("maps_icon.png", icon_data)?;
/// ```
pub async fn get_icon_pngdata(
&mut self,
bundle_identifier: String,
) -> Result<Vec<u8>, IdeviceError> {
let req = crate::plist!({
"command": "getIconPNGData",
"bundleId": bundle_identifier,
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
match res.remove("pngData") {
Some(plist::Value::Data(res)) => Ok(res),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Retrieves the current icon state from the device
///
/// The icon state contains the layout and organization of all apps on the home screen,
/// including folder structures and icon positions. This is a read-only operation.
///
/// # Arguments
/// * `format_version` - Optional format version string for the icon state format
///
/// # Returns
/// A plist Value containing the complete icon state structure
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let icon_state = client.get_icon_state(None).await?;
/// println!("Icon state: {:?}", icon_state);
/// ```
///
/// # Notes
/// This method successfully reads the home screen layout on all iOS versions.
pub async fn get_icon_state(
&mut self,
format_version: Option<&str>,
) -> Result<plist::Value, IdeviceError> {
let req = crate::plist!({
"command": "getIconState",
"formatVersion":? format_version,
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist_value().await?;
// Some devices may return an error dictionary instead of icon state.
// Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata.
if let plist::Value::Dictionary(ref dict) = res
&& (dict.contains_key("error") || dict.contains_key("Error"))
{
return Err(IdeviceError::UnexpectedResponse);
}
truncate_dates_to_seconds(&mut res);
Ok(res)
}
/// Sets the icon state on the device
///
/// This method allows you to modify the home screen layout by providing a new icon state.
/// The icon state structure should match the format returned by `get_icon_state`.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
///
/// # Example
/// ```rust
/// use idevice::services::springboardservices::SpringBoardServicesClient;
///
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
/// let mut icon_state = client.get_icon_state(None).await?;
///
/// // Modify the icon state (e.g., swap two icons)
/// // ... modify icon_state ...
///
/// client.set_icon_state(icon_state).await?;
/// println!("Icon state updated successfully");
/// ```
///
/// # Notes
/// - Changes take effect immediately
/// - The device may validate the icon state structure before applying
/// - Invalid icon states will be rejected by the device
pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> {
let req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
});
self.idevice.send_plist(req).await?;
Ok(())
}
/// Sets the icon state with a specific format version
///
/// This is similar to `set_icon_state` but allows specifying a format version.
///
/// # Arguments
/// * `icon_state` - A plist Value containing the complete icon state structure
/// * `format_version` - Optional format version string
///
/// # Returns
/// Ok(()) if the icon state was successfully set
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The icon state format is invalid
/// - The device rejects the new layout
pub async fn set_icon_state_with_version(
&mut self,
icon_state: plist::Value,
format_version: Option<&str>,
) -> Result<(), IdeviceError> {
let req = crate::plist!({
"command": "setIconState",
"iconState": icon_state,
"formatVersion":? format_version,
});
self.idevice.send_plist(req).await?;
Ok(())
}
/// Gets the home screen wallpaper preview as PNG data
///
/// This gets a rendered preview of the home screen wallpaper.
///
/// # Returns
/// The raw PNG data of the home screen wallpaper preview
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device rejects the request
/// - The image is malformed/corupted
///
/// # Example
/// ```rust
/// let wallpaper = client.get_home_screen_wallpaper_preview_pngdata().await?;
/// std::fs::write("home.png", wallpaper)?;
/// ```
pub async fn get_home_screen_wallpaper_preview_pngdata(
&mut self,
) -> Result<Vec<u8>, IdeviceError> {
let req = crate::plist!({
"command": "getWallpaperPreviewImage",
"wallpaperName": "homescreen",
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
match res.remove("pngData") {
Some(plist::Value::Data(res)) => Ok(res),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Gets the lock screen wallpaper preview as PNG data
///
/// This gets a rendered preview of the lock screen wallpaper.
///
/// # Returns
/// The raw PNG data of the lock screen wallpaper preview
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device rejects the request
/// - The image is malformed/corupted
///
/// # Example
/// ```rust
/// let wallpaper = client.get_lock_screen_wallpaper_preview_pngdata().await?;
/// std::fs::write("lock.png", wallpaper)?;
/// ```
pub async fn get_lock_screen_wallpaper_preview_pngdata(
&mut self,
) -> Result<Vec<u8>, IdeviceError> {
let req = crate::plist!({
"command": "getWallpaperPreviewImage",
"wallpaperName": "lockscreen",
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
match res.remove("pngData") {
Some(plist::Value::Data(res)) => Ok(res),
_ => Err(IdeviceError::UnexpectedResponse),
}
}
/// Gets the current interface orientation of the device
///
/// This gets which way the device is currently facing
///
/// # Returns
/// The current `InterfaceOrientation` of the device
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The device doesn't support this command
/// - The response format is unexpected
///
/// # Example
/// ```rust
/// let orientation = client.get_interface_orientation().await?;
/// println!("Device orientation: {:?}", orientation);
/// ```
pub async fn get_interface_orientation(
&mut self,
) -> Result<InterfaceOrientation, IdeviceError> {
let req = crate::plist!({
"command": "getInterfaceOrientation",
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
let orientation_value = res
.get("interfaceOrientation")
.and_then(|v| v.as_unsigned_integer())
.ok_or(IdeviceError::UnexpectedResponse)?;
let orientation = match orientation_value {
1 => InterfaceOrientation::Portrait,
2 => InterfaceOrientation::PortraitUpsideDown,
3 => InterfaceOrientation::LandscapeRight,
4 => InterfaceOrientation::LandscapeLeft,
_ => InterfaceOrientation::Unknown,
};
Ok(orientation)
}
/// Gets the home screen icon layout metrics
///
/// Returns icon spacing, size, and positioning information
///
/// # Returns
/// A `plist::Dictionary` containing the icon layout metrics
///
/// # Errors
/// Returns `IdeviceError` if:
/// - Communication fails
/// - The response is malformed
///
/// # Example
/// ```rust
/// let metrics = client.get_homescreen_icon_metrics().await?;
/// println!("{:?}", metrics);
/// ```
pub async fn get_homescreen_icon_metrics(&mut self) -> Result<plist::Dictionary, IdeviceError> {
let req = crate::plist!({
"command": "getHomeScreenIconMetrics",
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
Ok(res)
}
}