1use std::fmt;
2
3use relay_protocol::{Annotated, Empty, FromValue, IntoValue};
4
5use crate::protocol::{Contexts, DeviceContext};
6
7#[derive(Clone, Copy, Debug, FromValue, IntoValue, Empty, PartialEq)]
8pub struct DeviceClass(pub u64);
9
10const GIB: u64 = 1024 * 1024 * 1024;
11
12impl DeviceClass {
13 pub const LOW: Self = Self(1);
14 pub const MEDIUM: Self = Self(2);
15 pub const HIGH: Self = Self(3);
16
17 pub fn from_contexts(contexts: &Contexts) -> Option<DeviceClass> {
18 let device = contexts.get::<DeviceContext>()?;
19 let family = device.family.value()?;
20
21 if family == "iPhone" || family == "iOS" || family == "iOS-Device" {
22 model_to_class(device.model.as_str()?)
23 } else if let (Some(&freq), Some(&proc), Some(&mem)) = (
24 device.processor_frequency.value(),
25 device.processor_count.value(),
26 device.memory_size.value(),
27 ) {
28 if freq < 2000 || proc < 8 || mem < 4 * GIB {
29 Some(DeviceClass::LOW)
30 } else if freq < 2500 || mem < 6 * GIB {
31 Some(DeviceClass::MEDIUM)
32 } else {
33 Some(DeviceClass::HIGH)
34 }
35 } else {
36 None
37 }
38 }
39}
40
41impl fmt::Display for DeviceClass {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 self.0.fmt(f)
44 }
45}
46
47fn model_to_class(model: &str) -> Option<DeviceClass> {
48 match model {
49 "iPhone1,1" => Some(DeviceClass::LOW),
51 "iPhone1,2" => Some(DeviceClass::LOW),
52 "iPhone2,1" => Some(DeviceClass::LOW),
53 "iPhone3,1" => Some(DeviceClass::LOW),
54 "iPhone3,2" => Some(DeviceClass::LOW),
55 "iPhone3,3" => Some(DeviceClass::LOW),
56 "iPhone4,1" => Some(DeviceClass::LOW),
57 "iPhone5,1" => Some(DeviceClass::LOW),
58 "iPhone5,2" => Some(DeviceClass::LOW),
59 "iPhone5,3" => Some(DeviceClass::LOW),
60 "iPhone5,4" => Some(DeviceClass::LOW),
61 "iPhone6,1" => Some(DeviceClass::LOW),
62 "iPhone6,2" => Some(DeviceClass::LOW),
63 "iPhone7,1" => Some(DeviceClass::LOW),
64 "iPhone7,2" => Some(DeviceClass::LOW),
65 "iPhone8,1" => Some(DeviceClass::LOW),
66 "iPhone8,2" => Some(DeviceClass::LOW),
67 "iPhone8,4" => Some(DeviceClass::LOW),
68 "iPhone9,1" => Some(DeviceClass::MEDIUM),
69 "iPhone9,3" => Some(DeviceClass::MEDIUM),
70 "iPhone9,2" => Some(DeviceClass::MEDIUM),
71 "iPhone9,4" => Some(DeviceClass::MEDIUM),
72 "iPhone10,1" => Some(DeviceClass::MEDIUM),
73 "iPhone10,4" => Some(DeviceClass::MEDIUM),
74 "iPhone10,2" => Some(DeviceClass::MEDIUM),
75 "iPhone10,5" => Some(DeviceClass::MEDIUM),
76 "iPhone10,3" => Some(DeviceClass::MEDIUM),
77 "iPhone10,6" => Some(DeviceClass::MEDIUM),
78 "iPhone11,8" => Some(DeviceClass::MEDIUM),
79 "iPhone11,2" => Some(DeviceClass::MEDIUM),
80 "iPhone11,4" => Some(DeviceClass::MEDIUM),
81 "iPhone11,6" => Some(DeviceClass::MEDIUM),
82 "iPhone12,1" => Some(DeviceClass::MEDIUM),
83 "iPhone12,3" => Some(DeviceClass::MEDIUM),
84 "iPhone12,5" => Some(DeviceClass::MEDIUM),
85 "iPhone12,8" => Some(DeviceClass::MEDIUM),
86 "iPhone13,1" => Some(DeviceClass::HIGH),
87 "iPhone13,2" => Some(DeviceClass::HIGH),
88 "iPhone13,3" => Some(DeviceClass::HIGH),
89 "iPhone13,4" => Some(DeviceClass::HIGH),
90 "iPhone14,4" => Some(DeviceClass::HIGH),
91 "iPhone14,5" => Some(DeviceClass::HIGH),
92 "iPhone14,2" => Some(DeviceClass::HIGH),
93 "iPhone14,3" => Some(DeviceClass::HIGH),
94 "iPhone14,6" => Some(DeviceClass::HIGH),
95 "iPhone14,7" => Some(DeviceClass::HIGH),
96 "iPhone14,8" => Some(DeviceClass::HIGH),
97 "iPhone15,2" => Some(DeviceClass::HIGH),
98 "iPhone15,3" => Some(DeviceClass::HIGH),
99 "iPhone15,4" => Some(DeviceClass::HIGH),
100 "iPhone15,5" => Some(DeviceClass::HIGH),
101 "iPhone16,1" => Some(DeviceClass::HIGH),
102 "iPhone16,2" => Some(DeviceClass::HIGH),
103 "iPhone17,1" => Some(DeviceClass::HIGH),
104 "iPhone17,2" => Some(DeviceClass::HIGH),
105 "iPhone17,3" => Some(DeviceClass::HIGH),
106 "iPhone17,4" => Some(DeviceClass::HIGH),
107 "iPhone17,5" => Some(DeviceClass::HIGH),
108
109 "iPad1,1" => Some(DeviceClass::LOW),
111 "iPad1,2" => Some(DeviceClass::LOW),
112 "iPad2,1" => Some(DeviceClass::LOW),
113 "iPad2,2" => Some(DeviceClass::LOW),
114 "iPad2,3" => Some(DeviceClass::LOW),
115 "iPad2,4" => Some(DeviceClass::LOW),
116 "iPad3,1" => Some(DeviceClass::LOW),
117 "iPad3,2" => Some(DeviceClass::LOW),
118 "iPad3,3" => Some(DeviceClass::LOW),
119 "iPad2,5" => Some(DeviceClass::LOW),
120 "iPad2,6" => Some(DeviceClass::LOW),
121 "iPad2,7" => Some(DeviceClass::LOW),
122 "iPad3,4" => Some(DeviceClass::LOW),
123 "iPad3,5" => Some(DeviceClass::LOW),
124 "iPad3,6" => Some(DeviceClass::LOW),
125 "iPad4,1" => Some(DeviceClass::LOW),
126 "iPad4,2" => Some(DeviceClass::LOW),
127 "iPad4,3" => Some(DeviceClass::LOW),
128 "iPad4,4" => Some(DeviceClass::LOW),
129 "iPad4,5" => Some(DeviceClass::LOW),
130 "iPad4,6" => Some(DeviceClass::LOW),
131 "iPad4,7" => Some(DeviceClass::LOW),
132 "iPad4,8" => Some(DeviceClass::LOW),
133 "iPad4,9" => Some(DeviceClass::LOW),
134 "iPad5,1" => Some(DeviceClass::LOW),
135 "iPad5,2" => Some(DeviceClass::LOW),
136 "iPad5,3" => Some(DeviceClass::LOW),
137 "iPad5,4" => Some(DeviceClass::LOW),
138 "iPad6,3" => Some(DeviceClass::MEDIUM),
139 "iPad6,4" => Some(DeviceClass::MEDIUM),
140 "iPad6,7" => Some(DeviceClass::MEDIUM),
141 "iPad6,8" => Some(DeviceClass::MEDIUM),
142 "iPad6,11" => Some(DeviceClass::LOW),
143 "iPad6,12" => Some(DeviceClass::LOW),
144 "iPad7,2" => Some(DeviceClass::MEDIUM),
145 "iPad7,3" => Some(DeviceClass::MEDIUM),
146 "iPad7,4" => Some(DeviceClass::MEDIUM),
147 "iPad7,5" => Some(DeviceClass::MEDIUM),
148 "iPad7,6" => Some(DeviceClass::MEDIUM),
149 "iPad7,1" => Some(DeviceClass::MEDIUM),
150 "iPad7,11" => Some(DeviceClass::MEDIUM),
151 "iPad7,12" => Some(DeviceClass::MEDIUM),
152 "iPad8,1" => Some(DeviceClass::MEDIUM),
153 "iPad8,2" => Some(DeviceClass::MEDIUM),
154 "iPad8,3" => Some(DeviceClass::MEDIUM),
155 "iPad8,4" => Some(DeviceClass::MEDIUM),
156 "iPad8,5" => Some(DeviceClass::MEDIUM),
157 "iPad8,6" => Some(DeviceClass::MEDIUM),
158 "iPad8,7" => Some(DeviceClass::MEDIUM),
159 "iPad8,8" => Some(DeviceClass::MEDIUM),
160 "iPad8,9" => Some(DeviceClass::MEDIUM),
161 "iPad8,10" => Some(DeviceClass::MEDIUM),
162 "iPad8,11" => Some(DeviceClass::MEDIUM),
163 "iPad8,12" => Some(DeviceClass::MEDIUM),
164 "iPad11,1" => Some(DeviceClass::MEDIUM),
165 "iPad11,2" => Some(DeviceClass::MEDIUM),
166 "iPad11,3" => Some(DeviceClass::MEDIUM),
167 "iPad11,4" => Some(DeviceClass::MEDIUM),
168 "iPad11,6" => Some(DeviceClass::MEDIUM),
169 "iPad11,7" => Some(DeviceClass::MEDIUM),
170 "iPad12,1" => Some(DeviceClass::MEDIUM),
171 "iPad12,2" => Some(DeviceClass::MEDIUM),
172 "iPad14,1" => Some(DeviceClass::HIGH),
173 "iPad14,2" => Some(DeviceClass::HIGH),
174 "iPad13,1" => Some(DeviceClass::HIGH),
175 "iPad13,2" => Some(DeviceClass::HIGH),
176 "iPad13,4" => Some(DeviceClass::HIGH),
177 "iPad13,5" => Some(DeviceClass::HIGH),
178 "iPad13,6" => Some(DeviceClass::HIGH),
179 "iPad13,7" => Some(DeviceClass::HIGH),
180 "iPad13,8" => Some(DeviceClass::HIGH),
181 "iPad13,9" => Some(DeviceClass::HIGH),
182 "iPad13,10" => Some(DeviceClass::HIGH),
183 "iPad13,11" => Some(DeviceClass::HIGH),
184 "iPad13,16" => Some(DeviceClass::HIGH),
185 "iPad13,17" => Some(DeviceClass::HIGH),
186 "iPad13,18" => Some(DeviceClass::HIGH),
187 "iPad13,19" => Some(DeviceClass::HIGH),
188 "iPad14,3" => Some(DeviceClass::HIGH),
189 "iPad14,4" => Some(DeviceClass::HIGH),
190 "iPad14,5" => Some(DeviceClass::HIGH),
191 "iPad14,6" => Some(DeviceClass::HIGH),
192 "iPad14,8" => Some(DeviceClass::HIGH),
193 "iPad14,9" => Some(DeviceClass::HIGH),
194 "iPad14,10" => Some(DeviceClass::HIGH),
195 "iPad14,11" => Some(DeviceClass::HIGH),
196 "iPad16,1" => Some(DeviceClass::HIGH),
197 "iPad16,2" => Some(DeviceClass::HIGH),
198 "iPad16,3" => Some(DeviceClass::HIGH),
199 "iPad16,4" => Some(DeviceClass::HIGH),
200 "iPad16,5" => Some(DeviceClass::HIGH),
201 "iPad16,6" => Some(DeviceClass::HIGH),
202
203 _ => Some(DeviceClass::HIGH),
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_iphone17_5_returns_device_class_high() {
214 let mut contexts = Contexts::new();
215 contexts.add(DeviceContext {
216 family: Annotated::new("iOS".to_string()),
217 model: Annotated::new("iPhone17,5".to_string()),
218 ..DeviceContext::default()
219 });
220 assert_eq!(
221 DeviceClass::from_contexts(&contexts),
222 Some(DeviceClass::HIGH)
223 );
224 }
225
226 #[test]
227 fn test_iphone99_1_returns_device_class_high() {
228 let mut contexts = Contexts::new();
229 contexts.add(DeviceContext {
230 family: Annotated::new("iOS".to_string()),
231 model: Annotated::new("iPhone99,1".to_string()),
232 ..DeviceContext::default()
233 });
234 assert_eq!(
235 DeviceClass::from_contexts(&contexts),
236 Some(DeviceClass::HIGH)
237 );
238 }
239
240 #[test]
241 fn test_ipad99_1_returns_device_class_high() {
242 let mut contexts = Contexts::new();
243 contexts.add(DeviceContext {
244 family: Annotated::new("iOS".to_string()),
245 model: Annotated::new("iPad99,1".to_string()),
246 ..DeviceContext::default()
247 });
248 assert_eq!(
249 DeviceClass::from_contexts(&contexts),
250 Some(DeviceClass::HIGH)
251 );
252 }
253
254 #[test]
255 fn test_garbage_device_model_returns_device_class_high() {
256 let mut contexts = Contexts::new();
257 contexts.add(DeviceContext {
258 family: Annotated::new("iOS".to_string()),
259 model: Annotated::new("garbage-device-model".to_string()),
260 ..DeviceContext::default()
261 });
262 assert_eq!(
263 DeviceClass::from_contexts(&contexts),
264 Some(DeviceClass::HIGH)
265 );
266 }
267
268 #[test]
269 fn test_wrong_family_returns_none() {
270 let mut contexts = Contexts::new();
271 contexts.add(DeviceContext {
272 family: Annotated::new("iOSS".to_string()),
273 model: Annotated::new("iPhone17,5".to_string()),
274 ..DeviceContext::default()
275 });
276 assert_eq!(DeviceClass::from_contexts(&contexts), None);
277 }
278}