Skip to content
academia.sh

Lesson 15 / 15

The Permission Model

A fulfillment model of install-time and runtime permissions, normal and dangerous permissions, precise and approximate location, denial and the do-not-ask-again state, with the closing table of the course.

Published Updated

In this lesson

  • Distinguishes install-time permissions from runtime permissions and normal permissions from dangerous permissions
  • Measures that a dangerous permission the user has not yet answered is pending and one that was denied is unmet
  • Measures that approximate location given in place of precise location is substitute and that the do-not-ask-again state is unmet
  • Reads the fulfillment of the fifteen lessons measured across the course in a single table
Contents

The previous lesson measured the installation of the app on a device. Installation is done once, and the app stays on the device. Afterward the app asks for the user’s camera, location or contacts. A permission is the app’s right to access protected data or hardware. This lesson measures what the platform gives to a permission request, and closes the course.

The measurement axis of the course does not change: how does the platform fulfill this request. There are four fulfillments: full, substitute, pending and unmet. The rule, not a label, gives the fulfillment: a given value that equals the wanted one is full, and one that differs is substitute; pending and unmet come directly from the outcome. The file below carries that rule and is repeated verbatim in every lesson.

// 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 permission lessons in M30/K02 measured the meeting of the requested permission set with the given set. Here each permission is a separate request, and a permission the user has not yet answered is a category in its own right: pending. In M12/K03 the decision belonged to the user, but the category did not name the owner of the decision; the fulfillment stayed the same and only the reason= field could name the owner. It is the same here.

Install-Time Permissions and Runtime Permissions

Permissions are given at two moments. An install-time permission is given while the app is being installed, without asking the user. A runtime permission is requested while the app is running, by showing the user a dialog. The permission type determines which goes through which path:

  • A normal permission touches the user’s privacy little, for example network access. Declaring it in the manifest is enough, and it is given at install.
  • A dangerous permission touches the user’s data or hardware, for example the camera, audio recording and location. It must be declared in the manifest, but it is not given unless the user approves it at run time.

The declaration in the manifest looks like this. It is a file that the platform tools read and is not run.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

The model describes a permission by its name, whether it is dangerous, and whether it is declared in the manifest. The user’s reply is a separate input: null if there is no reply, grant for approval, deny for denial. Any other text shows that the user gave something different from what was requested.

// Permission.kt — a model of the permission request
class Perm(val name: String, val dangerous: Boolean, val declared: Boolean)

fun resolvePermission(label: String, perm: Perm, reply: String?): Ask {
    val outcome = when {
        !perm.declared -> Outcome.Refused("not-declared")
        !perm.dangerous -> Outcome.Given(perm.name)
        reply == null -> Outcome.Waiting("user-reply")
        reply == "deny" -> Outcome.Refused("user-denied")
        reply == "grant" -> Outcome.Given(perm.name)
        else -> Outcome.Given(reply)
    }
    return Ask(label, perm.name, outcome)
}

There are three permissions in the measurement. One normal permission and two dangerous permissions are asked, and the user has not yet answered any of them.

// Main.kt — normal and dangerous permissions
fun main() {
    val asks = listOf(
        resolvePermission("network", Perm("internet", false, true), null),
        resolvePermission("camera", Perm("camera", true, true), null),
        resolvePermission("microphone", Perm("audio-recording", true, true), null),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
network: wanted=internet | given=internet -> full
camera: wanted=camera | waiting-on=user-reply -> pending
microphone: wanted=audio-recording | waiting-on=user-reply -> pending
full: 1
substitute: 0
pending: 2
unmet: 0

The normal permission became full without waiting for any reply. The two dangerous permissions were not refused; they are waiting, and the name of what they wait on is user-reply. This lesson’s fulfillment is pending.

PS6. The model builds the reply as a single piece of text. On the real platform the reply is the result of a dialog and differs according to the option with which the permission was given; the measurement does not imitate the dialog.

PS7. The model takes the separation of normal and dangerous permissions as one logical value per permission. On the real platform the version determines the protection level of a permission; which permission is at which level was not measured.

The Permission Request Flow

A runtime permission is requested in code with a contract. ActivityResultContracts.RequestPermission takes the name of the permission and reports through a callback whether the user approved. The fragment below shows the shape of the flow. It uses a platform API, so it is not measured and its output is not claimed.

val launcher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) openCamera() else showFallback()
}

launcher.launch(Manifest.permission.CAMERA)

The flow has two return paths, and the model separates both. At the moment the dialog opens, the app is waiting for a reply: pending. When the user replies, the same request becomes either full or unmet. To measure this, the same permission is asked in three user states.

// Main.kt — before the reply, approval and denial
fun main() {
    val camera = Perm("camera", true, true)
    val asks = listOf(
        resolvePermission("no-reply", camera, null),
        resolvePermission("approval", camera, "grant"),
        resolvePermission("denial", camera, "deny"),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
no-reply: wanted=camera | waiting-on=user-reply -> pending
approval: wanted=camera | given=camera -> full
denial: wanted=camera | reason=user-denied -> unmet
full: 1
substitute: 0
pending: 1
unmet: 1

The same request received three fulfillments, and the difference did not come from the permission but from the user’s reply. The fulfillment category does not say the owner; only reason=user-denied names the user’s decision.

Precise and Approximate Location

The location permission is requested at two levels of detail. Precise location gives the place where the device is in detail. Approximate location gives only the surroundings. The app can request both together, and if the user picks the approximate one, the app has to work without precise location. The model counts this as substitute: the wanted one is precise location, the given one is approximate location.

// Main.kt — precise location was requested, what the user gave
fun main() {
    val location = Perm("precise-location", true, true)
    val asks = listOf(
        resolvePermission("precise-grant", location, "grant"),
        resolvePermission("approximate-grant", location, "approximate-location"),
        resolvePermission("no-reply", location, null),
        resolvePermission("denial", location, "deny"),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
precise-grant: wanted=precise-location | given=precise-location -> full
approximate-grant: wanted=precise-location | given=approximate-location -> substitute
no-reply: wanted=precise-location | waiting-on=user-reply -> pending
denial: wanted=precise-location | reason=user-denied -> unmet
full: 1
substitute: 1
pending: 1
unmet: 1

All four cases fell into a separate category. When the user gave approximate location, the app was not refused; it is working with less than it asked for.

PS8. The model names approximate location as the substitute of precise location. On the real platform the accuracy of approximate location varies by device and setting; the measurement does not give this accuracy as a number.

Denial, Do Not Ask Again and Rationale

When the user denies a request, the app may not be able to show the dialog for the same permission again right away. The platform separates two cases. After the first denial, the system suggests showing a rationale to explain why the permission is needed: the value of shouldShowRequestPermissionRationale returns true. A rationale is the app’s own screen; it tells the user what the permission is for. When the user denies the request with “do not ask again”, the system never shows the dialog and the request comes back directly denied.

A rationale does not change the fulfillment. What changes is the probability of the reply the user will give. What changes the fulfillment is whether the user can be reached. The model builds this with a single input: whether the dialog is blocked.

// Denial.kt — a model of the request after denial
fun askAfterDenial(label: String, perm: Perm, blocked: Boolean, reply: String?): Ask {
    val outcome = when {
        blocked -> Outcome.Refused("do-not-ask-again")
        reply == null -> Outcome.Waiting("user-reply")
        reply == "deny" -> Outcome.Refused("user-denied")
        else -> Outcome.Given(perm.name)
    }
    return Ask(label, perm.name, outcome)
}

In the measurement the user has denied the camera once. In the first two rows the dialog is open, and the request is asked before and after the rationale. In the last two rows the user has said “do not ask again”: one is asked with no reply, one with approval.

// Main.kt — rationale and do not ask again
fun main() {
    val camera = Perm("camera", true, true)
    val asks = listOf(
        askAfterDenial("before-rationale", camera, false, null),
        askAfterDenial("after-rationale", camera, false, "grant"),
        askAfterDenial("do-not-ask-again", camera, true, null),
        askAfterDenial("do-not-ask-again-grant", camera, true, "grant"),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
before-rationale: wanted=camera | waiting-on=user-reply -> pending
after-rationale: wanted=camera | given=camera -> full
do-not-ask-again: wanted=camera | reason=do-not-ask-again -> unmet
do-not-ask-again-grant: wanted=camera | reason=do-not-ask-again -> unmet
full: 1
substitute: 0
pending: 1
unmet: 2

In the blocked dialog even an approval reply did not change the result, because the reply never reached the user. The way out of this state is not inside the app: the user turns on the permission itself from the system settings.

PS9. The model takes the “do not ask again” state as a single logical value. On the real platform, how many repetitions of which denial create this state varies by version; the measurement does not determine this.

The Bounding Measurement

The fulfillment rule sees the result and the equality, not the reason. In permissions this boundary has one face: a permission that was not declared cannot be given by any user reply. The model makes the comparison in order. First, is the permission in the manifest; then, is it dangerous; then, what did the user say. The first field that does not match determines the result and gives its name.

In the measurement the same camera permission is asked without being written in the declaration and with the user having approved. For comparison, a normal permission is also asked without being declared, and its declared form is added.

// Main.kt — an undeclared permission
fun main() {
    val asks = listOf(
        resolvePermission("undeclared-camera", Perm("camera", true, false), "grant"),
        resolvePermission("undeclared-network", Perm("internet", false, false), null),
        resolvePermission("declared-camera", Perm("camera", true, true), "grant"),
    )
    for (ask in asks) ask.report()
    tally(asks)
}
undeclared-camera: wanted=camera | reason=not-declared -> unmet
undeclared-network: wanted=internet | reason=not-declared -> unmet
declared-camera: wanted=camera | given=camera -> full
full: 1
substitute: 0
pending: 0
unmet: 2

The user’s approval did not rescue the undeclared camera permission. The normal permission was not given either without being declared: an install-time permission also starts from the manifest.

The Closing Table

The course asked the same question across fifteen lessons. The table below gives together, for each lesson, the request it measured, that lesson’s fulfillment, and what the lesson’s bounding measurement showed. The fulfillment cell is taken from the “This lesson’s fulfillment” item in each lesson’s Summary. The selection rule is single: a lesson’s fulfillment is the fulfillment of the focus request in the “Measured request” column, not the majority of the measurements in the lesson.

Lesson Measured request Fulfillment Bounding measurement
Android Architecture Layers declared activity, normal permission full the same full comes from two separate reasons, the rule does not see the reason; compatibility fallback substitute
Activities undeclared or non-exported activity unmet android:exported: the same activity is full or unmet depending on the caller
The Activity Lifecycle state on a configuration change substitute a UI update to a stopped activity waits, and to a destroyed one is unmet
Fragments fragment transaction while the host is stopped pending commit after the state is saved is unmet, commitAllowingStateLoss is substitute
Services starting a service from the background unmet a foreground service is substitute, a conditionally scheduled job is pending
Broadcast Receivers implicit system broadcast unmet the same receiver class gives two results on three paths
Content Providers row query from a provider full a query without permission is unmet, a URI permission covers only a single row
Intents and Intent Filters implicit intent pending number of installed apps: zero is unmet, one is full, two is pending
Tasks and the Back Stack new instance request substitute the same mode gives two results depending on the state of the stack
The Manifest File merged manifest element full the result is not known until the placeholder is resolved: pending
The Resource System resource selection with qualifiers substitute the language directory when the region directory is absent; qualifier conflict
The Build System transitive dependency version substitute version 1.10 is higher than version 1.9; strict version conflict
Product Flavors file requested from a source set substitute a .kt class found in the same two sets is a conflict, unmet
The Development Environment installing the app on a device pending a processor architecture mismatch is unmet even when the API level is sufficient
The Permission Model dangerous runtime permission pending an undeclared permission is not given by any reply

The distribution: full 3, substitute 5, pending 4, unmet 3; total 15. The opening lesson gave the core distribution of 3/2/2/1. The two distributions are not the same thing. The core was a single measurement of eight requests; the table counts the own fulfillment of each of the fifteen lessons. No forecast bridge is built between them: one is the output of one model, and the other is a selected row from each of fifteen separate models.

Summary

  • This lesson’s criterion: fulfillment; whether the permission waits for a reply from the user, not who decides. This lesson’s fulfillment: pending.
  • An install-time permission is given at install without asking the user; a runtime permission is given while running, by asking the user.
  • A normal permission becomes full once declared; a dangerous permission is pending until the user replies, and a denial is unmet.
  • If precise location is requested and approximate location is given, the result is substitute.
  • A rationale does not change the fulfillment; in the “do not ask again” state the system does not reach the user and the request comes back unmet.
  • A permission not declared in the manifest is not given even if the user approves.

Next Step

The course closes here. The next course covers building the interface in a declarative form.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close