#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AndroidPermission {
pub name: &'static str,
pub max_sdk: Option<u32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OhosScene {
InUse,
Always,
}
impl OhosScene {
pub fn as_str(self) -> &'static str {
match self {
OhosScene::InUse => "inuse",
OhosScene::Always => "always",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OhosPermission {
pub name: &'static str,
pub when: OhosScene,
}
#[derive(Clone, Copy, Debug)]
pub struct PermissionSpec {
pub name: &'static str,
pub variant: &'static str,
pub android: &'static [AndroidPermission],
pub ios: &'static [&'static str],
pub macos: &'static [&'static str],
pub ohos: &'static [OhosPermission],
pub needs_reason: bool,
}
const fn android(name: &'static str) -> AndroidPermission {
AndroidPermission {
name,
max_sdk: None,
}
}
const fn ohos(name: &'static str) -> OhosPermission {
OhosPermission {
name,
when: OhosScene::InUse,
}
}
pub const ALL: &[PermissionSpec] = &[
PermissionSpec {
name: "location-when-in-use",
variant: "Location",
android: &[
android("android.permission.ACCESS_FINE_LOCATION"),
android("android.permission.ACCESS_COARSE_LOCATION"),
],
ios: &["NSLocationWhenInUseUsageDescription"],
macos: &[
"NSLocationWhenInUseUsageDescription",
"NSLocationUsageDescription",
],
ohos: &[
ohos("ohos.permission.APPROXIMATELY_LOCATION"),
ohos("ohos.permission.LOCATION"),
],
needs_reason: true,
},
PermissionSpec {
name: "location-always",
variant: "LocationAlways",
android: &[
android("android.permission.ACCESS_FINE_LOCATION"),
android("android.permission.ACCESS_COARSE_LOCATION"),
android("android.permission.ACCESS_BACKGROUND_LOCATION"),
],
ios: &[
"NSLocationAlwaysAndWhenInUseUsageDescription",
"NSLocationWhenInUseUsageDescription",
],
macos: &[
"NSLocationAlwaysAndWhenInUseUsageDescription",
"NSLocationWhenInUseUsageDescription",
"NSLocationUsageDescription",
],
ohos: &[
ohos("ohos.permission.APPROXIMATELY_LOCATION"),
ohos("ohos.permission.LOCATION"),
OhosPermission {
name: "ohos.permission.LOCATION_IN_BACKGROUND",
when: OhosScene::Always,
},
],
needs_reason: true,
},
PermissionSpec {
name: "camera",
variant: "Camera",
android: &[android("android.permission.CAMERA")],
ios: &["NSCameraUsageDescription"],
macos: &["NSCameraUsageDescription"],
ohos: &[ohos("ohos.permission.CAMERA")],
needs_reason: true,
},
PermissionSpec {
name: "microphone",
variant: "Microphone",
android: &[android("android.permission.RECORD_AUDIO")],
ios: &["NSMicrophoneUsageDescription"],
macos: &["NSMicrophoneUsageDescription"],
ohos: &[ohos("ohos.permission.MICROPHONE")],
needs_reason: true,
},
PermissionSpec {
name: "notifications",
variant: "Notifications",
android: &[android("android.permission.POST_NOTIFICATIONS")],
ios: &[],
macos: &[],
ohos: &[],
needs_reason: false,
},
PermissionSpec {
name: "photos",
variant: "Photos",
android: &[
AndroidPermission {
name: "android.permission.READ_MEDIA_IMAGES",
max_sdk: None,
},
AndroidPermission {
name: "android.permission.READ_MEDIA_VIDEO",
max_sdk: None,
},
AndroidPermission {
name: "android.permission.READ_EXTERNAL_STORAGE",
max_sdk: Some(32),
},
],
ios: &["NSPhotoLibraryUsageDescription"],
macos: &["NSPhotoLibraryUsageDescription"],
ohos: &[ohos("ohos.permission.READ_IMAGEVIDEO")],
needs_reason: true,
},
PermissionSpec {
name: "motion",
variant: "Motion",
android: &[android("android.permission.ACTIVITY_RECOGNITION")],
ios: &["NSMotionUsageDescription"],
macos: &[],
ohos: &[ohos("ohos.permission.ACTIVITY_MOTION")],
needs_reason: true,
},
];
pub const OHOS_PHOTOS_APL_NOTE: &str = "ohos.permission.READ_IMAGEVIDEO is a system_basic permission, which an app signed at the \
default `normal` level cannot be granted. Prefer PhotoViewPicker, which needs no permission.";
pub fn find(name: &str) -> Option<&'static PermissionSpec> {
ALL.iter().find(|s| s.name == name)
}
pub fn find_variant(variant: &str) -> Option<&'static PermissionSpec> {
ALL.iter().find(|s| s.variant == variant)
}
pub fn names() -> Vec<&'static str> {
ALL.iter().map(|s| s.name).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_unique_kebab_and_round_trip() {
let mut seen = std::collections::BTreeSet::new();
for spec in ALL {
assert!(seen.insert(spec.name), "duplicate name {}", spec.name);
assert!(
spec.name
.chars()
.all(|c| c.is_ascii_lowercase() || c == '-'),
"{} is not kebab-case",
spec.name
);
assert_eq!(find(spec.name).map(|s| s.variant), Some(spec.variant));
assert_eq!(find_variant(spec.variant).map(|s| s.name), Some(spec.name));
}
assert_eq!(names().len(), ALL.len());
assert!(find("nonsense").is_none());
}
#[test]
fn location_always_also_declares_when_in_use() {
let spec = find("location-always").expect("location-always");
assert!(spec.ios.contains(&"NSLocationWhenInUseUsageDescription"));
assert!(spec.macos.contains(&"NSLocationWhenInUseUsageDescription"));
}
#[test]
fn notifications_declares_android_only() {
let spec = find("notifications").expect("notifications");
assert_eq!(spec.android.len(), 1);
assert!(spec.ios.is_empty() && spec.macos.is_empty() && spec.ohos.is_empty());
assert!(!spec.needs_reason);
}
#[test]
fn photos_caps_legacy_storage() {
let spec = find("photos").expect("photos");
let legacy = spec
.android
.iter()
.find(|p| p.name.ends_with("READ_EXTERNAL_STORAGE"))
.expect("legacy storage permission");
assert_eq!(legacy.max_sdk, Some(32));
for p in spec
.android
.iter()
.filter(|p| p.name.contains("READ_MEDIA"))
{
assert_eq!(p.max_sdk, None, "{} should not be capped", p.name);
}
}
#[test]
fn reasons_have_a_destination() {
for spec in ALL {
if spec.needs_reason {
assert!(
!spec.ios.is_empty() || !spec.ohos.is_empty(),
"{} needs a reason but no platform consumes it",
spec.name
);
} else {
assert!(
spec.ios.is_empty() && spec.macos.is_empty() && spec.ohos.is_empty(),
"{} needs no reason, so it must declare no reason-carrying key",
spec.name
);
}
}
}
#[test]
fn motion_is_ios_only_on_apple() {
let spec = find("motion").expect("motion");
assert!(!spec.ios.is_empty());
assert!(spec.macos.is_empty());
}
}