feat(springboard): add get_icon_state and set_icon_state methods (#63)

* feat(springboard): add get_icon_state method for reading home screen layout

Add get_icon_state() method to SpringBoardServicesClient that retrieves
the current home screen icon layout from iOS devices.

Features:
- Read complete home screen layout including icon positions and folders
- Support for optional formatVersion parameter
- Works on all iOS versions (tested on iOS 18.7.3)
- Comprehensive documentation with usage examples

Note: This PR intentionally does NOT include set_icon_state() as that
functionality is non-operational on iOS 18+ (see issue #62 for details).

Tested on:
- Device: iPhone 16,2 (iPhone 15 Pro)
- iOS: 18.7.3 (Build 22H217)

* feat(springboard): add set_icon_state method with date precision fix

- Implement set_icon_state() to modify home screen layout
- Implement set_icon_state_with_version() with format_version parameter
- Add truncate_dates_to_seconds() to convert nanosecond precision dates to second precision
- Fix iOS compatibility issue where high-precision dates were rejected
- Successfully tested on iOS 18.7.3 (previously believed to be restricted)
- Follows pymobiledevice3 implementation pattern

* refactor(utils): extract truncate_dates_to_seconds to utils::plist module

- Move date truncation logic from springboardservices to reusable utils::plist module
- Add comprehensive unit tests for date truncation functionality
- Add public API documentation for the utility function
- This makes the date normalization logic available for other services that may need it

* perf(springboard): normalize dates on read instead of write

- Move date truncation from set_icon_state to get_icon_state
- Eliminates unnecessary clone() operation in set_icon_state
- Better performance when setting icon state multiple times
- Cleaner API: data from get_icon_state is directly usable in set_icon_state
- Users don't need to worry about date precision issues

* refactor(springboard): address PR feedback - use Option<&str> and add error validation

- Change format_version parameter from Option<String> to Option<&str> for consistency
- Remove outdated iOS 18+ restriction comments since setIconState works on iOS 18+
- Add error validation to get_icon_state method similar to get_icon_pngdata
- Update documentation to reflect accurate iOS compatibility

* Fix cargo clippy warnings

* Fix clippy warnings in plist.rs

* Add springboard CLI commands

---------

Co-authored-by: Jackson Coxson <jkcoxson@gmail.com>
This commit is contained in:
fulln
2026-01-23 06:32:01 +08:00
committed by GitHub
parent 142708c289
commit 9a71279fe9
8 changed files with 371 additions and 5 deletions

View File

@@ -42,6 +42,7 @@ mod process_control;
mod remotexpc;
mod restore_service;
mod screenshot;
mod springboardservices;
mod syslog_relay;
mod pcap;
@@ -120,6 +121,7 @@ async fn main() {
.with_subcommand("remotexpc", remotexpc::register())
.with_subcommand("restore_service", restore_service::register())
.with_subcommand("screenshot", screenshot::register())
.with_subcommand("springboard", springboardservices::register())
.with_subcommand("syslog_relay", syslog_relay::register())
.subcommand_required(true)
.collect()
@@ -236,6 +238,9 @@ async fn main() {
"screenshot" => {
screenshot::main(sub_args, provider).await;
}
"springboard" => {
springboardservices::main(sub_args, provider).await;
}
"syslog_relay" => {
syslog_relay::main(sub_args, provider).await;
}

View File

@@ -0,0 +1,76 @@
// Jackson Coxson
use idevice::{
IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient,
};
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist};
pub fn register() -> JkCommand {
JkCommand::new()
.help("Manage the springboard service")
.with_subcommand(
"get_icon_state",
JkCommand::new()
.help("Gets the icon state from the device")
.with_argument(
JkArgument::new()
.with_help("Version to query by")
.required(false),
)
.with_flag(
JkFlag::new("save")
.with_help("Path to save to")
.with_argument(JkArgument::new().required(true)),
),
)
.with_subcommand(
"set_icon_state",
JkCommand::new().help("Sets the icon state").with_argument(
JkArgument::new()
.with_help("plist to set based on")
.required(true),
),
)
.subcommand_required(true)
}
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
let mut sbc = SpringBoardServicesClient::connect(&*provider)
.await
.expect("Failed to connect to springboardservices");
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
let mut sub_args = sub_args.clone();
match sub_name.as_str() {
"get_icon_state" => {
let version: Option<String> = sub_args.next_argument();
let version = version.as_deref();
let state = sbc
.get_icon_state(version)
.await
.expect("Failed to get icon state");
println!("{}", pretty_print_plist(&state));
if let Some(path) = sub_args.get_flag::<String>("save") {
tokio::fs::write(path, plist_value_to_xml_bytes(&state))
.await
.expect("Failed to save to path");
}
}
"set_icon_state" => {
let load_path = sub_args.next_argument::<String>().unwrap();
let load = tokio::fs::read(load_path)
.await
.expect("Failed to read plist");
let load: plist::Value =
plist::from_bytes(&load).expect("Failed to parse bytes as plist");
sbc.set_icon_state(load)
.await
.expect("Failed to set icon state");
}
_ => unreachable!(),
}
}