use std::fmt;
use crate::android::{AttrDef, PLATFORM_API_LEVEL, attr};
use crate::error::Error;
use crate::res::Value;
use crate::types::{ApiLevel, ApiLevelError, ClassName, PackageName, PermissionName, VersionCode};
use crate::xml::{Document, Element};
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ManifestError {
#[error("the root element is <{root}>, not <manifest>")]
NotAManifest { root: String },
#[error("<manifest> has no package attribute")]
MissingPackage,
#[error("android:{attribute} has invalid SDK version {value:?}")]
InvalidSdkVersion { attribute: &'static str, value: String },
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum SdkVersion {
Level(u32),
Codename(String),
}
impl SdkVersion {
pub fn api_level(&self) -> Result<ApiLevel, ApiLevelError> {
match self {
Self::Level(level) => ApiLevel::try_new(*level),
Self::Codename(_) => ApiLevel::try_new(PLATFORM_API_LEVEL + 1),
}
}
fn from_value(value: &Value) -> Option<Self> {
match value {
Value::String(text) => Some(text.parse().map_or_else(|_| Self::Codename(text.clone()), Self::Level)),
value => value.as_int().and_then(|i| u32::try_from(i).ok()).map(Self::Level),
}
}
}
impl fmt::Display for SdkVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Level(level) => write!(f, "{level}"),
Self::Codename(codename) => f.write_str(codename),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Manifest {
document: Document,
}
const NATIVE_ACTIVITY: &str = "android.app.NativeActivity";
const LIB_NAME: &str = "android.app.lib_name";
impl Manifest {
pub fn new(package: &PackageName) -> Self {
let uses_sdk = Element::new("uses-sdk")
.with_attr(attr::MIN_SDK_VERSION, ApiLevel::N.get() as i32)
.with_attr(attr::TARGET_SDK_VERSION, ApiLevel::BAKLAVA.get() as i32);
let root = Element::new("manifest")
.with_android_namespace()
.with_attr("package", package.as_str())
.with_attr(attr::VERSION_CODE, VersionCode::ONE.get() as i32)
.with_child(uses_sdk)
.with_child(Element::new("application"));
Self { document: Document::new(root) }
}
pub fn parse(text: &str) -> Result<Self, Error> {
Ok(Self::from_document(Document::parse(text)?)?)
}
pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
Ok(Self::from_document(Document::decode(bytes)?)?)
}
pub fn from_document(document: Document) -> Result<Self, ManifestError> {
let manifest = Self { document };
manifest.validate()?;
Ok(manifest)
}
pub fn validate(&self) -> Result<(), ManifestError> {
if self.root().name != "manifest" {
return Err(ManifestError::NotAManifest { root: self.root().name.clone() });
}
self.package().ok_or(ManifestError::MissingPackage)?;
if let Some(sdk) = self.root().element("uses-sdk") {
for def in [attr::MIN_SDK_VERSION, attr::TARGET_SDK_VERSION, attr::MAX_SDK_VERSION] {
let Some(value) = sdk.android_attr(def) else { continue };
let valid = match value {
Value::Int(level) => ApiLevel::try_new(*level as u32).is_ok(),
Value::Hex(level) => ApiLevel::try_new(*level).is_ok(),
Value::String(text) => match text.parse::<u32>() {
Ok(level) => ApiLevel::try_new(level).is_ok(),
Err(_) => def != attr::MAX_SDK_VERSION && is_codename(text),
},
_ => false,
};
if !valid {
return Err(ManifestError::InvalidSdkVersion { attribute: def.name(), value: value.to_string() });
}
}
}
Ok(())
}
pub fn document(&self) -> &Document {
&self.document
}
pub fn into_document(self) -> Document {
self.document
}
pub fn root(&self) -> &Element {
&self.document.root
}
pub fn root_mut(&mut self) -> &mut Element {
&mut self.document.root
}
pub fn package(&self) -> Option<&str> {
self.root().attr(None, "package").and_then(Value::as_str)
}
pub fn version_code(&self) -> Option<u32> {
self.root().android_attr(attr::VERSION_CODE)?.as_int().and_then(|v| u32::try_from(v).ok())
}
pub fn version_name(&self) -> Option<&str> {
android_str(self.root(), attr::VERSION_NAME)
}
pub fn min_sdk(&self) -> SdkVersion {
self.sdk(attr::MIN_SDK_VERSION).unwrap_or(SdkVersion::Level(1))
}
pub fn target_sdk(&self) -> Option<SdkVersion> {
self.sdk(attr::TARGET_SDK_VERSION)
}
pub fn max_sdk(&self) -> Option<u32> {
match self.sdk(attr::MAX_SDK_VERSION)? {
SdkVersion::Level(level) => Some(level),
SdkVersion::Codename(_) => None,
}
}
fn sdk(&self, def: &AttrDef) -> Option<SdkVersion> {
SdkVersion::from_value(self.root().element("uses-sdk")?.android_attr(def)?)
}
pub fn effective_target_sdk(&self) -> Result<ApiLevel, ApiLevelError> {
self.target_sdk().unwrap_or_else(|| self.min_sdk()).api_level()
}
pub fn permissions(&self) -> impl Iterator<Item = &str> {
let elements =
self.root().elements().filter(|e| matches!(e.name.as_str(), "uses-permission" | "uses-permission-sdk-23"));
elements.filter_map(|e| android_str(e, attr::NAME))
}
pub fn features(&self) -> impl Iterator<Item = &str> {
self.root().elements_named("uses-feature").filter_map(|e| android_str(e, attr::NAME))
}
pub fn application(&self) -> Option<&Element> {
self.root().element("application")
}
pub fn application_mut(&mut self) -> &mut Element {
let root = self.root_mut();
if root.element("application").is_none() {
root.push_child(Element::new("application"));
}
root.element_mut("application").expect("ensured above")
}
fn application_bool(&self, def: &AttrDef) -> Option<bool> {
self.application()?.android_attr(def)?.as_bool()
}
pub fn has_code(&self) -> bool {
self.application_bool(attr::HAS_CODE).unwrap_or(true)
}
pub fn debuggable(&self) -> bool {
self.application_bool(attr::DEBUGGABLE).unwrap_or(false)
}
pub fn extract_native_libs(&self) -> Option<bool> {
self.application_bool(attr::EXTRACT_NATIVE_LIBS)
}
pub fn label(&self) -> Option<&Value> {
self.application()?.android_attr(attr::LABEL)
}
pub fn activities(&self) -> impl Iterator<Item = &Element> {
self.application().into_iter().flat_map(|application| application.elements_named("activity"))
}
pub fn components(&self) -> impl Iterator<Item = &Element> {
let application = self.application().into_iter().flat_map(Element::elements);
application.filter(|e| matches!(e.name.as_str(), "activity" | "activity-alias" | "service" | "receiver"))
}
pub fn launcher_activity(&self) -> Option<&str> {
let launcher =
self.components().filter(|c| matches!(c.name.as_str(), "activity" | "activity-alias")).find(|activity| {
activity.elements_named("intent-filter").any(|filter| {
let has = |tag, name| filter.elements_named(tag).any(|e| android_str(e, attr::NAME) == Some(name));
has("action", "android.intent.action.MAIN") && has("category", "android.intent.category.LAUNCHER")
})
});
android_str(launcher?, attr::NAME)
}
pub fn native_activity_library(&self) -> Option<Result<&str, &Value>> {
let activity = self.activities().find(|a| android_str(a, attr::NAME) == Some(NATIVE_ACTIVITY))?;
let meta = activity.elements_named("meta-data").find(|m| android_str(m, attr::NAME) == Some(LIB_NAME));
Some(match meta.and_then(|meta| meta.android_attr(attr::VALUE)) {
Some(Value::String(library)) => Ok(library),
Some(value @ (Value::Reference(_) | Value::Attribute(_))) => Err(value),
_ => Ok("main"),
})
}
pub fn set_version(&mut self, code: VersionCode, name: &str) -> &mut Self {
self.root_mut().set_attr(attr::VERSION_CODE, code.get() as i32).set_attr(attr::VERSION_NAME, name);
self
}
pub fn set_min_sdk(&mut self, level: ApiLevel) -> &mut Self {
self.uses_sdk_mut().set_attr(attr::MIN_SDK_VERSION, level.get() as i32);
self
}
pub fn set_target_sdk(&mut self, level: ApiLevel) -> &mut Self {
self.uses_sdk_mut().set_attr(attr::TARGET_SDK_VERSION, level.get() as i32);
self
}
fn uses_sdk_mut(&mut self) -> &mut Element {
let root = self.root_mut();
if root.element("uses-sdk").is_none() {
root.children.insert(0, crate::xml::Node::Element(Element::new("uses-sdk")));
}
root.element_mut("uses-sdk").expect("ensured above")
}
pub fn add_permission(&mut self, permission: &PermissionName) -> &mut Self {
if !self.permissions().any(|p| p == permission.as_str()) {
let element = Element::new("uses-permission").with_attr(attr::NAME, permission.as_str());
self.root_mut().push_child(element);
}
self
}
}
impl fmt::Display for Manifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.document.fmt(f)
}
}
fn android_str<'e>(element: &'e Element, def: &AttrDef) -> Option<&'e str> {
element.android_attr(def)?.as_str()
}
fn is_codename(text: &str) -> bool {
let mut bytes = text.bytes();
bytes.next().is_some_and(|b| b.is_ascii_alphabetic()) && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
pub fn activity(name: &ClassName) -> Element {
Element::new("activity").with_attr(attr::NAME, name.as_str())
}
pub fn launcher_filter() -> Element {
Element::new("intent-filter")
.with_child(Element::new("action").with_attr(attr::NAME, "android.intent.action.MAIN"))
.with_child(Element::new("category").with_attr(attr::NAME, "android.intent.category.LAUNCHER"))
}
pub fn meta_data(name: &str, value: impl Into<Value>) -> Element {
Element::new("meta-data").with_attr(attr::NAME, name).with_attr(attr::VALUE, value)
}
pub fn native_activity(library: &str) -> Element {
const CONFIG_CHANGES: &str =
"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|uiMode|density";
let config_changes = attr::CONFIG_CHANGES.parse(CONFIG_CHANGES).expect("framework configChanges symbols");
activity(&ClassName::native_activity())
.with_attr(attr::EXPORTED, true)
.with_attr(attr::CONFIG_CHANGES, config_changes)
.with_child(meta_data(LIB_NAME, library))
.with_child(launcher_filter())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Manifest {
let mut manifest = Manifest::new(&PackageName::try_new("org.example.game").unwrap());
manifest
.set_version(VersionCode::try_new(3).unwrap(), "1.0")
.add_permission(&PermissionName::android("INTERNET").unwrap())
.add_permission(&PermissionName::android("INTERNET").unwrap());
manifest.application_mut().set_attr(attr::HAS_CODE, false).push_child(native_activity("game"));
manifest
}
#[test]
fn reads_back_what_it_sets() {
let manifest = sample();
assert_eq!(manifest.package(), Some("org.example.game"));
assert_eq!((manifest.version_code(), manifest.version_name()), (Some(3), Some("1.0")));
assert_eq!((manifest.min_sdk(), manifest.target_sdk()), (SdkVersion::Level(24), Some(SdkVersion::Level(36))));
assert_eq!(manifest.permissions().collect::<Vec<_>>(), ["android.permission.INTERNET"]);
assert_eq!(manifest.launcher_activity(), Some(NATIVE_ACTIVITY));
assert_eq!(manifest.native_activity_library(), Some(Ok("game")));
assert!(!manifest.has_code());
}
#[test]
fn round_trips_through_binary_and_text_xml() {
let manifest = sample();
let decoded = Manifest::decode(&manifest.document().encode(&()).unwrap()).unwrap();
assert_eq!(decoded, manifest);
let printed = decoded.to_string();
assert!(printed.contains(r#"android:configChanges="density|keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode""#), "{printed}");
assert_eq!(Manifest::parse(&printed).unwrap(), manifest);
}
#[test]
fn native_activities_default_to_libmain() {
let mut manifest = Manifest::new(&PackageName::try_new("org.example.app").unwrap());
manifest.application_mut().push_child(activity(&ClassName::native_activity()));
assert_eq!(manifest.native_activity_library(), Some(Ok("main")));
let library: Value = "@string/library".parse::<crate::res::ResourceRef>().unwrap().into();
let activity = manifest.application_mut().element_mut("activity").unwrap();
activity.push_child(meta_data(LIB_NAME, library.clone()));
assert_eq!(manifest.native_activity_library(), Some(Err(&library)));
}
#[test]
fn launcher_aliases_are_reported_by_their_component_name() {
let mut manifest = Manifest::new(&PackageName::try_new("org.example.alias").unwrap());
manifest.application_mut().push_child(activity(&".MainActivity".parse().unwrap())).push_child(
Element::new("activity-alias")
.with_attr(attr::NAME, ".Launcher")
.with_attr(attr::TARGET_ACTIVITY, ".MainActivity")
.with_child(launcher_filter()),
);
assert_eq!(manifest.activities().count(), 1);
assert_eq!(manifest.launcher_activity(), Some(".Launcher"));
}
#[test]
fn rejects_documents_that_are_not_manifests() {
let not_manifest = Document::new(Element::new("resources"));
assert_eq!(
Manifest::from_document(not_manifest),
Err(ManifestError::NotAManifest { root: "resources".into() })
);
assert_eq!(
Manifest::from_document(Document::new(Element::new("manifest"))),
Err(ManifestError::MissingPackage)
);
}
#[test]
fn invalid_sdk_values_never_fall_back_to_defaults_or_preview_levels() {
for attribute in [attr::MIN_SDK_VERSION, attr::TARGET_SDK_VERSION, attr::MAX_SDK_VERSION] {
for value in [Value::Int(-1), Value::Int(0), Value::Hex(u32::MAX), Value::String("-1".into()), Value::Null]
{
let mut manifest = sample();
manifest.uses_sdk_mut().set_attr(attribute, value);
assert!(matches!(
Manifest::from_document(manifest.into_document()),
Err(ManifestError::InvalidSdkVersion { attribute: found, .. }) if found == attribute.name()
));
}
}
let mut preview = sample();
preview.uses_sdk_mut().set_attr(attr::MIN_SDK_VERSION, "VanillaIceCream");
preview.validate().unwrap();
assert!(matches!(preview.min_sdk(), SdkVersion::Codename(_)));
}
}