use crate::compiler::Table;
use crate::res::Chunk;
use anyhow::{Context, Result};
use std::io::Cursor;
use std::path::{Path, PathBuf};
use xcommon::{Scaler, ScalerOpts, Zip, ZipFile, ZipFileOptions};
mod compiler;
pub mod manifest;
pub mod res;
mod sign;
mod utils;
pub use crate::manifest::AndroidManifest;
pub use crate::utils::{Target, VersionCode};
pub use xcommon::{Certificate, Signer};
pub use zip;
pub struct Apk {
manifest: AndroidManifest,
path: PathBuf,
zip: Zip,
}
impl Apk {
pub fn new(path: PathBuf, manifest: AndroidManifest, compress: bool) -> Result<Self> {
let zip = Zip::new(&path, compress)?;
Ok(Self {
manifest,
path,
zip,
})
}
pub fn add_res(&mut self, icon: Option<&Path>, android: &Path) -> Result<()> {
let mut buf = vec![];
let mut table = Table::default();
table.import_apk(android)?;
if let Some(path) = icon {
let mut scaler = Scaler::open(path)?;
scaler.optimize();
let package = if let Some(package) = self.manifest.package.as_ref() {
package
} else {
anyhow::bail!("missing manifest.package");
};
let mipmap = crate::compiler::compile_mipmap(package, "icon")?;
let mut cursor = Cursor::new(&mut buf);
mipmap.chunk().write(&mut cursor)?;
self.zip.create_file(
Path::new("resources.arsc"),
ZipFileOptions::Aligned(4),
&buf,
)?;
for (name, size) in mipmap.variants() {
buf.clear();
let mut cursor = Cursor::new(&mut buf);
scaler.write(&mut cursor, ScalerOpts::new(size))?;
self.zip
.create_file(name.as_ref(), ZipFileOptions::Aligned(4), &buf)?;
}
table.import_chunk(mipmap.chunk());
self.manifest.application.icon = Some("@mipmap/icon".into());
}
let manifest = crate::compiler::compile_manifest(&self.manifest, &table)?;
buf.clear();
let mut cursor = Cursor::new(&mut buf);
manifest.write(&mut cursor)?;
self.zip.create_file(
Path::new("AndroidManifest.xml"),
ZipFileOptions::Compressed,
&buf,
)?;
Ok(())
}
pub fn add_dex(&mut self, dex: &Path) -> Result<()> {
self.zip
.add_file(dex, Path::new("classes.dex"), ZipFileOptions::Compressed)?;
Ok(())
}
pub fn add_zip_file(&mut self, f: ZipFile) -> Result<()> {
self.zip.add_zip_file(f)
}
pub fn add_lib(&mut self, target: Target, path: &Path) -> Result<()> {
let name = path
.file_name()
.context("invalid path")?
.to_str()
.context("invalid path")?;
self.zip.add_file(
path,
&Path::new("lib").join(target.android_abi()).join(name),
ZipFileOptions::Compressed,
)
}
pub fn add_file(&mut self, source: &Path, dest: &Path, opts: ZipFileOptions) -> Result<()> {
self.zip.add_file(source, dest, opts)
}
pub fn add_directory(&mut self, source: &Path, dest: &Path) -> Result<()> {
self.zip.add_directory(source, dest)
}
pub fn finish(self, signer: Option<Signer>) -> Result<()> {
self.zip.finish()?;
crate::sign::sign(&self.path, signer)?;
Ok(())
}
pub fn sign(path: &Path, signer: Option<Signer>) -> Result<()> {
crate::sign::sign(path, signer)
}
pub fn verify(path: &Path) -> Result<Vec<Certificate>> {
crate::sign::verify(path)
}
pub fn entry_point(path: &Path) -> Result<EntryPoint> {
let manifest = xcommon::extract_zip_file(path, "AndroidManifest.xml")?;
let chunks = if let Chunk::Xml(chunks) = Chunk::parse(&mut Cursor::new(manifest))? {
chunks
} else {
anyhow::bail!("invalid manifest 0");
};
let strings = if let Chunk::StringPool(strings, _) = &chunks[0] {
strings
} else {
anyhow::bail!("invalid manifest 1");
};
let mut manifest = None;
let mut package = None;
let mut activity = None;
let mut name = None;
for (i, s) in strings.iter().enumerate() {
match s.as_str() {
"manifest" => {
manifest = Some(i as i32);
}
"package" => {
package = Some(i as i32);
}
"activity" => {
activity = Some(i as i32);
}
"name" => {
name = Some(i as i32);
}
_ => {}
}
}
let (manifest, package, activity, name) =
if let (Some(manifest), Some(package), Some(activity), Some(name)) =
(manifest, package, activity, name)
{
(manifest, package, activity, name)
} else {
anyhow::bail!("invalid manifest 2");
};
let mut package_value = None;
let mut name_value = None;
for chunk in &chunks[2..] {
if let Chunk::XmlStartElement(_, el, attrs) = chunk {
match el.name {
x if x == manifest => {
package_value = attrs
.iter()
.find(|attr| attr.name == package)
.map(|attr| attr.raw_value);
}
x if x == activity => {
if name_value.is_some() {
continue;
}
name_value = attrs
.iter()
.find(|attr| attr.name == name)
.map(|attr| attr.raw_value);
}
_ => {}
}
}
}
let entry = if let (Some(package_value), Some(name_value)) = (package_value, name_value) {
EntryPoint {
package: strings[package_value as usize].clone(),
activity: strings[name_value as usize].clone(),
}
} else {
anyhow::bail!("invalid manifest 3");
};
Ok(entry)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryPoint {
pub package: String,
pub activity: String,
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter};
pub fn init_logger() {
tracing_log::LogTracer::init().ok();
let env = std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_else(|_| "info".to_owned());
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_span_events(FmtSpan::ACTIVE | FmtSpan::CLOSE)
.with_env_filter(EnvFilter::new(env))
.with_writer(std::io::stderr)
.finish();
tracing::subscriber::set_global_default(subscriber).ok();
}
pub fn find_android_jar() -> Result<PathBuf> {
let home = std::env::var("ANDROID_HOME")?;
let platforms = Path::new(&home).join("platforms");
let mut jar = None;
for entry in std::fs::read_dir(platforms)? {
let android = entry?.path().join("android.jar");
if android.exists() {
jar = Some(android);
break;
}
}
Ok(jar.unwrap())
}
pub fn android_jar(platform: u16) -> Result<PathBuf> {
let home = std::env::var("ANDROID_HOME")?;
let android = Path::new(&home)
.join("platforms")
.join(format!("android-{}", platform))
.join("android.jar");
Ok(android)
}
}