Lesson 10 / 15
The Manifest File
The manifest file's role in introducing the app to the platform, and a model of merging that asks, for each element, whether what was wanted is preserved.
Published Updated
In this lesson
- Explains that the manifest file declares the app components, permissions and hardware features to the platform
- Distinguishes whether an element in manifest merging ends as the wanted value preserved, overwritten, or a conflict
- Measures that the tools:replace marker puts the given value in place of the wanted one
- Verifies with the bounding measurement that a placeholder is pending until its value arrives and full once it has arrived
Contents
The previous lesson, Tasks and the Back Stack, measured which task and which position a screen lands in. That screen cannot be started at all unless it has first been introduced to the platform as an app component. The place for that introduction is the manifest file. This lesson measures what is written in the manifest file and how manifest files coming from several sources merge into a single file.
The measurement axis of the course is the same as in the opening lesson: how does the platform fulfill this request. There are four fulfillments: full, substitute, pending and unmet. The rule, not a label, gives the fulfillment: pending and unmet come directly from the outcome, and a given value that equals the wanted one is full, while one that differs is substitute. The file below carries that rule and is repeated verbatim in every lesson. The binding count of 3/2/2/1 from the opening lesson is not changed here; it is only referred to.
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
data class Given(val value: String) : Outcome
data class Waiting(val on: String) : Outcome
data class Refused(val reason: String) : Outcome
}
class Ask(val label: String, val wanted: String, val outcome: Outcome) {
val fulfillment: String = when (outcome) {
is Outcome.Waiting -> "pending"
is Outcome.Refused -> "unmet"
is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
}
fun report() {
val detail = when (outcome) {
is Outcome.Given -> "given=${outcome.value}"
is Outcome.Waiting -> "waiting-on=${outcome.on}"
is Outcome.Refused -> "reason=${outcome.reason}"
}
println("$label: wanted=$wanted | $detail -> $fulfillment")
}
}
fun tally(asks: List<Ask>) {
for (name in listOf("full", "substitute", "pending", "unmet")) {
println("$name: ${asks.count { it.fulfillment == name }}")
}
}
The Role of the Manifest File
The manifest file is the app’s written declaration to the platform. Before running the app, the platform looks at this file and learns four things. The first is the app identity: the package name that makes the app unique on the device. The second is the app components: screens, background jobs, broadcast receivers and content providers, and the platform does not recognize a component that is not declared here. The third is the permissions the app requests. The fourth is the hardware features the app needs; the camera, for example, is declared as required or optional with the uses-feature element. The fragment below is a small file carrying these four things.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-permission android:name="android.permission.CAMERA" /> <application android:label="Notes" android:allowBackup="true"> <activity android:name=".NoteListActivity" android:exported="true" /> </application> </manifest>
This fragment is in the platform’s format and is not run. The measurements that follow model the merging rule, not the file itself. The model builds this rule of the platform; it is not the platform’s real merging tool.
The Model of Merging
An app does not arrive with a single manifest file. The app module has its file; each library that is added brings its own; there can also be files specific to a build variant. The build task merges them into one file. When there is no conflict, the elements come together as they are. When the same attribute of the same element has different values in two files, the merge cannot resolve it on its own: unless the author marks the element in the app’s file with tools:replace and declares that it will overwrite, the merge stops with an error.
The model builds this as one request per element. The wanted value is the value the library wrote for the element. If the app has not touched that element, or has written the same value, the library’s value is preserved. If the app has written a different value and marked the element with tools:replace, the given value is the app’s. If there is no marker, the conflict stays and no value is given. mergeManifest applies this; only the app and the library are taken as layers.
// Merge.kt — a model of manifest merging
class Layer(val values: Map<String, String>, val replaced: Set<String> = emptySet())
fun mergeManifest(app: Layer, library: Layer): List<Ask> =
library.values.toSortedMap().map { (key, wanted) ->
val own = app.values[key]
val outcome = when {
own == null || own == wanted -> Outcome.Given(wanted)
key in app.replaced -> Outcome.Given(own)
else -> Outcome.Refused("unmarked-conflict")
}
Ask(key, wanted, outcome)
}
The keys have the form element.attribute and are sorted before they are printed. In the first measurement the library brings two elements: a hardware feature that declares the camera required, and an app attribute that supports right-to-left writing. The app does not touch either one; it writes only its own label and its own screen.
// Main.kt — merging without conflicts
fun main() {
val app = Layer(mapOf(
"application.label" to "Notes",
"activity.NoteListActivity" to "exported",
))
val library = Layer(mapOf(
"uses-feature.camera" to "required",
"application.supportsRtl" to "true",
))
val asks = mergeManifest(app, library)
for (ask in asks) ask.report()
tally(asks)
}
application.supportsRtl: wanted=true | given=true -> full uses-feature.camera: wanted=required | given=required -> full full: 2 substitute: 0 pending: 0 unmet: 0
Both elements were preserved as wanted. The measurement gives not only the full count but a line for each element: that a value the library wanted was not silently lost can be seen in the line.
PS1. In the first two measurements the model merges only two layers, the app and a single library; three layers are measured separately in the next section. How the order of precedence works when there are several libraries or a build variant file is not in these measurements.
PS2. Each element is represented by a single key and a single value. In a real manifest file an element has several attributes; the model counts them as separate keys.
Overwriting and Conflict
The second measurement contains conflicts. The library brings three elements. For application.label the app has written the same value. For application.allowBackup the library wants true, the app has written false, and the app has marked the element with tools:replace. For application.theme the values are also different, but the app has not marked the element.
// Main.kt — overwriting and conflict
fun main() {
val app = Layer(
values = mapOf(
"application.label" to "Notes",
"application.allowBackup" to "false",
"application.theme" to "Dark",
),
replaced = setOf("application.allowBackup"),
)
val library = Layer(mapOf(
"application.label" to "Notes",
"application.allowBackup" to "true",
"application.theme" to "Light",
))
val asks = mergeManifest(app, library)
for (ask in asks) ask.report()
tally(asks)
}
application.allowBackup: wanted=true | given=false -> substitute application.label: wanted=Notes | given=Notes -> full application.theme: wanted=Light | reason=unmarked-conflict -> unmet full: 1 substitute: 1 pending: 0 unmet: 1
The three elements fell into three different fulfillments. allowBackup is substitute: a value was given, but not the one the library wanted. The rule sees this from the difference between true and false; it does not know that the author did it on purpose. theme is unmet: no value was given, because an unmarked conflict stops the merge. What the rule sees is only the result and the equality; whether tools:replace is there affects the fulfillment not directly but through the given value.
M30/K02 asked how two definitions are resolved when they meet, and its permission lessons measured the meeting of the wanted and given sets. That is not the measure in this lesson: what is measured is not the meeting of two sets but, for each element taken as a separate request, whether what was wanted is preserved. The same two files can have the same set of keys and still have some elements full and some substitute.
Merge Precedence
The manifest file is not a single file. At build time the app’s main file, the files of the build variant and the file of each library are merged. The order of precedence rises from the library toward the variant: the library has the lowest precedence, the app’s main file stands in the middle, and the build variant’s files have the highest precedence. A higher-precedence layer can leave the value written by a lower-precedence layer as it is, repeat it with the same value, or override it by marking it with a different value. The fragment below shows two markers: tools:replace overrides an attribute value, and tools:node="remove" removes the element from the merge.
<application android:label="@string/app_name" tools:replace="android:label"> <activity android:name=".LegacyScreen" tools:node="remove" /> </application>
The model builds this precedence as a chain of three layers. The wanted value is the one written by the lowest layer, the library. Each following layer preserves the value, repeats the same value, or writes a different one. A different value passes only if that layer has marked the element; an unmarked difference is a conflict and the chain stops there. For an element that does not exist in the library at all, there is no value to be wanted, so the model gives it as unmet.
// Chain.kt — a model of layered merging
fun mergeChain(key: String, layers: List<Layer>): Ask {
val wanted = layers.first().values[key] ?: return Ask(key, "-", Outcome.Refused("not-in-library"))
var current = wanted
for (layer in layers.drop(1)) {
val value = layer.values[key] ?: continue
if (value == current) continue
if (key in layer.replaced) current = value
else return Ask(key, wanted, Outcome.Refused("unmarked-conflict"))
}
return Ask(key, wanted, Outcome.Given(current))
}
In the first measurement there are three layers, in the order library, main file, variant. The library writes three elements. The app’s main file repeats one element with the same value and writes one element with an unmarked different value. The variant overrides one element by marking it.
// Main.kt — three layers
fun main() {
val library = Layer(mapOf("label" to "library-name", "direction" to "right-to-left", "camera" to "required"))
val variant = Layer(mapOf("label" to "variant-name"), setOf("label"))
val app = Layer(mapOf("direction" to "right-to-left", "camera" to "optional"))
val asks = listOf("label", "direction", "camera").map { mergeChain(it, listOf(library, app, variant)) }
for (ask in asks) ask.report()
tally(asks)
}
label: wanted=library-name | given=variant-name -> substitute direction: wanted=right-to-left | given=right-to-left -> full camera: wanted=required | reason=unmarked-conflict -> unmet full: 1 substitute: 1 pending: 0 unmet: 1
The label element, once overridden by the variant, is substitute because the given value differs from the wanted one. The direction element did not change at all and is full. The camera element met an unmarked difference: the app wrote the hardware the library called required as optional but did not mark the element, so no value was given.
PS3. The model takes the layer order as fixed: library, the app’s main file, variant. In a real merge the order among several libraries and among several variant files is determined separately; the measurement does not cover it.
Resolving the Conflict
An unmarked conflict is resolved in two ways: either the lower-precedence layer gives up its value, or the higher-precedence layer marks the element. Since changing the library file is mostly not possible, the resolution is on the app side. The result changes with the position of the marker: if the marker stays in the overriding layer the value is overridden, and if another layer comes in between the marker takes effect only as far as that layer. The measurement below compares two separate cases. In the first, the app marks the camera element and overrides it. In the second, the marker was placed by the app’s main file and the variant file later writes a different value.
// Main.kt — the position of the marker
fun main() {
val library = Layer(mapOf("camera" to "required"))
val marked = Layer(mapOf("camera" to "optional"), setOf("camera"))
val appMarked = Layer(mapOf("camera" to "optional"), setOf("camera"))
val variantOther = Layer(mapOf("camera" to "forbidden"))
val asks = listOf(
mergeChain("camera", listOf(library, marked)).let { Ask("app-marked", it.wanted, it.outcome) },
mergeChain("camera", listOf(library, appMarked, variantOther)).let { Ask("main-file-marked", it.wanted, it.outcome) },
)
for (ask in asks) ask.report()
tally(asks)
}
app-marked: wanted=required | given=optional -> substitute main-file-marked: wanted=required | reason=unmarked-conflict -> unmet full: 0 substitute: 1 pending: 0 unmet: 1
The marker protects only the value of the layer that places it; a different value in the following layer asks for a marker again. The rule tells these apart: both requests look at the same element, but in the first the last link of the chain is marked and in the second it is not.
PS4. The model keeps the marker at the element level. In a real file the marker can also be given at the attribute level and can cover several attributes at once; the measurement does not separate this.
The Bounding Measurement
The model has one side it cannot measure: the value may not be written in the manifest file. A library or the app file leaves the value as a placeholder such as ${appLabel}; the real value is supplied by the build variant’s setting. Until the placeholder is resolved, the result of the merge is not known. resolvePlaceholder asks about the same element twice: first when the variant has not supplied the value, then when it has.
// Placeholder.kt — resolving a placeholder
fun resolvePlaceholder(label: String, wanted: String, token: String, provided: Map<String, String>): Ask {
val value = provided[token]
val outcome = if (value == null) Outcome.Waiting("build-variant") else Outcome.Given(value)
return Ask(label, wanted, outcome)
}
// Main.kt — the same element, two moments
fun main() {
val before = resolvePlaceholder("application.label", "Notes", "appLabel", emptyMap())
val after = resolvePlaceholder("application.label", "Notes", "appLabel", mapOf("appLabel" to "Notes"))
val asks = listOf(before, after)
for (ask in asks) ask.report()
tally(asks)
}
application.label: wanted=Notes | waiting-on=build-variant -> pending application.label: wanted=Notes | given=Notes -> full full: 1 substitute: 0 pending: 1 unmet: 0
The same element was first pending, then full. The pending fulfillment is not a lasting result but a moment: what the rule calls pending turns into another result once the thing waited on arrives. That is why the two lines carry the same label and the tally sees them as two separate requests. The measurement also does not say whether the placeholder’s value is wrong; it looks only at whether the given value equals the wanted one.
PS5. The model does not claim what the build actually produces when the placeholder is unresolved, that is, whether it gives an error or continues with an empty value. The pending fulfillment is only the model’s classification.
Summary
- This lesson’s criterion: fulfillment; in merging, for each element, whether what was wanted is preserved; a request per element, not a set. This lesson’s fulfillment: full.
- The manifest file declares the app identity, the app components, the permissions and the hardware features to the platform.
- In merging, the library’s value is preserved as full as long as the app does not touch it or writes the same value.
- An element overwritten with
tools:replaceis substitute: the given value differs from the wanted one. An unmarked conflict is unmet. - A placeholder is pending until the build variant gives its value; once it has given it, the same element becomes full.
Next Step
The manifest file declares what the app is; the text, images and layouts shown on screens live in resources. The next lesson measures what the resource system gives when it selects a resource for the requested configuration and no exact match can be found.
To keep your progress and take notes, Log in
My notes
Log in to take notes.