Lesson 03 / 14
Location and Maps
The location provider delivers location updates through a callback; the minimum interval rule decides which update reaches the receiver, and once the runtime permission is revoked, later updates stop arriving. The lesson measures the delivery of these two rules.
Published Updated
In this lesson
- Recognizes the precise and approximate location permissions and the callback form of the location provider
- Measures which location updates the minimum interval rule drops
- Shows that when the interval falls to one, all updates arrive once
- Shows that updates produced after the permission is revoked are lost
Contents
In the previous lesson the event came from a server to the device, and the service collapsed the messages that piled up. In location, the event is born inside the device: the provider measures the position of the user continuously and hands each measurement to the app as a location update. In a courier app this is a stream lasting seconds, and the receiver does not need all of it. In this lesson we ask the same question again: how many copies of this sent event reached the receiver?
// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)
class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
val copies: Int = received.count { it.seq == sent.seq }
val verdict: String = when {
copies > 1 -> "duplicated"
copies == 1 -> "once"
received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
else -> "lost"
}
fun line() {
println("$label: key=${sent.key} | copies=$copies -> $verdict")
}
}
fun countArrivals(arrivals: List<Arrival>) {
for (name in listOf("once", "duplicated", "superseded", "lost")) {
println("$name: ${arrivals.count { it.verdict == name }}")
}
}
The classification is the same as in the first lessons. The rule counts the copies in the receiver’s list that carry the same sequence number; if there is no copy, it checks whether a later update with the same key arrived. It does not see the content or the cause.
Permission and Provider
Location is a capability that asks for a runtime permission. Two permissions are declared in the manifest: precise location and approximate location. The user can grant only approximate location, or neither; and after granting, the permission can also be revoked from the settings. The app has to handle all three situations.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
The location provider delivers updates through a callback. The app writes the minimum interval into the request, and the provider does not produce updates more often than that interval.
// Platform code: CourierTracker
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
class CourierTracker(private val manager: LocationManager) : LocationListener {
fun start() {
manager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 5000L, 10f, this
)
}
override fun onLocationChanged(location: Location) {
publish(location.latitude, location.longitude)
}
}
The snippet gives two numbers: the minimum time interval in milliseconds and the minimum distance in meters. Both are values the app requests; the snippet does not say how often the provider actually produces updates. The model builds this rule of the platform; the model is not the platform itself. The map view is outside this model: drawing is a question of display, not of delivery, and it is not measured in this lesson.
The Minimum Interval Rule
In the model, an update is a Signal and its key is the identity of the courier. The sequence number is the step at which the update was produced. The minimum interval rule drops an update if not enough steps have passed since the last accepted update. The number of the interval is a made-up value of the model; it is not the real interval of the provider.
// Sampling.kt — builds the minimum interval rule and the revocation of the permission
fun sample(sent: List<Signal>, minGap: Int): List<Signal> {
val kept = mutableListOf<Signal>()
for (s in sent) {
if (kept.isEmpty() || s.seq - kept.last().seq >= minGap) kept.add(s)
}
return kept
}
fun revoke(delivered: List<Signal>, revokedAt: Int): List<Signal> =
delivered.filter { it.seq < revokedAt }
We produce eight updates, with an interval of three steps.
// Main.kt — the minimum interval rule drops the location updates that fall in between
fun main() {
val sent = (1..8).map { Signal("location.courier", it) }
val received = sample(sent, minGap = 3)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [1, 4, 7] event-1: key=location.courier | copies=1 -> once event-2: key=location.courier | copies=0 -> superseded event-3: key=location.courier | copies=0 -> superseded event-4: key=location.courier | copies=1 -> once event-5: key=location.courier | copies=0 -> superseded event-6: key=location.courier | copies=0 -> superseded event-7: key=location.courier | copies=1 -> once event-8: key=location.courier | copies=0 -> lost once: 3 duplicated: 0 superseded: 4 lost: 1
This lesson’s delivery is superseded. Updates 2 and 3 never arrived, but update 4 arrived with the same key and took their place; for 5 and 6, update 7 did the same. For location this is the desired behavior: the new position replaces the old one. The eighth update, however, was lost, because no update with the same key came after it. The rule does not count the dropping of the last update as superseded; the latest state was never shown to the user.
SC1. The model builds the courier with a single key and places the updates at equal steps. On the platform the provider produces updates at irregular intervals, can lengthen the interval to save battery, and the time and distance thresholds apply together, so no update arrives until both are met; none of these exist in the model.
Precise and Approximate Location
The two permissions are two separate levels of accuracy. Precise location uses satellite and network sources together and gives a street-level position; approximate location gives only a rough area. The user can choose one of the two on the permission screen. When the app of a user who chose only approximate location makes a call asking for precise location, the platform lowers the request to the approximate level. The call does not fail; updates arrive, only with lower accuracy.
This is a place the delivery model does not see. The rule counts whether an update arrived; it does not ask how precise the arriving update is. Four updates arriving with approximate location and four updates arriving with precise location give the same result in the eyes of the rule: once 4. Accuracy is a property that the app must check separately on the receiver side. A courier app that needs precise location should check the permission state before showing an update that arrived at the approximate level to the user, and should ask the user for precise location.
Location in the Background
A location update is one thing for an app in the foreground and another for an app in the background. When the app leaves the screen, the platform can lower the update frequency on its own to save battery, and may give no updates at all to an app that has no separate background location permission. The counterparts of these two situations in the delivery model are different.
Lowering the frequency resembles the interval rule working with a large value: the updates in between are never produced and are counted as superseded. Having no background permission resembles the revocation of the permission: later updates never arrive and are counted as lost. In both situations the app sees no error. The answer to the user’s question of “why the location was not updated” depends on the app knowing which permission it holds.
More Than One Key
So far there was a single courier, and the minimum interval rule was applied to all updates like a single counter. This becomes wrong when two couriers receive updates from the same provider: the update of one courier can be dropped because it falls within the interval of the other courier’s update, and the only update reporting the position of the other courier is lost. The correct approach is to apply the interval per key.
// Keys.kt — applies the minimum interval to each key separately
fun sampleByKey(sent: List<Signal>, minGap: Int): List<Signal> =
sent.groupBy { it.key }.values.flatMap { sample(it, minGap) }
Two couriers send updates in alternation; the interval is four steps.
// Main.kt — the interval is applied to each courier separately and each courier keeps its own position
fun main() {
val sent = (19..26).map { Signal(if (it % 2 == 1) "location.courier1" else "location.courier2", it) }
val received = sampleByKey(sent, minGap = 4)
println("received seqs: ${received.map { it.seq }.sorted()}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [19, 20, 23, 24] event-19: key=location.courier1 | copies=1 -> once event-20: key=location.courier2 | copies=1 -> once event-21: key=location.courier1 | copies=0 -> superseded event-22: key=location.courier2 | copies=0 -> superseded event-23: key=location.courier1 | copies=1 -> once event-24: key=location.courier2 | copies=1 -> once event-25: key=location.courier1 | copies=0 -> lost event-26: key=location.courier2 | copies=0 -> lost once: 4 duplicated: 0 superseded: 2 lost: 2
Each courier advanced with its own counter: the first and third updates of both arrived once, the ones in between were superseded, and the last of each courier was lost. The update that takes the place of a superseded update has the same key; this is why the update of one courier is not counted as having taken the place of the other. The two lost updates, 25 and 26, are the cost of the interval: an update that does not come after the interval has no successor to be superseded by, so it never arrives.
SC2. The model builds the two couriers as two separate keys and distributes the updates in turn. On the platform each location request carries its own interval, and several listeners on the same provider can receive updates at independent intervals.
When the Interval Falls to One
Lowering the interval to one relaxes the rule: at least one step between two consecutive updates is enough, and this is always met. With the interval changed, the same rule shows that the measurement drops no update.
// Main.kt — when the interval falls to one, all updates arrive once
fun main() {
val sent = (9..12).map { Signal("location.courier", it) }
val received = sample(sent, minGap = 1)
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
event-9: key=location.courier | copies=1 -> once event-10: key=location.courier | copies=1 -> once event-11: key=location.courier | copies=1 -> once event-12: key=location.courier | copies=1 -> once once: 4 duplicated: 0 superseded: 0 lost: 0
All four updates arrived once. The cost of this lies with the receiver: the receiver now has to process every update. Which interval is suitable is the decision of the app; the rule only counts the result.
SC3. The model takes the processing time of the receiver as zero. On the platform a busy receiver can delay the callback, and the consequences of this delay are not in the model.
Two Thresholds and the Life of the Listener
The snippet in the platform code carried two thresholds: time and distance. The provider gives no update unless both are met; if the courier moved only two meters in five seconds, no update arrives because the distance threshold is not met, even if the time threshold has been met. The model using a single step counter is therefore a simplification. For a courier standing still, the real provider produces no updates at all, and the distinction between superseded and lost in the model raises the question of whether standing still counts as a loss. Standing still is not a loss: there is no sent event.
The life of the listener is a second source. An app that registers the callback and does not remove the registration when the screen closes keeps the provider producing updates, and the receiver works on a screen the user does not see. Conversely, an app that removes the registration too early receives no updates until it opens a new registration when the user returns, and the updates in this interval are never produced. The difference between the loss we measured throughout the lesson and this loss is what the rule can see: the rule counts only sent events; an update that is never produced because the registration was removed has, from the point of view of the model, never been sent.
The practical consequence of this distinction is as follows: an update not arriving is not always a delivery failure. The app should first look at whether the update was produced, and then at whether the produced update reached the receiver. The first question belongs to the provider and the registration, and the second to the rules this lesson measures.
Common Misconceptions
“The minimum interval guarantees the update frequency.” The interval is a lower bound, not a promise. The provider can give updates less often than the interval; saving battery, a weak signal or moving to the background does this. Reducing the interval does not increase the number of updates; it only reduces the number of updates to be dropped.
“More updates mean a better location.” Every update is processed on the receiver. An interval smaller than needed tires the receiver and the battery; in the measurement, when the interval fell to one all updates arrived once, but the cost was that the receiver processed every one of them.
“The user granted the permission once, so it is always valid.” The permission can be revoked from the settings and, in some versions, can reset on its own after a certain time. The app should check the permission state before every update request. Waiting for the callback while there is no permission gives nothing but accumulating lost updates, as in the measurement.
“If the user stopped, the update was lost.” For a user who stands still the provider produces no new update; since there is no sent event, the rule sees no loss. The same silence is seen when the registration is removed, and telling the two apart is the job of the app.
“The map view delivers the location update.” The map only draws the position it holds. If the update did not reach the receiver, the map shows the old place and this appears as a drawing error, although the loss happened earlier, between the provider and the receiver.
The Bounding Measurement
The interval rule only thins the arrival of updates. The permission, on the other hand, is the precondition of arrival. When the user revokes the permission from the settings, the provider gives no update after that step, and later updates do not arrive whatever the interval is.
// Main.kt — once the permission is revoked, later updates do not arrive
fun main() {
val sent = (13..18).map { Signal("location.courier", it) }
val received = revoke(sample(sent, minGap = 2), revokedAt = 16)
println("received seqs: ${received.map { it.seq }}")
val arrivals = sent.map { Arrival("event-${it.seq}", it, received) }
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
received seqs: [13, 15] event-13: key=location.courier | copies=1 -> once event-14: key=location.courier | copies=0 -> superseded event-15: key=location.courier | copies=1 -> once event-16: key=location.courier | copies=0 -> lost event-17: key=location.courier | copies=0 -> lost event-18: key=location.courier | copies=0 -> lost once: 2 duplicated: 0 superseded: 1 lost: 3
Updates 13 and 15 arrived, and 14 was counted as superseded because 15 took its place. Since the permission was revoked at step 16, updates 16, 17 and 18 were lost: no update with the same key came after them. The rule does not see that the cause of the loss was the revoked permission. To know this, the app has to check the permission state separately.
SC4. The model builds the revocation of the permission as a single step and as certain. On the platform, when the permission is revoked the app can be terminated, the precise permission can be lowered to approximate, and the user can grant the permission again later; none of these exist in the model.
Summary
- This lesson’s delivery: superseded. The minimum interval rule drops the location updates that fall in between, and the later update with the same key takes their place.
- Of eight updates, four were superseded, three arrived once and the last was lost: once 3, superseded 4, lost 1.
- When the interval is applied to each key separately, the first and third updates of both couriers arrived once, the ones in between were superseded and the last of each was lost: once 4, superseded 2, lost 2.
- When the interval fell to one, all four updates arrived once; the cost is that the receiver processes every update.
- The three updates produced after the permission was revoked were lost: lost 3. The rule does not see the cause.
- Precise and approximate location are separate permissions and the rule does not see accuracy; the map view is not a delivery question and was not measured.
Next Step
In this lesson the event was a continuous stream and the rule dropped the ones in between. In the camera the event is born from a single user action, and each action should yield a single result. The next lesson, Camera and Media, measures how two separate events arrive when the shutter is touched twice.
To keep your progress and take notes, Log in
My notes
Log in to take notes.