ios_core/services/instruments/
devicestate.rs1use 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}