Skip to main content

apk/manifest/
mod.rs

1//! `AndroidManifest.xml`.
2//!
3//! A [`Manifest`] is the XML [`Document`] itself, with typed accessors for
4//! the facts tools need and setters for the ones builders change. There is
5//! no second schema to keep in step with Android: anything else is edited
6//! through the element tree, with framework attributes typed by the
7//! platform's own definitions ([`AttrDef::parse`]).
8//!
9//! ```
10//! use apk::android::attr;
11//! use apk::manifest::{self, Manifest};
12//! use apk::{ClassName, PackageName};
13//!
14//! let mut manifest = Manifest::new(&PackageName::try_new("org.example.game")?);
15//! manifest.add_permission(&"android.permission.INTERNET".parse()?);
16//! let activity = manifest::activity(&ClassName::native_activity())
17//!     .with_attr(attr::EXPORTED, true)
18//!     .with_attr(attr::SCREEN_ORIENTATION, attr::SCREEN_ORIENTATION.parse("landscape")?)
19//!     .with_child(manifest::launcher_filter());
20//! manifest.application_mut().push_child(activity);
21//! assert_eq!(manifest.launcher_activity(), Some("android.app.NativeActivity"));
22//! println!("{manifest}");
23//! # Ok::<(), apk::Error>(())
24//! ```
25
26use 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/// Why a document is not a usable manifest.
35#[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/// An SDK version attribute: an API level, or the codename of a preview SDK.
47#[derive(Clone, Debug, Eq, Hash, PartialEq)]
48pub enum SdkVersion {
49    Level(u32),
50    Codename(String),
51}
52
53impl SdkVersion {
54    /// The API level, treating a preview codename as the release after the
55    /// newest one this crate knows, as `apksigner` does.
56    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/// An `AndroidManifest.xml` document.
81#[derive(Clone, Debug, PartialEq)]
82pub struct Manifest {
83    document: Document,
84}
85
86/// The activity that hosts a native library, and the meta-data naming it.
87const NATIVE_ACTIVITY: &str = "android.app.NativeActivity";
88const LIB_NAME: &str = "android.app.lib_name";
89
90impl Manifest {
91    /// A manifest for `package` with version code 1, `minSdkVersion` 24,
92    /// `targetSdkVersion` 36, and an empty `<application>`.
93    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    /// Parses a text manifest, typing attributes as aapt2 does.
107    pub fn parse(text: &str) -> Result<Self, Error> {
108        Ok(Self::from_document(Document::parse(text)?)?)
109    }
110
111    /// Decodes a compiled (binary XML) manifest.
112    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
113        Ok(Self::from_document(Document::decode(bytes)?)?)
114    }
115
116    /// Checks that `document` is a manifest: its root is `<manifest>` with a
117    /// `package`.
118    pub fn from_document(document: Document) -> Result<Self, ManifestError> {
119        let manifest = Self { document };
120        manifest.validate()?;
121        Ok(manifest)
122    }
123
124    /// Checks the root, package presence, and SDK values after tree edits.
125    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    /// The `<manifest>` element.
159    pub fn root(&self) -> &Element {
160        &self.document.root
161    }
162
163    /// The `<manifest>` element, for edits without a dedicated method.
164    pub fn root_mut(&mut self) -> &mut Element {
165        &mut self.document.root
166    }
167
168    /// The application ID (`package`).
169    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    /// `minSdkVersion`, which defaults to 1.
182    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    /// The target API level, which defaults to the minimum.
202    pub fn effective_target_sdk(&self) -> Result<ApiLevel, ApiLevelError> {
203        self.target_sdk().unwrap_or_else(|| self.min_sdk()).api_level()
204    }
205
206    /// The requested permissions, including `uses-permission-sdk-23`.
207    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    /// The named features (`uses-feature`) the app declares.
214    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    /// The `<application>` element, which is added if missing.
223    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    /// Whether the app has DEX code (`android:hasCode`, default `true`).
236    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    /// The application label: a string, or a reference to a string resource.
249    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    /// The components that can declare intent filters: activities, activity
258    /// aliases, services, and receivers.
259    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    /// The activity or alias started from the launcher (`MAIN` and `LAUNCHER`).
265    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    /// The library a `NativeActivity` loads, if the app has one: the string
277    /// value of its `android.app.lib_name` meta-data, or `main`, as
278    /// `NativeActivity` chooses it.
279    ///
280    /// `Err` holds a reference, which names the library only once Android
281    /// resolves it.
282    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            // `getString` reads anything else as absent.
289            _ => 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    /// The `<uses-sdk>` element, which is added first if missing.
309    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    /// Requests `permission`, unless it already is.
318    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
333/// The string value of framework attribute `def` on `element`.
334fn 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
343/// `<activity android:name="...">`.
344pub fn activity(name: &ClassName) -> Element {
345    Element::new("activity").with_attr(attr::NAME, name.as_str())
346}
347
348/// The intent filter of the activity the launcher starts: action `MAIN`,
349/// category `LAUNCHER`.
350pub 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
356/// `<meta-data android:name="..." android:value="...">`.
357pub 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
361/// A launchable `NativeActivity` that loads `lib<library>.so`, handling the
362/// configuration changes a full-screen renderer usually handles itself.
363pub 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}