use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AndroidDevice {
Pixel7Pro,
Pixel8,
GalaxyS23,
}
impl AndroidDevice {
pub fn as_str(self) -> &'static str {
match self {
Self::Pixel7Pro => "pixel-7-pro",
Self::Pixel8 => "pixel-8",
Self::GalaxyS23 => "galaxy-s23",
}
}
}
impl std::str::FromStr for AndroidDevice {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"pixel-7-pro" | "pixel7pro" => Ok(Self::Pixel7Pro),
"pixel-8" | "pixel8" => Ok(Self::Pixel8),
"galaxy-s23" | "s23" => Ok(Self::GalaxyS23),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AndroidProfile {
pub device: AndroidDevice,
pub width: u32,
pub height: u32,
pub device_scale_factor: f64,
pub mobile: bool,
pub user_agent: String,
pub user_agent_client_hints_platform: &'static str,
pub touch_enabled: bool,
}
impl AndroidProfile {
pub fn preset(device: AndroidDevice) -> Self {
match device {
AndroidDevice::Pixel7Pro => Self {
device,
width: 412,
height: 915,
device_scale_factor: 2.625,
mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 7 Pro) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/149.0.0.0 Mobile Safari/537.36"
.to_string(),
user_agent_client_hints_platform: "Android",
touch_enabled: true,
},
AndroidDevice::Pixel8 => Self {
device,
width: 412,
height: 915,
device_scale_factor: 2.625,
mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/149.0.0.0 Mobile Safari/537.36"
.to_string(),
user_agent_client_hints_platform: "Android",
touch_enabled: true,
},
AndroidDevice::GalaxyS23 => Self {
device,
width: 360,
height: 780,
device_scale_factor: 3.0,
mobile: true,
user_agent: "Mozilla/5.0 (Linux; Android 14; SM-S911B) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/149.0.0.0 Mobile Safari/537.36"
.to_string(),
user_agent_client_hints_platform: "Android",
touch_enabled: true,
},
}
}
pub fn device_metrics_payload(&self) -> serde_json::Value {
serde_json::json!({
"width": self.width,
"height": self.height,
"deviceScaleFactor": self.device_scale_factor,
"mobile": self.mobile,
})
}
pub fn user_agent_payload(&self) -> serde_json::Value {
serde_json::json!({
"userAgent": self.user_agent,
"platform": self.user_agent_client_hints_platform,
"userAgentMetadata": {
"platform": self.user_agent_client_hints_platform,
"platformVersion": "14.0.0",
"architecture": "",
"model": match self.device {
AndroidDevice::Pixel7Pro => "Pixel 7 Pro",
AndroidDevice::Pixel8 => "Pixel 8",
AndroidDevice::GalaxyS23 => "SM-S911B",
},
"mobile": self.mobile,
}
})
}
pub fn touch_payload(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.touch_enabled,
"maxTouchPoints": if self.touch_enabled { 5 } else { 0 },
})
}
pub fn cdp_commands(&self) -> Vec<(&'static str, serde_json::Value)> {
vec![
(
"Emulation.setDeviceMetricsOverride",
self.device_metrics_payload(),
),
("Emulation.setUserAgentOverride", self.user_agent_payload()),
("Emulation.setTouchEmulationEnabled", self.touch_payload()),
]
}
}
pub fn parse_mobile_profile(raw: &str) -> Option<AndroidProfile> {
let device: AndroidDevice = raw.parse().ok()?;
Some(AndroidProfile::preset(device))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pixel_7_pro_preset_is_stable() {
let p = AndroidProfile::preset(AndroidDevice::Pixel7Pro);
assert_eq!(p.width, 412);
assert_eq!(p.height, 915);
assert!((p.device_scale_factor - 2.625).abs() < 1e-6);
assert!(p.mobile);
assert!(p.touch_enabled);
assert!(p.user_agent.contains("Android 14"));
assert!(p.user_agent.contains("Pixel 7 Pro"));
}
#[test]
fn cdp_commands_cover_the_triple() {
let p = AndroidProfile::preset(AndroidDevice::Pixel8);
let cmds = p.cdp_commands();
assert_eq!(cmds.len(), 3);
assert_eq!(cmds[0].0, "Emulation.setDeviceMetricsOverride");
assert_eq!(cmds[1].0, "Emulation.setUserAgentOverride");
assert_eq!(cmds[2].0, "Emulation.setTouchEmulationEnabled");
}
#[test]
fn parse_mobile_profile_accepts_aliases() {
assert!(parse_mobile_profile("pixel-7-pro").is_some());
assert!(parse_mobile_profile("pixel8").is_some());
assert!(parse_mobile_profile("s23").is_some());
assert!(parse_mobile_profile("iphone").is_none());
}
}