use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
use sim_lib_scene::{GlanceCard, GlanceMetric};
use sim_lib_view_device::{
ConsentReceipt, DeviceSampleStore, EdgeId, Evicted, FrameClock, RetentionReaper, StoreKey,
StoredSample, require_with_consent,
};
use sim_value::{access, build};
pub const CAP_GLASSES_POSE: &str = "glasses/pose";
pub const CAP_GLASSES_CAMERA: &str = "glasses/camera";
pub const CAP_GLASSES_WORLD_ANCHOR: &str = "glasses/world-anchor";
pub const CAP_GLASSES_HAND: &str = "glasses/hand";
pub const CAP_GLASSES_MIC: &str = "glasses/mic";
pub const CAP_GLASSES_VENDOR_REPORT: &str = "glasses/vendor-report";
const XR_NAMESPACE: &str = "xr";
const GLASSES_NAMESPACE: &str = "glasses";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GlassesCapability {
Pose,
Camera,
WorldAnchor,
Hand,
Mic,
VendorReport,
}
impl GlassesCapability {
pub const ALL: [Self; 6] = [
Self::Pose,
Self::Camera,
Self::WorldAnchor,
Self::Hand,
Self::Mic,
Self::VendorReport,
];
pub fn as_str(self) -> &'static str {
match self {
Self::Pose => CAP_GLASSES_POSE,
Self::Camera => CAP_GLASSES_CAMERA,
Self::WorldAnchor => CAP_GLASSES_WORLD_ANCHOR,
Self::Hand => CAP_GLASSES_HAND,
Self::Mic => CAP_GLASSES_MIC,
Self::VendorReport => CAP_GLASSES_VENDOR_REPORT,
}
}
pub fn local_name(self) -> &'static str {
match self {
Self::Pose => "pose",
Self::Camera => "camera",
Self::WorldAnchor => "world-anchor",
Self::Hand => "hand",
Self::Mic => "mic",
Self::VendorReport => "vendor-report",
}
}
pub fn capability_name(self) -> CapabilityName {
CapabilityName::new(self.as_str())
}
pub fn grant_symbol(self) -> Symbol {
Symbol::qualified(GLASSES_NAMESPACE, self.local_name())
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|capability| capability.as_str() == name)
}
}
pub fn glasses_pose_grant() -> Symbol {
GlassesCapability::Pose.grant_symbol()
}
pub fn glasses_camera_grant() -> Symbol {
GlassesCapability::Camera.grant_symbol()
}
pub fn glasses_world_anchor_grant() -> Symbol {
GlassesCapability::WorldAnchor.grant_symbol()
}
pub fn glasses_hand_grant() -> Symbol {
GlassesCapability::Hand.grant_symbol()
}
pub fn glasses_mic_grant() -> Symbol {
GlassesCapability::Mic.grant_symbol()
}
pub fn glasses_vendor_report_grant() -> Symbol {
GlassesCapability::VendorReport.grant_symbol()
}
pub fn glasses_mic_capability() -> CapabilityName {
GlassesCapability::Mic.capability_name()
}
pub fn glasses_capability_for_expr(expr: &Expr) -> Result<GlassesCapability> {
if access::field(expr, "world-anchor").is_some() {
return Ok(GlassesCapability::WorldAnchor);
}
let symbol = access::field_sym(expr, "sample")
.or_else(|| access::field_sym(expr, "kind"))
.ok_or_else(|| Error::Eval("missing glasses sample or kind field".to_owned()))?;
match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
(Some(XR_NAMESPACE), "pose") => Ok(GlassesCapability::Pose),
(Some(XR_NAMESPACE), "camera-frame") => Ok(GlassesCapability::Camera),
(Some(XR_NAMESPACE), "hand") => Ok(GlassesCapability::Hand),
(Some(XR_NAMESPACE), "mic-chunk") => Ok(GlassesCapability::Mic),
(Some(GLASSES_NAMESPACE), "world-anchor") => Ok(GlassesCapability::WorldAnchor),
(Some(GLASSES_NAMESPACE), "vendor-report") => Ok(GlassesCapability::VendorReport),
_ => Err(Error::HostError(format!(
"no glasses consent capability for {}",
symbol.as_qualified_str()
))),
}
}
pub fn require_glasses_consent(
cx: &Cx,
capability: GlassesCapability,
receipt: &ConsentReceipt,
session: &EdgeId,
) -> Result<()> {
require_with_consent(cx, capability.as_str(), receipt, session)
}
pub fn require_glasses_expr_consent(
cx: &Cx,
expr: &Expr,
receipt: &ConsentReceipt,
session: &EdgeId,
) -> Result<GlassesCapability> {
let capability = glasses_capability_for_expr(expr)?;
require_glasses_consent(cx, capability, receipt, session)?;
Ok(capability)
}
pub fn store_glasses_sample(
store: &mut DeviceSampleStore,
capability: GlassesCapability,
sample_id: impl Into<String>,
value: Expr,
receipt: &ConsentReceipt,
clock: FrameClock,
content_refs: Vec<StoreKey>,
) -> Result<StoreKey> {
let sample_id = sample_id.into();
if sample_id.is_empty() {
return Err(Error::Eval(
"glasses sample id must not be empty".to_owned(),
));
}
let key = StoreKey::new(Symbol::qualified(
"glasses/store",
format!("{}-{sample_id}", capability.local_name()),
));
store.insert_sample(StoredSample::new(
key.clone(),
receipt.seq,
clock.tick,
content_refs,
stored_glasses_value(capability, value),
));
Ok(key)
}
pub fn sweep_glasses_privacy(
store: &mut DeviceSampleStore,
receipts: &[ConsentReceipt],
clock: FrameClock,
) -> Vec<Evicted> {
RetentionReaper::new().sweep(store, receipts, clock)
}
pub fn active_glasses_consent_badge_cluster(receipts: &[ConsentReceipt]) -> Expr {
let mut badges = Vec::new();
for receipt in receipts {
for grant in glasses_grants(receipt) {
badges.push(sim_lib_scene::badge(
"ok",
&format!("{} {}ms", grant.as_qualified_str(), receipt.retain_ms),
));
}
if !receipt.redact.is_empty() && receipt_has_glasses_grants(receipt) {
badges.push(sim_lib_scene::badge(
"warn",
&format!("retention {}ms", receipt.retain_ms),
));
}
}
sim_lib_scene::badge_cluster(badges)
}
pub fn halo_consent_glyph(receipts: &[ConsentReceipt]) -> Expr {
let grant_count = receipts.iter().flat_map(glasses_grants).count();
let retain_ms = receipts
.iter()
.filter(|receipt| receipt_has_glasses_grants(receipt))
.map(|receipt| receipt.retain_ms)
.min()
.unwrap_or(0);
GlanceCard::new(
"Consent",
Some(GlanceMetric::new(
"grants",
format!("{grant_count}/{retain_ms}ms"),
)),
None,
"info",
1,
)
.to_scene()
}
fn stored_glasses_value(capability: GlassesCapability, value: Expr) -> Expr {
build::map(vec![
("kind", build::qsym("glasses", "sensitive-sample")),
("capability", Expr::Symbol(capability.grant_symbol())),
(capability.local_name(), value),
])
}
fn glasses_grants(receipt: &ConsentReceipt) -> impl Iterator<Item = &Symbol> {
receipt
.grants
.iter()
.filter(|grant| grant.namespace.as_deref() == Some(GLASSES_NAMESPACE))
}
fn receipt_has_glasses_grants(receipt: &ConsentReceipt) -> bool {
glasses_grants(receipt).next().is_some()
}