Skip to main content

lcsa_core/
mobile_policy.rs

1use serde::{Deserialize, Serialize};
2
3use crate::signals::SignalType;
4use crate::topology::Platform;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum MobileClipboardModel {
9    LegacyBackgroundReadable,
10    ForegroundOrImeOnly,
11    UserIntentGated,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum SignalDeliveryModel {
17    SystemWide,
18    AppLocalOnly,
19    ForegroundOnly,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct MobilePolicy {
24    pub platform: Platform,
25    pub major_version: u32,
26    pub clipboard_model: MobileClipboardModel,
27    pub selection_model: SignalDeliveryModel,
28    pub focus_model: SignalDeliveryModel,
29}
30
31impl MobilePolicy {
32    pub fn for_platform(platform: Platform, os_version: Option<&str>) -> Option<Self> {
33        match platform {
34            Platform::Android => {
35                let major = parse_major_version(os_version)?;
36                let clipboard_model = if major <= 9 {
37                    MobileClipboardModel::LegacyBackgroundReadable
38                } else {
39                    MobileClipboardModel::ForegroundOrImeOnly
40                };
41
42                Some(Self {
43                    platform,
44                    major_version: major,
45                    clipboard_model,
46                    selection_model: SignalDeliveryModel::AppLocalOnly,
47                    focus_model: SignalDeliveryModel::AppLocalOnly,
48                })
49            }
50            Platform::Ios => {
51                let major = parse_major_version(os_version)?;
52                let clipboard_model = if major >= 16 {
53                    MobileClipboardModel::UserIntentGated
54                } else {
55                    MobileClipboardModel::ForegroundOrImeOnly
56                };
57
58                Some(Self {
59                    platform,
60                    major_version: major,
61                    clipboard_model,
62                    selection_model: SignalDeliveryModel::AppLocalOnly,
63                    focus_model: SignalDeliveryModel::AppLocalOnly,
64                })
65            }
66            _ => None,
67        }
68    }
69
70    pub fn signal_delivery(&self, signal_type: SignalType) -> SignalDeliveryModel {
71        match signal_type {
72            SignalType::Clipboard => match self.clipboard_model {
73                MobileClipboardModel::LegacyBackgroundReadable => SignalDeliveryModel::SystemWide,
74                MobileClipboardModel::ForegroundOrImeOnly => SignalDeliveryModel::ForegroundOnly,
75                MobileClipboardModel::UserIntentGated => SignalDeliveryModel::AppLocalOnly,
76            },
77            SignalType::Selection => self.selection_model,
78            SignalType::Focus => self.focus_model,
79        }
80    }
81}
82
83fn parse_major_version(version: Option<&str>) -> Option<u32> {
84    let version = version?.trim();
85    if version.is_empty() {
86        return None;
87    }
88
89    let mut digits = String::new();
90    for char in version.chars() {
91        if char.is_ascii_digit() {
92            digits.push(char);
93        } else if !digits.is_empty() {
94            break;
95        }
96    }
97
98    if digits.is_empty() {
99        None
100    } else {
101        digits.parse::<u32>().ok()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn android_legacy_policy_is_systemwide_for_clipboard() {
111        let policy = MobilePolicy::for_platform(Platform::Android, Some("9")).expect("policy");
112        assert_eq!(
113            policy.clipboard_model,
114            MobileClipboardModel::LegacyBackgroundReadable
115        );
116        assert_eq!(
117            policy.signal_delivery(SignalType::Clipboard),
118            SignalDeliveryModel::SystemWide
119        );
120    }
121
122    #[test]
123    fn android_modern_policy_limits_clipboard_scope() {
124        let policy = MobilePolicy::for_platform(Platform::Android, Some("14.0.0")).expect("policy");
125        assert_eq!(
126            policy.clipboard_model,
127            MobileClipboardModel::ForegroundOrImeOnly
128        );
129        assert_eq!(
130            policy.signal_delivery(SignalType::Clipboard),
131            SignalDeliveryModel::ForegroundOnly
132        );
133    }
134
135    #[test]
136    fn ios_16_and_newer_is_user_intent_gated() {
137        let policy = MobilePolicy::for_platform(Platform::Ios, Some("16.6")).expect("policy");
138        assert_eq!(
139            policy.clipboard_model,
140            MobileClipboardModel::UserIntentGated
141        );
142        assert_eq!(
143            policy.signal_delivery(SignalType::Clipboard),
144            SignalDeliveryModel::AppLocalOnly
145        );
146    }
147
148    #[test]
149    fn ios_15_is_foreground_only_for_clipboard() {
150        let policy = MobilePolicy::for_platform(Platform::Ios, Some("15")).expect("policy");
151        assert_eq!(
152            policy.clipboard_model,
153            MobileClipboardModel::ForegroundOrImeOnly
154        );
155        assert_eq!(
156            policy.signal_delivery(SignalType::Clipboard),
157            SignalDeliveryModel::ForegroundOnly
158        );
159    }
160}