use crate::error::AstudiosError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Serialize)]
pub struct AndroidStudioReleasesList {
#[serde(rename = "@version")]
pub version: String,
#[serde(rename = "item")]
pub items: Vec<AndroidStudio>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AndroidStudio {
pub name: String,
pub build: String,
pub version: String,
pub channel: String,
#[serde(rename = "platformBuild")]
pub platform_build: String,
#[serde(rename = "platformVersion")]
pub platform_version: String,
pub date: String,
#[serde(rename = "download")]
pub downloads: Vec<Download>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Download {
pub link: String,
pub size: String,
pub checksum: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ReleaseChannel {
Release,
Beta,
Canary,
ReleaseCandidate,
Patch,
}
impl AndroidStudio {
pub fn is_release(&self) -> bool {
self.channel == "Release"
}
pub fn is_beta(&self) -> bool {
self.channel == "Beta"
}
pub fn is_canary(&self) -> bool {
self.channel == "Canary"
}
pub fn is_rc(&self) -> bool {
self.channel == "RC"
}
pub fn is_patch(&self) -> bool {
self.channel == "Patch"
}
pub fn channel_type(&self) -> ReleaseChannel {
match self.channel.as_str() {
"Release" => ReleaseChannel::Release,
"Beta" => ReleaseChannel::Beta,
"Canary" => ReleaseChannel::Canary,
"RC" => ReleaseChannel::ReleaseCandidate,
"Patch" => ReleaseChannel::Patch,
_ => ReleaseChannel::Release, }
}
pub fn get_macos_download(&self) -> Option<&Download> {
self.downloads.iter().find(|d| d.link.contains("mac"))
}
pub fn get_platform_download(&self) -> Option<&Download> {
self.get_macos_download()
}
pub fn display_name(&self) -> String {
let channel_indicator = match self.channel_type() {
ReleaseChannel::Release => "",
ReleaseChannel::Beta => " (Beta)",
ReleaseChannel::Canary => " (Canary)",
ReleaseChannel::ReleaseCandidate => " (RC)",
ReleaseChannel::Patch => " (Patch)",
};
format!("{}{}", self.name, channel_indicator)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AndroidStudioVersion {
pub short_version: String,
pub build_version: String,
pub product_code: String,
pub build_number: String,
pub product_name: String,
}
impl AndroidStudioVersion {
pub fn new(
short_version: String,
build_version: String,
product_code: String,
build_number: String,
product_name: String,
) -> Self {
Self {
short_version,
build_version,
product_code,
build_number,
product_name,
}
}
pub fn display_version(&self) -> String {
format!("{} ({})", self.short_version, self.build_version)
}
pub fn identifier(&self) -> String {
self.build_version.clone()
}
pub fn is_stable(&self) -> bool {
!self.build_version.contains("Beta")
&& !self.build_version.contains("Canary")
&& !self.build_version.contains("RC")
}
}
impl std::fmt::Display for AndroidStudioVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_version())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledAndroidStudio {
pub path: PathBuf,
pub version: AndroidStudioVersion,
}
impl InstalledAndroidStudio {
pub fn new(path: PathBuf) -> Result<Option<Self>, AstudiosError> {
if !path.exists() {
return Ok(None);
}
if path.extension().is_none_or(|ext| ext != "app") {
return Ok(None);
}
match Self::parse_version_info(&path) {
Ok(version) => Ok(Some(Self { path, version })),
Err(_) => Ok(None), }
}
fn parse_version_info(app_path: &Path) -> Result<AndroidStudioVersion, AstudiosError> {
let contents_path = app_path.join("Contents");
let info_plist_path = contents_path.join("Info.plist");
let (short_version, build_version) = Self::parse_info_plist(&info_plist_path)?;
let product_info_path = contents_path.join("Resources").join("product-info.json");
let (product_name, product_code, build_number) =
Self::parse_product_info(&product_info_path)?;
Ok(AndroidStudioVersion::new(
short_version,
build_version,
product_code,
build_number,
product_name,
))
}
fn parse_info_plist(plist_path: &Path) -> Result<(String, String), AstudiosError> {
use plist::Value;
use std::fs::File;
let file = File::open(plist_path)
.map_err(|e| AstudiosError::General(format!("Failed to open Info.plist: {e}")))?;
let plist: Value = plist::from_reader(file)
.map_err(|e| AstudiosError::General(format!("Failed to parse Info.plist: {e}")))?;
let dict = plist
.as_dictionary()
.ok_or_else(|| AstudiosError::General("Info.plist is not a dictionary".to_string()))?;
let short_version = dict
.get("CFBundleShortVersionString")
.and_then(|v| v.as_string())
.ok_or_else(|| {
AstudiosError::General("CFBundleShortVersionString not found".to_string())
})?
.to_string();
let build_version = dict
.get("CFBundleVersion")
.and_then(|v| v.as_string())
.ok_or_else(|| AstudiosError::General("CFBundleVersion not found".to_string()))?
.to_string();
let bundle_id = dict
.get("CFBundleIdentifier")
.and_then(|v| v.as_string())
.unwrap_or("");
if !bundle_id.contains("android.studio") {
return Err(AstudiosError::General(
"Not an Android Studio application".to_string(),
));
}
Ok((short_version, build_version))
}
fn parse_product_info(
product_info_path: &Path,
) -> Result<(String, String, String), AstudiosError> {
use std::fs;
let content = fs::read_to_string(product_info_path).map_err(|e| {
AstudiosError::General(format!("Failed to read product-info.json: {e}"))
})?;
let json: serde_json::Value = serde_json::from_str(&content).map_err(|e| {
AstudiosError::General(format!("Failed to parse product-info.json: {e}"))
})?;
let product_name = json
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Android Studio")
.to_string();
let version = json
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AstudiosError::General("Version not found in product-info.json".to_string())
})?;
let build_number = json
.get("buildNumber")
.and_then(|v| v.as_str())
.unwrap_or(version);
let product_code = version.split('-').next().unwrap_or("AI").to_string();
Ok((product_name, product_code, build_number.to_string()))
}
pub fn display_name(&self) -> String {
format!(
"{} {}",
self.version.product_name, self.version.short_version
)
}
pub fn extract_detailed_version(&self) -> String {
if let Ok(Some(api_version)) = self.get_full_version_from_api() {
api_version
} else {
self.version.short_version.clone()
}
}
pub fn get_full_version_from_api(&self) -> Result<Option<String>, AstudiosError> {
use crate::list::AndroidStudioLister;
let lister = AndroidStudioLister::new()?;
let releases = lister.get_releases()?;
for release in &releases.items {
if release.build == self.version.build_version {
return Ok(Some(release.version.clone()));
}
}
Ok(None)
}
pub fn enhanced_display_name(&self) -> String {
let detailed_version = self.extract_detailed_version();
let channel_info = self.detect_channel_from_name();
if channel_info.is_empty() {
format!("{} {}", self.version.product_name, detailed_version)
} else {
format!(
"{} {} ({})",
self.version.product_name, detailed_version, channel_info
)
}
}
fn detect_channel_from_name(&self) -> String {
let app_name = self
.path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
if app_name.contains("Patch") {
"Patch".to_string()
} else if app_name.contains("Feature Drop") {
"Feature Drop".to_string()
} else if app_name.contains("Beta") {
"Beta".to_string()
} else if app_name.contains("Canary") {
"Canary".to_string()
} else if app_name.contains("RC") {
"RC".to_string()
} else {
"Release".to_string()
}
}
pub fn identifier(&self) -> String {
self.version.identifier()
}
pub fn is_valid(&self) -> bool {
self.path.exists() && self.path.join("Contents").exists()
}
}
impl PartialOrd for InstalledAndroidStudio {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for InstalledAndroidStudio {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.version.cmp(&other.version)
}
}