1use std::fmt;
2
3use relay_conventions::consts::*;
4use relay_protocol::{Annotated, Empty, FromValue, IntoValue};
5
6use crate::protocol::{Attributes, Contexts, DeviceContext};
7
8#[derive(Clone, Copy, Debug, FromValue, IntoValue, Empty, PartialEq)]
9pub struct DeviceClass(pub u64);
10
11const GIB: u64 = 1024 * 1024 * 1024;
12
13impl DeviceClass {
14 pub const LOW: Self = Self(1);
15 pub const MEDIUM: Self = Self(2);
16 pub const HIGH: Self = Self(3);
17
18 pub fn from_attributes(attributes: &Attributes) -> Option<DeviceClass> {
23 let family = attributes.get_value(DEVICE_FAMILY)?.as_str()?;
24
25 if is_apple_family(family) {
26 let model = attributes.get_value(DEVICE_MODEL)?.as_str()?;
27 model_to_class(model)
28 } else {
29 let freq = attributes.get_value(DEVICE_PROCESSOR_FREQUENCY)?.as_f64()? as u64;
30 let proc = attributes.get_value(DEVICE_PROCESSOR_COUNT)?.as_f64()? as u64;
31 let mem = attributes.get_value(DEVICE_MEMORY_SIZE)?.as_f64()? as u64;
32 classify_by_hardware(freq, proc, mem)
33 }
34 }
35
36 pub fn from_contexts(contexts: &Contexts) -> Option<DeviceClass> {
37 let device = contexts.get::<DeviceContext>()?;
38 let family = device.family.value()?;
39
40 if is_apple_family(family) {
41 model_to_class(device.model.as_str()?)
42 } else if let (Some(&freq), Some(&proc), Some(&mem)) = (
43 device.processor_frequency.value(),
44 device.processor_count.value(),
45 device.memory_size.value(),
46 ) {
47 classify_by_hardware(freq, proc, mem)
48 } else {
49 None
50 }
51 }
52}
53
54impl fmt::Display for DeviceClass {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 self.0.fmt(f)
57 }
58}
59
60fn is_apple_family(family: &str) -> bool {
66 matches!(family, "iPhone" | "iOS" | "iOS-Device")
67}
68
69fn classify_by_hardware(freq: u64, proc: u64, mem: u64) -> Option<DeviceClass> {
74 if freq < 2000 || proc < 8 || mem < 4 * GIB {
75 Some(DeviceClass::LOW)
76 } else if freq < 2500 || mem < 6 * GIB {
77 Some(DeviceClass::MEDIUM)
78 } else {
79 Some(DeviceClass::HIGH)
80 }
81}
82
83fn model_to_class(model: &str) -> Option<DeviceClass> {
84 match model {
85 "iPhone1,1" => Some(DeviceClass::LOW),
87 "iPhone1,2" => Some(DeviceClass::LOW),
88 "iPhone2,1" => Some(DeviceClass::LOW),
89 "iPhone3,1" => Some(DeviceClass::LOW),
90 "iPhone3,2" => Some(DeviceClass::LOW),
91 "iPhone3,3" => Some(DeviceClass::LOW),
92 "iPhone4,1" => Some(DeviceClass::LOW),
93 "iPhone5,1" => Some(DeviceClass::LOW),
94 "iPhone5,2" => Some(DeviceClass::LOW),
95 "iPhone5,3" => Some(DeviceClass::LOW),
96 "iPhone5,4" => Some(DeviceClass::LOW),
97 "iPhone6,1" => Some(DeviceClass::LOW),
98 "iPhone6,2" => Some(DeviceClass::LOW),
99 "iPhone7,1" => Some(DeviceClass::LOW),
100 "iPhone7,2" => Some(DeviceClass::LOW),
101 "iPhone8,1" => Some(DeviceClass::LOW),
102 "iPhone8,2" => Some(DeviceClass::LOW),
103 "iPhone8,4" => Some(DeviceClass::LOW),
104 "iPhone9,1" => Some(DeviceClass::MEDIUM),
105 "iPhone9,3" => Some(DeviceClass::MEDIUM),
106 "iPhone9,2" => Some(DeviceClass::MEDIUM),
107 "iPhone9,4" => Some(DeviceClass::MEDIUM),
108 "iPhone10,1" => Some(DeviceClass::MEDIUM),
109 "iPhone10,4" => Some(DeviceClass::MEDIUM),
110 "iPhone10,2" => Some(DeviceClass::MEDIUM),
111 "iPhone10,5" => Some(DeviceClass::MEDIUM),
112 "iPhone10,3" => Some(DeviceClass::MEDIUM),
113 "iPhone10,6" => Some(DeviceClass::MEDIUM),
114 "iPhone11,8" => Some(DeviceClass::MEDIUM),
115 "iPhone11,2" => Some(DeviceClass::MEDIUM),
116 "iPhone11,4" => Some(DeviceClass::MEDIUM),
117 "iPhone11,6" => Some(DeviceClass::MEDIUM),
118 "iPhone12,1" => Some(DeviceClass::MEDIUM),
119 "iPhone12,3" => Some(DeviceClass::MEDIUM),
120 "iPhone12,5" => Some(DeviceClass::MEDIUM),
121 "iPhone12,8" => Some(DeviceClass::MEDIUM),
122 "iPhone13,1" => Some(DeviceClass::HIGH),
123 "iPhone13,2" => Some(DeviceClass::HIGH),
124 "iPhone13,3" => Some(DeviceClass::HIGH),
125 "iPhone13,4" => Some(DeviceClass::HIGH),
126 "iPhone14,4" => Some(DeviceClass::HIGH),
127 "iPhone14,5" => Some(DeviceClass::HIGH),
128 "iPhone14,2" => Some(DeviceClass::HIGH),
129 "iPhone14,3" => Some(DeviceClass::HIGH),
130 "iPhone14,6" => Some(DeviceClass::HIGH),
131 "iPhone14,7" => Some(DeviceClass::HIGH),
132 "iPhone14,8" => Some(DeviceClass::HIGH),
133 "iPhone15,2" => Some(DeviceClass::HIGH),
134 "iPhone15,3" => Some(DeviceClass::HIGH),
135 "iPhone15,4" => Some(DeviceClass::HIGH),
136 "iPhone15,5" => Some(DeviceClass::HIGH),
137 "iPhone16,1" => Some(DeviceClass::HIGH),
138 "iPhone16,2" => Some(DeviceClass::HIGH),
139 "iPhone17,1" => Some(DeviceClass::HIGH),
140 "iPhone17,2" => Some(DeviceClass::HIGH),
141 "iPhone17,3" => Some(DeviceClass::HIGH),
142 "iPhone17,4" => Some(DeviceClass::HIGH),
143 "iPhone17,5" => Some(DeviceClass::HIGH),
144 "iPhone18,1" => Some(DeviceClass::HIGH),
145 "iPhone18,2" => Some(DeviceClass::HIGH),
146 "iPhone18,3" => Some(DeviceClass::HIGH),
147 "iPhone18,4" => Some(DeviceClass::HIGH),
148 "iPhone18,5" => Some(DeviceClass::HIGH),
149
150 "iPad1,1" => Some(DeviceClass::LOW),
152 "iPad2,1" => Some(DeviceClass::LOW),
153 "iPad2,2" => Some(DeviceClass::LOW),
154 "iPad2,3" => Some(DeviceClass::LOW),
155 "iPad2,4" => Some(DeviceClass::LOW),
156 "iPad2,5" => Some(DeviceClass::LOW),
157 "iPad2,6" => Some(DeviceClass::LOW),
158 "iPad2,7" => Some(DeviceClass::LOW),
159 "iPad3,1" => Some(DeviceClass::LOW),
160 "iPad3,2" => Some(DeviceClass::LOW),
161 "iPad3,3" => Some(DeviceClass::LOW),
162 "iPad3,4" => Some(DeviceClass::LOW),
163 "iPad3,5" => Some(DeviceClass::LOW),
164 "iPad3,6" => Some(DeviceClass::LOW),
165 "iPad4,1" => Some(DeviceClass::LOW),
166 "iPad4,2" => Some(DeviceClass::LOW),
167 "iPad4,3" => Some(DeviceClass::LOW),
168 "iPad4,4" => Some(DeviceClass::LOW),
169 "iPad4,5" => Some(DeviceClass::LOW),
170 "iPad4,6" => Some(DeviceClass::LOW),
171 "iPad4,7" => Some(DeviceClass::LOW),
172 "iPad4,8" => Some(DeviceClass::LOW),
173 "iPad4,9" => Some(DeviceClass::LOW),
174 "iPad5,1" => Some(DeviceClass::LOW),
175 "iPad5,2" => Some(DeviceClass::LOW),
176 "iPad5,3" => Some(DeviceClass::LOW),
177 "iPad5,4" => Some(DeviceClass::LOW),
178 "iPad6,3" => Some(DeviceClass::MEDIUM),
179 "iPad6,4" => Some(DeviceClass::MEDIUM),
180 "iPad6,7" => Some(DeviceClass::MEDIUM),
181 "iPad6,8" => Some(DeviceClass::MEDIUM),
182 "iPad6,11" => Some(DeviceClass::LOW),
183 "iPad6,12" => Some(DeviceClass::LOW),
184 "iPad7,1" => Some(DeviceClass::MEDIUM),
185 "iPad7,2" => Some(DeviceClass::MEDIUM),
186 "iPad7,3" => Some(DeviceClass::MEDIUM),
187 "iPad7,4" => Some(DeviceClass::MEDIUM),
188 "iPad7,5" => Some(DeviceClass::MEDIUM),
189 "iPad7,6" => Some(DeviceClass::MEDIUM),
190 "iPad7,11" => Some(DeviceClass::MEDIUM),
191 "iPad7,12" => Some(DeviceClass::MEDIUM),
192 "iPad8,1" => Some(DeviceClass::MEDIUM),
193 "iPad8,2" => Some(DeviceClass::MEDIUM),
194 "iPad8,3" => Some(DeviceClass::MEDIUM),
195 "iPad8,4" => Some(DeviceClass::MEDIUM),
196 "iPad8,5" => Some(DeviceClass::MEDIUM),
197 "iPad8,6" => Some(DeviceClass::MEDIUM),
198 "iPad8,7" => Some(DeviceClass::MEDIUM),
199 "iPad8,8" => Some(DeviceClass::MEDIUM),
200 "iPad8,9" => Some(DeviceClass::MEDIUM),
201 "iPad8,10" => Some(DeviceClass::MEDIUM),
202 "iPad8,11" => Some(DeviceClass::MEDIUM),
203 "iPad8,12" => Some(DeviceClass::MEDIUM),
204 "iPad11,1" => Some(DeviceClass::MEDIUM),
205 "iPad11,2" => Some(DeviceClass::MEDIUM),
206 "iPad11,3" => Some(DeviceClass::MEDIUM),
207 "iPad11,4" => Some(DeviceClass::MEDIUM),
208 "iPad11,6" => Some(DeviceClass::MEDIUM),
209 "iPad11,7" => Some(DeviceClass::MEDIUM),
210 "iPad12,1" => Some(DeviceClass::MEDIUM),
211 "iPad12,2" => Some(DeviceClass::MEDIUM),
212 "iPad13,1" => Some(DeviceClass::HIGH),
213 "iPad13,2" => Some(DeviceClass::HIGH),
214 "iPad13,4" => Some(DeviceClass::HIGH),
215 "iPad13,5" => Some(DeviceClass::HIGH),
216 "iPad13,6" => Some(DeviceClass::HIGH),
217 "iPad13,7" => Some(DeviceClass::HIGH),
218 "iPad13,8" => Some(DeviceClass::HIGH),
219 "iPad13,9" => Some(DeviceClass::HIGH),
220 "iPad13,10" => Some(DeviceClass::HIGH),
221 "iPad13,11" => Some(DeviceClass::HIGH),
222 "iPad13,16" => Some(DeviceClass::HIGH),
223 "iPad13,17" => Some(DeviceClass::HIGH),
224 "iPad13,18" => Some(DeviceClass::HIGH),
225 "iPad13,19" => Some(DeviceClass::HIGH),
226 "iPad14,1" => Some(DeviceClass::HIGH),
227 "iPad14,2" => Some(DeviceClass::HIGH),
228 "iPad14,3" => Some(DeviceClass::HIGH),
229 "iPad14,4" => Some(DeviceClass::HIGH),
230 "iPad14,5" => Some(DeviceClass::HIGH),
231 "iPad14,6" => Some(DeviceClass::HIGH),
232 "iPad14,8" => Some(DeviceClass::HIGH),
233 "iPad14,9" => Some(DeviceClass::HIGH),
234 "iPad14,10" => Some(DeviceClass::HIGH),
235 "iPad14,11" => Some(DeviceClass::HIGH),
236 "iPad15,3" => Some(DeviceClass::HIGH),
237 "iPad15,4" => Some(DeviceClass::HIGH),
238 "iPad15,5" => Some(DeviceClass::HIGH),
239 "iPad15,6" => Some(DeviceClass::HIGH),
240 "iPad15,7" => Some(DeviceClass::HIGH),
241 "iPad15,8" => Some(DeviceClass::HIGH),
242 "iPad16,1" => Some(DeviceClass::HIGH),
243 "iPad16,2" => Some(DeviceClass::HIGH),
244 "iPad16,3" => Some(DeviceClass::HIGH),
245 "iPad16,4" => Some(DeviceClass::HIGH),
246 "iPad16,5" => Some(DeviceClass::HIGH),
247 "iPad16,6" => Some(DeviceClass::HIGH),
248 "iPad16,8" => Some(DeviceClass::HIGH),
249 "iPad16,9" => Some(DeviceClass::HIGH),
250 "iPad16,10" => Some(DeviceClass::HIGH),
251 "iPad16,11" => Some(DeviceClass::HIGH),
252 "iPad17,1" => Some(DeviceClass::HIGH),
253 "iPad17,2" => Some(DeviceClass::HIGH),
254 "iPad17,3" => Some(DeviceClass::HIGH),
255 "iPad17,4" => Some(DeviceClass::HIGH),
256
257 _ => Some(DeviceClass::HIGH),
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 macro_rules! attributes {
267 ($($key:expr => $value:expr),* $(,)?) => {
268 Attributes::from([
269 $(($key.into(), Annotated::new($value.into())),)*
270 ])
271 };
272 }
273
274 #[test]
275 fn test_from_attributes_iphone17_5() {
276 let attrs = attributes! {
277 DEVICE_FAMILY => "iOS",
278 DEVICE_MODEL => "iPhone17,5",
279 };
280 assert_eq!(
281 DeviceClass::from_attributes(&attrs),
282 Some(DeviceClass::HIGH)
283 );
284 }
285
286 #[test]
287 fn test_from_attributes_iphone99_1() {
288 let attrs = attributes! {
289 DEVICE_FAMILY => "iOS",
290 DEVICE_MODEL => "iPhone99,1",
291 };
292 assert_eq!(
293 DeviceClass::from_attributes(&attrs),
294 Some(DeviceClass::HIGH)
295 );
296 }
297
298 #[test]
299 fn test_from_attributes_ipad99_1() {
300 let attrs = attributes! {
301 DEVICE_FAMILY => "iOS",
302 DEVICE_MODEL => "iPad99,1",
303 };
304 assert_eq!(
305 DeviceClass::from_attributes(&attrs),
306 Some(DeviceClass::HIGH)
307 );
308 }
309
310 #[test]
311 fn test_from_attributes_garbage_device_model() {
312 let attrs = attributes! {
313 DEVICE_FAMILY => "iOS",
314 DEVICE_MODEL => "garbage-device-model",
315 };
316 assert_eq!(
317 DeviceClass::from_attributes(&attrs),
318 Some(DeviceClass::HIGH)
319 );
320 }
321
322 #[test]
323 fn test_from_attributes_wrong_family() {
324 let attrs = attributes! {
325 DEVICE_FAMILY => "iOSS",
326 DEVICE_MODEL => "iPhone17,5",
327 };
328 assert_eq!(DeviceClass::from_attributes(&attrs), None);
329 }
330
331 #[test]
332 fn test_from_attributes_android_low() {
333 let attrs = attributes! {
334 DEVICE_FAMILY => "Android",
335 DEVICE_PROCESSOR_FREQUENCY => 1500.0,
336 DEVICE_PROCESSOR_COUNT => 4.0,
337 DEVICE_MEMORY_SIZE => 2_147_483_648.0,
338 };
339 assert_eq!(DeviceClass::from_attributes(&attrs), Some(DeviceClass::LOW));
340 }
341
342 #[test]
343 fn test_from_attributes_android_medium() {
344 let attrs = attributes! {
345 DEVICE_FAMILY => "Android",
346 DEVICE_PROCESSOR_FREQUENCY => 2200.0,
347 DEVICE_PROCESSOR_COUNT => 8.0,
348 DEVICE_MEMORY_SIZE => 5_368_709_120.0,
349 };
350 assert_eq!(
351 DeviceClass::from_attributes(&attrs),
352 Some(DeviceClass::MEDIUM)
353 );
354 }
355
356 #[test]
357 fn test_from_attributes_android_high() {
358 let attrs = attributes! {
359 DEVICE_FAMILY => "Android",
360 DEVICE_PROCESSOR_FREQUENCY => 3000.0,
361 DEVICE_PROCESSOR_COUNT => 8.0,
362 DEVICE_MEMORY_SIZE => 8_589_934_592.0,
363 };
364 assert_eq!(
365 DeviceClass::from_attributes(&attrs),
366 Some(DeviceClass::HIGH)
367 );
368 }
369
370 #[test]
371 fn test_from_attributes_android_missing_specs() {
372 let attrs = attributes! {
373 DEVICE_FAMILY => "Android",
374 };
375 assert_eq!(DeviceClass::from_attributes(&attrs), None);
376 }
377
378 #[test]
379 fn test_iphone17_5_returns_device_class_high() {
380 let mut contexts = Contexts::new();
381 contexts.add(DeviceContext {
382 family: Annotated::new("iOS".to_owned()),
383 model: Annotated::new("iPhone17,5".to_owned()),
384 ..DeviceContext::default()
385 });
386 assert_eq!(
387 DeviceClass::from_contexts(&contexts),
388 Some(DeviceClass::HIGH)
389 );
390 }
391
392 #[test]
393 fn test_iphone99_1_returns_device_class_high() {
394 let mut contexts = Contexts::new();
395 contexts.add(DeviceContext {
396 family: Annotated::new("iOS".to_owned()),
397 model: Annotated::new("iPhone99,1".to_owned()),
398 ..DeviceContext::default()
399 });
400 assert_eq!(
401 DeviceClass::from_contexts(&contexts),
402 Some(DeviceClass::HIGH)
403 );
404 }
405
406 #[test]
407 fn test_ipad99_1_returns_device_class_high() {
408 let mut contexts = Contexts::new();
409 contexts.add(DeviceContext {
410 family: Annotated::new("iOS".to_owned()),
411 model: Annotated::new("iPad99,1".to_owned()),
412 ..DeviceContext::default()
413 });
414 assert_eq!(
415 DeviceClass::from_contexts(&contexts),
416 Some(DeviceClass::HIGH)
417 );
418 }
419
420 #[test]
421 fn test_garbage_device_model_returns_device_class_high() {
422 let mut contexts = Contexts::new();
423 contexts.add(DeviceContext {
424 family: Annotated::new("iOS".to_owned()),
425 model: Annotated::new("garbage-device-model".to_owned()),
426 ..DeviceContext::default()
427 });
428 assert_eq!(
429 DeviceClass::from_contexts(&contexts),
430 Some(DeviceClass::HIGH)
431 );
432 }
433
434 #[test]
435 fn test_wrong_family_returns_none() {
436 let mut contexts = Contexts::new();
437 contexts.add(DeviceContext {
438 family: Annotated::new("iOSS".to_owned()),
439 model: Annotated::new("iPhone17,5".to_owned()),
440 ..DeviceContext::default()
441 });
442 assert_eq!(DeviceClass::from_contexts(&contexts), None);
443 }
444}