1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct AndroidPermission {
24 pub name: &'static str,
25 pub max_sdk: Option<u32>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum OhosScene {
34 InUse,
35 Always,
36}
37
38impl OhosScene {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 OhosScene::InUse => "inuse",
42 OhosScene::Always => "always",
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct OhosPermission {
50 pub name: &'static str,
51 pub when: OhosScene,
52}
53
54#[derive(Clone, Copy, Debug)]
56pub struct PermissionSpec {
57 pub name: &'static str,
59 pub variant: &'static str,
62 pub android: &'static [AndroidPermission],
63 pub ios: &'static [&'static str],
64 pub macos: &'static [&'static str],
65 pub ohos: &'static [OhosPermission],
66 pub needs_reason: bool,
69}
70
71const fn android(name: &'static str) -> AndroidPermission {
72 AndroidPermission {
73 name,
74 max_sdk: None,
75 }
76}
77
78const fn ohos(name: &'static str) -> OhosPermission {
79 OhosPermission {
80 name,
81 when: OhosScene::InUse,
82 }
83}
84
85pub const ALL: &[PermissionSpec] = &[
87 PermissionSpec {
88 name: "location-when-in-use",
89 variant: "Location",
90 android: &[
91 android("android.permission.ACCESS_FINE_LOCATION"),
92 android("android.permission.ACCESS_COARSE_LOCATION"),
93 ],
94 ios: &["NSLocationWhenInUseUsageDescription"],
95 macos: &[
97 "NSLocationWhenInUseUsageDescription",
98 "NSLocationUsageDescription",
99 ],
100 ohos: &[
101 ohos("ohos.permission.APPROXIMATELY_LOCATION"),
102 ohos("ohos.permission.LOCATION"),
103 ],
104 needs_reason: true,
105 },
106 PermissionSpec {
107 name: "location-always",
108 variant: "LocationAlways",
109 android: &[
110 android("android.permission.ACCESS_FINE_LOCATION"),
111 android("android.permission.ACCESS_COARSE_LOCATION"),
112 android("android.permission.ACCESS_BACKGROUND_LOCATION"),
113 ],
114 ios: &[
116 "NSLocationAlwaysAndWhenInUseUsageDescription",
117 "NSLocationWhenInUseUsageDescription",
118 ],
119 macos: &[
120 "NSLocationAlwaysAndWhenInUseUsageDescription",
121 "NSLocationWhenInUseUsageDescription",
122 "NSLocationUsageDescription",
123 ],
124 ohos: &[
125 ohos("ohos.permission.APPROXIMATELY_LOCATION"),
126 ohos("ohos.permission.LOCATION"),
127 OhosPermission {
128 name: "ohos.permission.LOCATION_IN_BACKGROUND",
129 when: OhosScene::Always,
130 },
131 ],
132 needs_reason: true,
133 },
134 PermissionSpec {
135 name: "camera",
136 variant: "Camera",
137 android: &[android("android.permission.CAMERA")],
138 ios: &["NSCameraUsageDescription"],
139 macos: &["NSCameraUsageDescription"],
140 ohos: &[ohos("ohos.permission.CAMERA")],
141 needs_reason: true,
142 },
143 PermissionSpec {
144 name: "microphone",
145 variant: "Microphone",
146 android: &[android("android.permission.RECORD_AUDIO")],
147 ios: &["NSMicrophoneUsageDescription"],
148 macos: &["NSMicrophoneUsageDescription"],
149 ohos: &[ohos("ohos.permission.MICROPHONE")],
150 needs_reason: true,
151 },
152 PermissionSpec {
153 name: "notifications",
154 variant: "Notifications",
155 android: &[android("android.permission.POST_NOTIFICATIONS")],
157 ios: &[],
160 macos: &[],
161 ohos: &[],
162 needs_reason: false,
163 },
164 PermissionSpec {
165 name: "photos",
166 variant: "Photos",
167 android: &[
168 AndroidPermission {
169 name: "android.permission.READ_MEDIA_IMAGES",
170 max_sdk: None,
171 },
172 AndroidPermission {
173 name: "android.permission.READ_MEDIA_VIDEO",
174 max_sdk: None,
175 },
176 AndroidPermission {
177 name: "android.permission.READ_EXTERNAL_STORAGE",
178 max_sdk: Some(32),
179 },
180 ],
181 ios: &["NSPhotoLibraryUsageDescription"],
182 macos: &["NSPhotoLibraryUsageDescription"],
183 ohos: &[ohos("ohos.permission.READ_IMAGEVIDEO")],
186 needs_reason: true,
187 },
188 PermissionSpec {
189 name: "motion",
190 variant: "Motion",
191 android: &[android("android.permission.ACTIVITY_RECOGNITION")],
192 ios: &["NSMotionUsageDescription"],
193 macos: &[],
195 ohos: &[ohos("ohos.permission.ACTIVITY_MOTION")],
196 needs_reason: true,
197 },
198];
199
200pub const OHOS_PHOTOS_APL_NOTE: &str = "ohos.permission.READ_IMAGEVIDEO is a system_basic permission, which an app signed at the \
203 default `normal` level cannot be granted. Prefer PhotoViewPicker, which needs no permission.";
204
205pub fn find(name: &str) -> Option<&'static PermissionSpec> {
207 ALL.iter().find(|s| s.name == name)
208}
209
210pub fn find_variant(variant: &str) -> Option<&'static PermissionSpec> {
212 ALL.iter().find(|s| s.variant == variant)
213}
214
215pub fn names() -> Vec<&'static str> {
217 ALL.iter().map(|s| s.name).collect()
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn names_are_unique_kebab_and_round_trip() {
226 let mut seen = std::collections::BTreeSet::new();
227 for spec in ALL {
228 assert!(seen.insert(spec.name), "duplicate name {}", spec.name);
229 assert!(
230 spec.name
231 .chars()
232 .all(|c| c.is_ascii_lowercase() || c == '-'),
233 "{} is not kebab-case",
234 spec.name
235 );
236 assert_eq!(find(spec.name).map(|s| s.variant), Some(spec.variant));
237 assert_eq!(find_variant(spec.variant).map(|s| s.name), Some(spec.name));
238 }
239 assert_eq!(names().len(), ALL.len());
240 assert!(find("nonsense").is_none());
241 }
242
243 #[test]
246 fn location_always_also_declares_when_in_use() {
247 let spec = find("location-always").expect("location-always");
248 assert!(spec.ios.contains(&"NSLocationWhenInUseUsageDescription"));
249 assert!(spec.macos.contains(&"NSLocationWhenInUseUsageDescription"));
250 }
251
252 #[test]
255 fn notifications_declares_android_only() {
256 let spec = find("notifications").expect("notifications");
257 assert_eq!(spec.android.len(), 1);
258 assert!(spec.ios.is_empty() && spec.macos.is_empty() && spec.ohos.is_empty());
259 assert!(!spec.needs_reason);
260 }
261
262 #[test]
264 fn photos_caps_legacy_storage() {
265 let spec = find("photos").expect("photos");
266 let legacy = spec
267 .android
268 .iter()
269 .find(|p| p.name.ends_with("READ_EXTERNAL_STORAGE"))
270 .expect("legacy storage permission");
271 assert_eq!(legacy.max_sdk, Some(32));
272 for p in spec
274 .android
275 .iter()
276 .filter(|p| p.name.contains("READ_MEDIA"))
277 {
278 assert_eq!(p.max_sdk, None, "{} should not be capped", p.name);
279 }
280 }
281
282 #[test]
284 fn reasons_have_a_destination() {
285 for spec in ALL {
286 if spec.needs_reason {
287 assert!(
288 !spec.ios.is_empty() || !spec.ohos.is_empty(),
289 "{} needs a reason but no platform consumes it",
290 spec.name
291 );
292 } else {
293 assert!(
294 spec.ios.is_empty() && spec.macos.is_empty() && spec.ohos.is_empty(),
295 "{} needs no reason, so it must declare no reason-carrying key",
296 spec.name
297 );
298 }
299 }
300 }
301
302 #[test]
304 fn motion_is_ios_only_on_apple() {
305 let spec = find("motion").expect("motion");
306 assert!(!spec.ios.is_empty());
307 assert!(spec.macos.is_empty());
308 }
309}