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