ios-core 0.1.7

High-level device API, pairing transport, and discovery for iOS devices
Documentation
use tokio::io::{AsyncRead, AsyncWrite};

use crate::services::dtx::codec::{DtxConnection, DtxError};
use crate::services::dtx::primitive_enc::archived_object;
use crate::services::dtx::types::{DtxMessage, DtxPayload, NSObject};

#[derive(Debug, Clone, PartialEq)]
pub struct NotificationEvent {
    pub selector: String,
    pub payload: NSObject,
    pub channel_code: i32,
}

pub struct NotificationClient<S> {
    conn: DtxConnection<S>,
}

impl<S: AsyncRead + AsyncWrite + Unpin + Send> NotificationClient<S> {
    pub async fn connect(stream: S) -> Result<Self, DtxError> {
        let mut conn = DtxConnection::new(stream);
        let channel_code = conn
            .request_channel(super::MOBILE_NOTIFICATIONS_SVC)
            .await?;

        conn.method_call(
            channel_code,
            "setApplicationStateNotificationsEnabled:",
            &[archived_object(
                crate::proto::nskeyedarchiver_encode::archive_bool(true),
            )],
        )
        .await?;
        conn.method_call(
            channel_code,
            "setMemoryNotificationsEnabled:",
            &[archived_object(
                crate::proto::nskeyedarchiver_encode::archive_bool(true),
            )],
        )
        .await?;

        Ok(Self { conn })
    }

    pub async fn next_notification(&mut self) -> Result<NotificationEvent, DtxError> {
        loop {
            let msg = self.conn.recv().await?;
            if msg.expects_reply {
                self.conn.send_ack(&msg).await?;
            }
            if let Some(event) = parse_notification_message(&msg) {
                return Ok(event);
            }
        }
    }
}

fn parse_notification_message(msg: &DtxMessage) -> Option<NotificationEvent> {
    let (selector, args) = match &msg.payload {
        DtxPayload::MethodInvocation { selector, args } => (selector, args),
        _ => return None,
    };

    if selector != "applicationStateNotification:" && selector != "memoryNotification:" {
        return None;
    }

    let payload = args.first().cloned().unwrap_or(NSObject::Null);
    Some(NotificationEvent {
        selector: selector.clone(),
        payload,
        channel_code: msg.channel_code,
    })
}

#[cfg(test)]
mod tests {
    use indexmap::IndexMap;

    use super::*;

    #[test]
    fn parses_application_state_notification_payload() {
        let msg = DtxMessage {
            identifier: 11,
            conversation_idx: 0,
            channel_code: 7,
            expects_reply: false,
            payload: DtxPayload::MethodInvocation {
                selector: "applicationStateNotification:".into(),
                args: vec![NSObject::Dict(IndexMap::from_iter([
                    (
                        "ApplicationBundleIdentifier".into(),
                        NSObject::String("com.apple.Preferences".into()),
                    ),
                    ("State".into(), NSObject::Int(8)),
                ]))],
            },
        };

        let event = parse_notification_message(&msg).expect("notification");
        assert_eq!(event.selector, "applicationStateNotification:");
        assert_eq!(event.channel_code, 7);
        match event.payload {
            NSObject::Dict(payload) => {
                assert_eq!(
                    payload.get("ApplicationBundleIdentifier"),
                    Some(&NSObject::String("com.apple.Preferences".into()))
                );
                assert_eq!(payload.get("State"), Some(&NSObject::Int(8)));
            }
            other => panic!("unexpected payload: {other:?}"),
        }
    }

    #[test]
    fn ignores_non_notification_messages() {
        let msg = DtxMessage {
            identifier: 12,
            conversation_idx: 0,
            channel_code: 3,
            expects_reply: false,
            payload: DtxPayload::MethodInvocation {
                selector: "runningProcesses".into(),
                args: vec![],
            },
        };

        assert!(parse_notification_message(&msg).is_none());
    }
}