1use std::fmt;
27
28use crate::android::{AttrDef, PLATFORM_API_LEVEL, attr};
29use crate::error::Error;
30use crate::res::Value;
31use crate::types::{ApiLevel, ApiLevelError, ClassName, PackageName, PermissionName, VersionCode};
32use crate::xml::{Document, Element};
33
34#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
36#[non_exhaustive]
37pub enum ManifestError {
38 #[error("the root element is <{root}>, not <manifest>")]
39 NotAManifest { root: String },
40 #[error("<manifest> has no package attribute")]
41 MissingPackage,
42 #[error("android:{attribute} has invalid SDK version {value:?}")]
43 InvalidSdkVersion { attribute: &'static str, value: String },
44}
45
46#[derive(Clone, Debug, Eq, Hash, PartialEq)]
48pub enum SdkVersion {
49 Level(u32),
50 Codename(String),
51}
52
53impl SdkVersion {
54 pub fn api_level(&self) -> Result<ApiLevel, ApiLevelError> {
57 match self {
58 Self::Level(level) => ApiLevel::try_new(*level),
59 Self::Codename(_) => ApiLevel::try_new(PLATFORM_API_LEVEL + 1),
60 }
61 }
62
63 fn from_value(value: &Value) -> Option<Self> {
64 match value {
65 Value::String(text) => Some(text.parse().map_or_else(|_| Self::Codename(text.clone()), Self::Level)),
66 value => value.as_int().and_then(|i| u32::try_from(i).ok()).map(Self::Level),
67 }
68 }
69}
70
71impl fmt::Display for SdkVersion {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::Level(level) => write!(f, "{level}"),
75 Self::Codename(codename) => f.write_str(codename),
76 }
77 }
78}
79
80#[derive(Clone, Debug, PartialEq)]
82pub struct Manifest {
83 document: Document,
84}
85
86const NATIVE_ACTIVITY: &str = "android.app.NativeActivity";
88const LIB_NAME: &str = "android.app.lib_name";
89
90impl Manifest {
91 pub fn new(package: &PackageName) -> Self {
94 let uses_sdk = Element::new("uses-sdk")
95 .with_attr(attr::MIN_SDK_VERSION, ApiLevel::N.get() as i32)
96 .with_attr(attr::TARGET_SDK_VERSION, ApiLevel::BAKLAVA.get() as i32);
97 let root = Element::new("manifest")
98 .with_android_namespace()
99 .with_attr("package", package.as_str())
100 .with_attr(attr::VERSION_CODE, VersionCode::ONE.get() as i32)
101 .with_child(uses_sdk)
102 .with_child(Element::new("application"));
103 Self { document: Document::new(root) }
104 }
105
106 pub fn parse(text: &str) -> Result<Self, Error> {
108 Ok(Self::from_document(Document::parse(text)?)?)
109 }
110
111 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
113 Ok(Self::from_document(Document::decode(bytes)?)?)
114 }
115
116 pub fn from_document(document: Document) -> Result<Self, ManifestError> {
119 let manifest = Self { document };
120 manifest.validate()?;
121 Ok(manifest)
122 }
123
124 pub fn validate(&self) -> Result<(), ManifestError> {
126 if self.root().name != "manifest" {
127 return Err(ManifestError::NotAManifest { root: self.root().name.clone() });
128 }
129 self.package().ok_or(ManifestError::MissingPackage)?;
130 if let Some(sdk) = self.root().element("uses-sdk") {
131 for def in [attr::MIN_SDK_VERSION, attr::TARGET_SDK_VERSION, attr::MAX_SDK_VERSION] {
132 let Some(value) = sdk.android_attr(def) else { continue };
133 let valid = match value {
134 Value::Int(level) => ApiLevel::try_new(*level as u32).is_ok(),
135 Value::Hex(level) => ApiLevel::try_new(*level).is_ok(),
136 Value::String(text) => match text.parse::<u32>() {
137 Ok(level) => ApiLevel::try_new(level).is_ok(),
138 Err(_) => def != attr::MAX_SDK_VERSION && is_codename(text),
139 },
140 _ => false,
141 };
142 if !valid {
143 return Err(ManifestError::InvalidSdkVersion { attribute: def.name(), value: value.to_string() });
144 }
145 }
146 }
147 Ok(())
148 }
149
150 pub fn document(&self) -> &Document {
151 &self.document
152 }
153
154 pub fn into_document(self) -> Document {
155 self.document
156 }
157
158 pub fn root(&self) -> &Element {
160 &self.document.root
161 }
162
163 pub fn root_mut(&mut self) -> &mut Element {
165 &mut self.document.root
166 }
167
168 pub fn package(&self) -> Option<&str> {
170 self.root().attr(None, "package").and_then(Value::as_str)
171 }
172
173 pub fn version_code(&self) -> Option<u32> {
174 self.root().android_attr(attr::VERSION_CODE)?.as_int().and_then(|v| u32::try_from(v).ok())
175 }
176
177 pub fn version_name(&self) -> Option<&str> {
178 android_str(self.root(), attr::VERSION_NAME)
179 }
180
181 pub fn min_sdk(&self) -> SdkVersion {
183 self.sdk(attr::MIN_SDK_VERSION).unwrap_or(SdkVersion::Level(1))
184 }
185
186 pub fn target_sdk(&self) -> Option<SdkVersion> {
187 self.sdk(attr::TARGET_SDK_VERSION)
188 }
189
190 pub fn max_sdk(&self) -> Option<u32> {
191 match self.sdk(attr::MAX_SDK_VERSION)? {
192 SdkVersion::Level(level) => Some(level),
193 SdkVersion::Codename(_) => None,
194 }
195 }
196
197 fn sdk(&self, def: &AttrDef) -> Option<SdkVersion> {
198 SdkVersion::from_value(self.root().element("uses-sdk")?.android_attr(def)?)
199 }
200
201 pub fn effective_target_sdk(&self) -> Result<ApiLevel, ApiLevelError> {
203 self.target_sdk().unwrap_or_else(|| self.min_sdk()).api_level()
204 }
205
206 pub fn permissions(&self) -> impl Iterator<Item = &str> {
208 let elements =
209 self.root().elements().filter(|e| matches!(e.name.as_str(), "uses-permission" | "uses-permission-sdk-23"));
210 elements.filter_map(|e| android_str(e, attr::NAME))
211 }
212
213 pub fn features(&self) -> impl Iterator<Item = &str> {
215 self.root().elements_named("uses-feature").filter_map(|e| android_str(e, attr::NAME))
216 }
217
218 pub fn application(&self) -> Option<&Element> {
219 self.root().element("application")
220 }
221
222 pub fn application_mut(&mut self) -> &mut Element {
224 let root = self.root_mut();
225 if root.element("application").is_none() {
226 root.push_child(Element::new("application"));
227 }
228 root.element_mut("application").expect("ensured above")
229 }
230
231 fn application_bool(&self, def: &AttrDef) -> Option<bool> {
232 self.application()?.android_attr(def)?.as_bool()
233 }
234
235 pub fn has_code(&self) -> bool {
237 self.application_bool(attr::HAS_CODE).unwrap_or(true)
238 }
239
240 pub fn debuggable(&self) -> bool {
241 self.application_bool(attr::DEBUGGABLE).unwrap_or(false)
242 }
243
244 pub fn extract_native_libs(&self) -> Option<bool> {
245 self.application_bool(attr::EXTRACT_NATIVE_LIBS)
246 }
247
248 pub fn label(&self) -> Option<&Value> {
250 self.application()?.android_attr(attr::LABEL)
251 }
252
253 pub fn activities(&self) -> impl Iterator<Item = &Element> {
254 self.application().into_iter().flat_map(|application| application.elements_named("activity"))
255 }
256
257 pub fn components(&self) -> impl Iterator<Item = &Element> {
260 let application = self.application().into_iter().flat_map(Element::elements);
261 application.filter(|e| matches!(e.name.as_str(), "activity" | "activity-alias" | "service" | "receiver"))
262 }
263
264 pub fn launcher_activity(&self) -> Option<&str> {
266 let launcher =
267 self.components().filter(|c| matches!(c.name.as_str(), "activity" | "activity-alias")).find(|activity| {
268 activity.elements_named("intent-filter").any(|filter| {
269 let has = |tag, name| filter.elements_named(tag).any(|e| android_str(e, attr::NAME) == Some(name));
270 has("action", "android.intent.action.MAIN") && has("category", "android.intent.category.LAUNCHER")
271 })
272 });
273 android_str(launcher?, attr::NAME)
274 }
275
276 pub fn native_activity_library(&self) -> Option<Result<&str, &Value>> {
283 let activity = self.activities().find(|a| android_str(a, attr::NAME) == Some(NATIVE_ACTIVITY))?;
284 let meta = activity.elements_named("meta-data").find(|m| android_str(m, attr::NAME) == Some(LIB_NAME));
285 Some(match meta.and_then(|meta| meta.android_attr(attr::VALUE)) {
286 Some(Value::String(library)) => Ok(library),
287 Some(value @ (Value::Reference(_) | Value::Attribute(_))) => Err(value),
288 _ => Ok("main"),
290 })
291 }
292
293 pub fn set_version(&mut self, code: VersionCode, name: &str) -> &mut Self {
294 self.root_mut().set_attr(attr::VERSION_CODE, code.get() as i32).set_attr(attr::VERSION_NAME, name);
295 self
296 }
297
298 pub fn set_min_sdk(&mut self, level: ApiLevel) -> &mut Self {
299 self.uses_sdk_mut().set_attr(attr::MIN_SDK_VERSION, level.get() as i32);
300 self
301 }
302
303 pub fn set_target_sdk(&mut self, level: ApiLevel) -> &mut Self {
304 self.uses_sdk_mut().set_attr(attr::TARGET_SDK_VERSION, level.get() as i32);
305 self
306 }
307
308 fn uses_sdk_mut(&mut self) -> &mut Element {
310 let root = self.root_mut();
311 if root.element("uses-sdk").is_none() {
312 root.children.insert(0, crate::xml::Node::Element(Element::new("uses-sdk")));
313 }
314 root.element_mut("uses-sdk").expect("ensured above")
315 }
316
317 pub fn add_permission(&mut self, permission: &PermissionName) -> &mut Self {
319 if !self.permissions().any(|p| p == permission.as_str()) {
320 let element = Element::new("uses-permission").with_attr(attr::NAME, permission.as_str());
321 self.root_mut().push_child(element);
322 }
323 self
324 }
325}
326
327impl fmt::Display for Manifest {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 self.document.fmt(f)
330 }
331}
332
333fn android_str<'e>(element: &'e Element, def: &AttrDef) -> Option<&'e str> {
335 element.android_attr(def)?.as_str()
336}
337
338fn is_codename(text: &str) -> bool {
339 let mut bytes = text.bytes();
340 bytes.next().is_some_and(|b| b.is_ascii_alphabetic()) && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
341}
342
343pub fn activity(name: &ClassName) -> Element {
345 Element::new("activity").with_attr(attr::NAME, name.as_str())
346}
347
348pub fn launcher_filter() -> Element {
351 Element::new("intent-filter")
352 .with_child(Element::new("action").with_attr(attr::NAME, "android.intent.action.MAIN"))
353 .with_child(Element::new("category").with_attr(attr::NAME, "android.intent.category.LAUNCHER"))
354}
355
356pub fn meta_data(name: &str, value: impl Into<Value>) -> Element {
358 Element::new("meta-data").with_attr(attr::NAME, name).with_attr(attr::VALUE, value)
359}
360
361pub fn native_activity(library: &str) -> Element {
364 const CONFIG_CHANGES: &str =
365 "orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|uiMode|density";
366 let config_changes = attr::CONFIG_CHANGES.parse(CONFIG_CHANGES).expect("framework configChanges symbols");
367 activity(&ClassName::native_activity())
368 .with_attr(attr::EXPORTED, true)
369 .with_attr(attr::CONFIG_CHANGES, config_changes)
370 .with_child(meta_data(LIB_NAME, library))
371 .with_child(launcher_filter())
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn sample() -> Manifest {
379 let mut manifest = Manifest::new(&PackageName::try_new("org.example.game").unwrap());
380 manifest
381 .set_version(VersionCode::try_new(3).unwrap(), "1.0")
382 .add_permission(&PermissionName::android("INTERNET").unwrap())
383 .add_permission(&PermissionName::android("INTERNET").unwrap());
384 manifest.application_mut().set_attr(attr::HAS_CODE, false).push_child(native_activity("game"));
385 manifest
386 }
387
388 #[test]
389 fn reads_back_what_it_sets() {
390 let manifest = sample();
391 assert_eq!(manifest.package(), Some("org.example.game"));
392 assert_eq!((manifest.version_code(), manifest.version_name()), (Some(3), Some("1.0")));
393 assert_eq!((manifest.min_sdk(), manifest.target_sdk()), (SdkVersion::Level(24), Some(SdkVersion::Level(36))));
394 assert_eq!(manifest.permissions().collect::<Vec<_>>(), ["android.permission.INTERNET"]);
395 assert_eq!(manifest.launcher_activity(), Some(NATIVE_ACTIVITY));
396 assert_eq!(manifest.native_activity_library(), Some(Ok("game")));
397 assert!(!manifest.has_code());
398 }
399
400 #[test]
401 fn round_trips_through_binary_and_text_xml() {
402 let manifest = sample();
403 let decoded = Manifest::decode(&manifest.document().encode(&()).unwrap()).unwrap();
404 assert_eq!(decoded, manifest);
405 let printed = decoded.to_string();
406 assert!(printed.contains(r#"android:configChanges="density|keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode""#), "{printed}");
407 assert_eq!(Manifest::parse(&printed).unwrap(), manifest);
408 }
409
410 #[test]
411 fn native_activities_default_to_libmain() {
412 let mut manifest = Manifest::new(&PackageName::try_new("org.example.app").unwrap());
413 manifest.application_mut().push_child(activity(&ClassName::native_activity()));
414 assert_eq!(manifest.native_activity_library(), Some(Ok("main")));
415 let library: Value = "@string/library".parse::<crate::res::ResourceRef>().unwrap().into();
416 let activity = manifest.application_mut().element_mut("activity").unwrap();
417 activity.push_child(meta_data(LIB_NAME, library.clone()));
418 assert_eq!(manifest.native_activity_library(), Some(Err(&library)));
419 }
420
421 #[test]
422 fn launcher_aliases_are_reported_by_their_component_name() {
423 let mut manifest = Manifest::new(&PackageName::try_new("org.example.alias").unwrap());
424 manifest.application_mut().push_child(activity(&".MainActivity".parse().unwrap())).push_child(
425 Element::new("activity-alias")
426 .with_attr(attr::NAME, ".Launcher")
427 .with_attr(attr::TARGET_ACTIVITY, ".MainActivity")
428 .with_child(launcher_filter()),
429 );
430 assert_eq!(manifest.activities().count(), 1);
431 assert_eq!(manifest.launcher_activity(), Some(".Launcher"));
432 }
433
434 #[test]
435 fn rejects_documents_that_are_not_manifests() {
436 let not_manifest = Document::new(Element::new("resources"));
437 assert_eq!(
438 Manifest::from_document(not_manifest),
439 Err(ManifestError::NotAManifest { root: "resources".into() })
440 );
441 assert_eq!(
442 Manifest::from_document(Document::new(Element::new("manifest"))),
443 Err(ManifestError::MissingPackage)
444 );
445 }
446
447 #[test]
448 fn invalid_sdk_values_never_fall_back_to_defaults_or_preview_levels() {
449 for attribute in [attr::MIN_SDK_VERSION, attr::TARGET_SDK_VERSION, attr::MAX_SDK_VERSION] {
450 for value in [Value::Int(-1), Value::Int(0), Value::Hex(u32::MAX), Value::String("-1".into()), Value::Null]
451 {
452 let mut manifest = sample();
453 manifest.uses_sdk_mut().set_attr(attribute, value);
454 assert!(matches!(
455 Manifest::from_document(manifest.into_document()),
456 Err(ManifestError::InvalidSdkVersion { attribute: found, .. }) if found == attribute.name()
457 ));
458 }
459 }
460 let mut preview = sample();
461 preview.uses_sdk_mut().set_attr(attr::MIN_SDK_VERSION, "VanillaIceCream");
462 preview.validate().unwrap();
463 assert!(matches!(preview.min_sdk(), SdkVersion::Codename(_)));
464 }
465}