lcsa_core/
mobile_policy.rs1use 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}