Skip to main content

ios_core/services/instruments/
devicestate.rs

1//! Device state / condition inducer service.
2
3use crate::proto::nskeyedarchiver_encode;
4use tokio::io::{AsyncRead, AsyncWrite};
5
6use crate::services::dtx::codec::{DtxConnection, DtxError};
7use crate::services::dtx::primitive_enc::archived_object;
8use crate::services::dtx::types::{DtxPayload, NSObject};
9
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
11pub struct ConditionProfile {
12    pub description: String,
13    pub identifier: String,
14    pub name: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
18pub struct ConditionProfileType {
19    pub active_profile: Option<String>,
20    pub identifier: String,
21    pub is_active: bool,
22    pub is_destructive: bool,
23    pub is_internal: bool,
24    pub name: String,
25    pub profiles_sorted: bool,
26    pub profiles: Vec<ConditionProfile>,
27}
28
29pub struct DeviceStateClient<S> {
30    conn: DtxConnection<S>,
31    channel_code: i32,
32}
33
34impl<S: AsyncRead + AsyncWrite + Unpin + Send> DeviceStateClient<S> {
35    pub async fn connect(stream: S) -> Result<Self, DtxError> {
36        let mut conn = DtxConnection::new(stream);
37        let channel_code = conn.request_channel(super::CONDITION_INDUCER_SVC).await?;
38        Ok(Self { conn, channel_code })
39    }
40
41    pub async fn list(&mut self) -> Result<Vec<ConditionProfileType>, DtxError> {
42        let msg = self
43            .conn
44            .method_call(self.channel_code, "availableConditionInducers", &[])
45            .await?;
46        parse_condition_profile_types(&msg.payload)
47    }
48
49    pub async fn enable(
50        &mut self,
51        profile_type_id: &str,
52        profile_id: &str,
53    ) -> Result<bool, DtxError> {
54        let response = self
55            .conn
56            .method_call(
57                self.channel_code,
58                "enableConditionWithIdentifier:profileIdentifier:",
59                &[
60                    archived_object(nskeyedarchiver_encode::archive_string(profile_type_id)),
61                    archived_object(nskeyedarchiver_encode::archive_string(profile_id)),
62                ],
63            )
64            .await?;
65        match response.payload {
66            DtxPayload::Response(NSObject::Bool(value)) => Ok(value),
67            DtxPayload::Response(NSObject::Int(value)) => Ok(value != 0),
68            DtxPayload::Response(NSObject::Uint(value)) => Ok(value != 0),
69            other => Err(DtxError::Protocol(format!(
70                "unexpected device state enable response: {other:?}"
71            ))),
72        }
73    }
74
75    pub async fn disable(&mut self) -> Result<(), DtxError> {
76        self.conn
77            .method_call_async(self.channel_code, "disableActiveCondition", &[])
78            .await
79    }
80}
81
82fn parse_condition_profile_types(
83    payload: &DtxPayload,
84) -> Result<Vec<ConditionProfileType>, DtxError> {
85    let items = match payload {
86        DtxPayload::Response(NSObject::Array(items)) => items,
87        DtxPayload::MethodInvocation { args, .. } => args
88            .iter()
89            .find_map(|arg| {
90                if let NSObject::Array(items) = arg {
91                    Some(items)
92                } else {
93                    None
94                }
95            })
96            .ok_or_else(|| DtxError::Protocol("condition inducer response missing array".into()))?,
97        other => {
98            return Err(DtxError::Protocol(format!(
99                "unexpected condition inducer response: {other:?}"
100            )))
101        }
102    };
103
104    items.iter().map(parse_profile_type).collect()
105}
106
107fn parse_profile_type(item: &NSObject) -> Result<ConditionProfileType, DtxError> {
108    let dict = match item {
109        NSObject::Dict(dict) => dict,
110        other => {
111            return Err(DtxError::Protocol(format!(
112                "condition profile type was not a dictionary: {other:?}"
113            )))
114        }
115    };
116
117    let profiles = match dict.get("profiles") {
118        Some(NSObject::Array(profiles)) => profiles
119            .iter()
120            .map(parse_profile)
121            .collect::<Result<Vec<_>, _>>()?,
122        _ => Vec::new(),
123    };
124
125    Ok(ConditionProfileType {
126        active_profile: get_string(dict, "activeProfile"),
127        identifier: require_string(dict, "identifier")?,
128        is_active: get_bool(dict, "isActive"),
129        is_destructive: get_bool(dict, "isDestructive"),
130        is_internal: get_bool(dict, "isInternal"),
131        name: require_string(dict, "name")?,
132        profiles_sorted: get_bool(dict, "profilesSorted"),
133        profiles,
134    })
135}
136
137fn parse_profile(item: &NSObject) -> Result<ConditionProfile, DtxError> {
138    let dict = match item {
139        NSObject::Dict(dict) => dict,
140        other => {
141            return Err(DtxError::Protocol(format!(
142                "condition profile was not a dictionary: {other:?}"
143            )))
144        }
145    };
146    Ok(ConditionProfile {
147        description: require_string(dict, "description")?,
148        identifier: require_string(dict, "identifier")?,
149        name: require_string(dict, "name")?,
150    })
151}
152
153fn get_string(dict: &indexmap::IndexMap<String, NSObject>, key: &str) -> Option<String> {
154    dict.get(key).and_then(|value| match value {
155        NSObject::String(value) => Some(value.clone()),
156        _ => None,
157    })
158}
159
160fn require_string(
161    dict: &indexmap::IndexMap<String, NSObject>,
162    key: &str,
163) -> Result<String, DtxError> {
164    get_string(dict, key).ok_or_else(|| DtxError::Protocol(format!("missing string key '{key}'")))
165}
166
167fn get_bool(dict: &indexmap::IndexMap<String, NSObject>, key: &str) -> bool {
168    dict.get(key)
169        .and_then(|value| match value {
170            NSObject::Bool(value) => Some(*value),
171            _ => None,
172        })
173        .unwrap_or(false)
174}
175
176#[cfg(test)]
177mod tests {
178    use indexmap::IndexMap;
179
180    use super::*;
181
182    #[test]
183    fn parses_condition_profile_types_from_response_array() {
184        let payload = DtxPayload::Response(NSObject::Array(vec![NSObject::Dict(
185            IndexMap::from_iter([
186                (
187                    "activeProfile".to_string(),
188                    NSObject::String("SlowNetwork3GGood".into()),
189                ),
190                (
191                    "identifier".to_string(),
192                    NSObject::String("SlowNetworkCondition".into()),
193                ),
194                ("isActive".to_string(), NSObject::Bool(true)),
195                ("isDestructive".to_string(), NSObject::Bool(false)),
196                ("isInternal".to_string(), NSObject::Bool(false)),
197                ("name".to_string(), NSObject::String("Slow Network".into())),
198                ("profilesSorted".to_string(), NSObject::Bool(true)),
199                (
200                    "profiles".to_string(),
201                    NSObject::Array(vec![NSObject::Dict(IndexMap::from_iter([
202                        (
203                            "description".to_string(),
204                            NSObject::String("3G good".into()),
205                        ),
206                        (
207                            "identifier".to_string(),
208                            NSObject::String("SlowNetwork3GGood".into()),
209                        ),
210                        ("name".to_string(), NSObject::String("3G".into())),
211                    ]))]),
212                ),
213            ]),
214        )]));
215
216        let parsed = parse_condition_profile_types(&payload).unwrap();
217        assert_eq!(parsed.len(), 1);
218        assert_eq!(parsed[0].identifier, "SlowNetworkCondition");
219        assert_eq!(parsed[0].profiles[0].identifier, "SlowNetwork3GGood");
220    }
221}