Lesson 01 / 15
Android Architecture Layers
Measures whether a request an app makes to the platform is fulfilled as full, substitute, pending, or unmet, and how that request passes through the layers from the Linux kernel to the application framework.
Published Updated
In this lesson
- Measures that a request falls into one of four fulfillments across eight requests
- Orders the Android architecture layers from the Linux kernel to the system apps
- Shows that a device's API level leaves a framework API full, substitute, or unmet
- Verifies with the bounding measurement that the fulfillment rule sees the result, not the reason
Contents
The previous course measured what makes two values equal in Kotlin and set up the difference between == and === at the language level. This course moves from the same language to the platform itself. An Android app constantly asks the platform for something: to start an activity, to get a permission, to find the text that matches the device’s language, to run a job at a suitable moment. The platform gives every request an answer, but not always the answer that was wanted. An activity named in the manifest file opens as requested, and one that is not named never opens. Where precise location was wanted, the user may give approximate location. If no text matches the device’s language, the default text arrives. A job whose condition is not met waits on that condition.
The question of the course is therefore a single sentence: how does the platform fulfill this request? There are four answers, and their names do not change throughout the course. The category’s umbrella name is fulfillment.
- full: the platform gives a value, and the given value is the wanted one itself.
- substitute: the platform gives a value, but the given value is something other than the wanted one.
- pending: the platform gives no value yet; the request waits on a condition.
- unmet: the platform refuses the request; no value arrives and there is no condition to wait on.
In this course the == of the previous course does one job: it says whether the given value equals the wanted one, which separates full from substitute. The criterion of equality is not a category here; the core compares two strings, and the equality of strings is their content.
The Fulfillment Rule
The rule, not a label, gives the fulfillment. The helper file below sets up the rule and is repeated verbatim as the first Kotlin block of every lesson in the course. The Ask class carries three things: the label of the request, the wanted value, and the result returned by the platform model. The result is one of three subtypes of the sealed Outcome interface: Given says a value was given, Waiting says a condition is awaited, Refused says the request was refused.
// 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 }}")
}
}
fulfillment asks the question in order. If the result is Waiting, the fulfillment is pending. If it is Refused, it is unmet. If it is Given, it is full when the given value equals the wanted one and substitute otherwise. tally counts the four categories in a fixed order and also prints a category whose count is zero.
The measurement has one condition: each request carries a single wanted value, and the result is never written by hand. A line that writes the result directly, in the form Ask("...", "X", Outcome.Given("X")), is not a measurement; it does no more than show the rule itself. The wanted value is written in the request’s own word, while the result comes from a small Kotlin function that sets up how the platform fulfills that mechanism.
Eight Requests, Four Fulfillments
The measurement below builds eight requests from the four topics of the course: two from app components, three from the permission model, two from the resource system, and one from platform services. Four functions imitate four mechanisms of the platform. startExplicit opens the activities declared in the manifest file. grant first separates a permission into normal or dangerous, and for a dangerous permission waits on the user’s choice. pickResource looks for the resource directory that matches the device’s language and falls back to the default directory if there is none. schedule gives a job once its conditions are met, and collects the unmet conditions in sorted order.
// Main.kt — eight requests, four fulfillments
fun startExplicit(target: String, declared: Set<String>): Outcome =
if (target in declared) Outcome.Given(target) else Outcome.Refused("not-in-manifest")
fun grant(permission: String, dangerous: Boolean, choice: String?): Outcome = when {
!dangerous -> Outcome.Given(permission)
choice == null -> Outcome.Waiting("user")
choice == "deny" -> Outcome.Refused("user-denied")
else -> Outcome.Given(choice)
}
fun pickResource(qualifier: String, available: List<String>): Outcome = when {
"values-$qualifier" in available -> Outcome.Given("values-$qualifier")
"values" in available -> Outcome.Given("values")
else -> Outcome.Refused("no-resource")
}
fun schedule(work: String, needs: Set<String>, met: Set<String>): Outcome {
val missing = (needs - met).sorted()
return if (missing.isEmpty()) Outcome.Given(work) else Outcome.Waiting(missing.joinToString(","))
}
fun main() {
val declared = setOf("MainActivity", "ProfileActivity")
val folders = listOf("values", "values-de")
val asks = listOf(
Ask("explicit-start", "ProfileActivity", startExplicit("ProfileActivity", declared)),
Ask("undeclared", "SettingsActivity", startExplicit("SettingsActivity", declared)),
Ask("normal-permission", "INTERNET", grant("INTERNET", dangerous = false, choice = null)),
Ask("dangerous-permission", "CAMERA", grant("CAMERA", dangerous = true, choice = null)),
Ask("precise-location", "ACCESS_FINE_LOCATION",
grant("ACCESS_FINE_LOCATION", dangerous = true, choice = "ACCESS_COARSE_LOCATION")),
Ask("german-text", "values-de", pickResource("de", folders)),
Ask("turkish-text", "values-tr", pickResource("tr", folders)),
Ask("scheduled-work", "sync", schedule("sync", setOf("network", "charging"), setOf("charging"))),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
explicit-start: wanted=ProfileActivity | given=ProfileActivity -> full undeclared: wanted=SettingsActivity | reason=not-in-manifest -> unmet normal-permission: wanted=INTERNET | given=INTERNET -> full dangerous-permission: wanted=CAMERA | waiting-on=user -> pending precise-location: wanted=ACCESS_FINE_LOCATION | given=ACCESS_COARSE_LOCATION -> substitute german-text: wanted=values-de | given=values-de -> full turkish-text: wanted=values-tr | given=values -> substitute scheduled-work: wanted=sync | waiting-on=network -> pending full: 3 substitute: 2 pending: 2 unmet: 1
Of the eight requests, three came out full, two substitute, two pending, and one unmet. This distribution is the binding number of the course; later lessons refer to it and do not change it. It is easy to read which question each line passed through. explicit-start asked for a declared activity and received the same one: full. undeclared asked for an undeclared activity and was refused; because there is no condition to wait on, it is unmet. dangerous-permission asked for a permission the user has not yet chosen and is waiting on user: pending. precise-location asked for precise location and the user chose approximate location; a value was given, but a different one: substitute. turkish-text asked for the Turkish resource directory, and because the device carries only values and values-de, it was given the default directory: substitute again. scheduled-work requires two conditions and one of them is met; the model collects the unmet conditions in sorted order and prints the first one, network.
AC1. The four functions simplify the rules of the platform: whether a real permission counts as normal or dangerous depends on the permission’s definition, and the order of resource selection depends on all the qualifiers of the device configuration. The model sets up these rules of the platform; the platform itself was not measured.
Architecture Layers
Where does a request go? The startActivity an app writes, or a permission query, does not stay in app code alone; it passes through a chain of layers. The architecture of Android is ordered from the bottom up as follows.
- Linux kernel. It manages processes, memory, device drivers, and the foundation of the security mechanism. Each app runs in a separate process with a separate user ID; this separation is the basis of the permission mechanism above it. This is the kernel described in the Linux and System Administration curriculum.
- Hardware abstraction layer (HAL). It exposes hardware such as the camera, sensors, and audio to the upper layers through standard interfaces. The code above does not know a specific piece of hardware; it knows the interface.
- Android runtime (ART). It runs the compiled code of the app. Each app runs in its own runtime instance.
- Native libraries. These are libraries written in C and C++ that do work such as graphics, media, and databases; the framework often uses them.
- Application framework. This is the layer the app calls directly: activities, services, broadcast receivers, content providers, and resources are the app components this layer offers. The topics of this course live here.
- System apps. These are the apps that come with the device, such as phone, messaging, contacts, and settings. They are often the first face an app shows the user.
The app hands the request to the framework, the framework descends into the native libraries when needed, the runtime runs the code, and everything that touches hardware passes through the HAL and the kernel. This chain also explains why a fulfillment is not always in the app’s hands: a request can be refused, made to wait, or turned into another value at one of the layers. A requested permission is asked of the user in the framework, requested hardware depends on the interface the HAL offers, and a requested API depends on the version of the framework on the device.
The Framework API and the Device’s API Level
The framework arrives with the device’s system version. Each version carries an API level, and the level at which an API was added is known. If an app calls an API on a device older than the level at which it was added, the call is not fulfilled. One solution to this problem is the compatibility library: it carries a counterpart of the framework API along with the app, and on the older device that counterpart runs. The counterpart often does not give the wanted thing exactly; it gives a similar behavior.
On the platform this decision is made in app code with a version check. The fragment below is a simplified example. Because it imports a platform class, it does not compile and no output is claimed for it.
// Platform code: framework call or compatibility counterpart by device API level
import android.os.Build
fun applyFeature() {
if (Build.VERSION.SDK_INT >= 33) {
// the framework API is called directly
} else {
// the counterpart in the compatibility library is called
}
}
The model sets up this rule of the platform. The helper file below takes three inputs: the level at which the API was added, the device’s level, and the APIs for which the compatibility library offers a counterpart. If the device’s level is equal or higher, the framework API is given in the wanted form. If it is lower and a counterpart exists, what is given is the counterpart. If neither exists, the request is refused. The file has no output and is compiled together with the later measured blocks.
// Levels.kt — builds the framework call by device API level
fun callFramework(name: String, since: Int, level: Int, compat: Set<String>): Outcome = when {
level >= since -> Outcome.Given(name)
name in compat -> Outcome.Given("$name-compat")
else -> Outcome.Refused("no-api")
}
Three APIs are requested and the device’s level is 31. The first API was added at 26, the second was added at 33 and has a compatibility counterpart, and the third was added at 34 and has no counterpart.
// Main.kt — framework API and device API level
fun main() {
val level = 31
val compat = setOf("ApiB")
val asks = listOf(
Ask("low-level-api", "ApiA", callFramework("ApiA", since = 26, level = level, compat = compat)),
Ask("compatible-api", "ApiB", callFramework("ApiB", since = 33, level = level, compat = compat)),
Ask("missing-api", "ApiC", callFramework("ApiC", since = 34, level = level, compat = compat)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
low-level-api: wanted=ApiA | given=ApiA -> full compatible-api: wanted=ApiB | given=ApiB-compat -> substitute missing-api: wanted=ApiC | reason=no-api -> unmet full: 1 substitute: 1 pending: 0 unmet: 1
The same device and the same rule gave three different fulfillments. The first API exists on the device and was given as wanted. The second API does not exist on the device, but because the compatibility library carries its counterpart, what was given is ApiB-compat; the value is different from the wanted one, so it is substitute. The third API exists neither on the device nor in the compatibility library, and the request stayed unmet. The device’s level and the presence of the compatibility library together determined the fulfillment, which is this lesson’s criterion.
AC2. ApiA, ApiB, and ApiC are example names; their levels are model input. At which level a real API was added cannot be derived from this measurement.
The Bounding Measurement
The fulfillment rule sees the result and the equality, not the reason. This sentence has two sides, and the measurement below sets up both together. The first side is that the same fulfillment can come from two separate reasons: an activity declared in the manifest file and a normal permission are both full, but the first comes from the activity having been declared and the second from the permission being normal. The second side is that two different fulfillments can come from the same missing API: the same API is absent at the same level, and in one case a compatibility counterpart exists and in the other it does not.
// Main.kt — the rule does not see the reason
fun openDeclared(name: String, declared: Set<String>): Outcome =
if (name in declared) Outcome.Given(name) else Outcome.Refused("not-in-manifest")
fun usePermission(name: String, normal: Set<String>): Outcome =
if (name in normal) Outcome.Given(name) else Outcome.Waiting("user")
fun main() {
val asks = listOf(
Ask("declared-activity", "ProfileActivity", openDeclared("ProfileActivity", setOf("ProfileActivity"))),
Ask("normal-permission", "INTERNET", usePermission("INTERNET", setOf("INTERNET"))),
Ask("compat-present", "ApiD", callFramework("ApiD", since = 34, level = 31, compat = setOf("ApiD"))),
Ask("compat-absent", "ApiD", callFramework("ApiD", since = 34, level = 31, compat = emptySet())),
)
for (ask in asks) {
ask.report()
}
println(asks.filter { it.fulfillment == "full" }.map { it.label })
}
declared-activity: wanted=ProfileActivity | given=ProfileActivity -> full normal-permission: wanted=INTERNET | given=INTERNET -> full compat-present: wanted=ApiD | given=ApiD-compat -> substitute compat-absent: wanted=ApiD | reason=no-api -> unmet [declared-activity, normal-permission]
The last line shows the rule’s view: it gathers the two full requests in a single list and does not say for which reason each one is full. In the same way, compat-present and compat-absent fail for the same ApiD at the same level; the only difference between them is whether the compatibility library carries a counterpart, and the rule sees only whether the given value exists and whether it equals the wanted one. The reason is written in the reason= and waiting-on= fields; the category does not use it. Knowing this limit helps when reading the measurement: if two lines have the same fulfillment, it does not follow that their reasons are the same.
AC3. This measurement covers two full requests, one substitute, and one unmet. Two pending requests from the same reason, or two unmet requests from different reasons, were not measured.
Summary
- This lesson’s criterion: fulfillment. The platform answers every request with full, substitute, pending, or unmet, and the rule reads this from the given value. This lesson’s fulfillment: full; the lesson’s focus requests, the declared activity and the normal permission, were fulfilled in full; the compatibility fallback is a substitute and is not part of the focus request.
- The distribution of the eight requests came out 3/2/2/1: three full, two substitute, two pending, one unmet. The later lessons of the course refer to this number.
- The architecture layers, from the bottom up, are the Linux kernel, the hardware abstraction layer, the Android runtime together with the native libraries, the application framework, and the system apps.
- The device’s API level leaves a framework API full, substitute if a compatibility library exists, and unmet if that is missing too.
- The fulfillment rule sees the result and the equality, not the reason: the same fulfillment can come from two reasons, and two different fulfillments can come from the same missing API.
Next Step
One of the eight requests stayed unmet: an undeclared activity never opened. The next lesson, Activities, reads that request closely. It measures with the same rule what an activity is, how it is declared in the manifest file, and whether another app can open it.
To keep your progress and take notes, Log in
My notes
Log in to take notes.