---
title: 'Store Review and Policies'
source: 'https://academia.sh/en/courses/testing-performance-and-release/store-review-and-policies'
course: 'Testing, Performance and Release'
language: en
updated: '2026-10-21T05:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Distinguishes what the app store's review checks from what it does not check"
    - 'Counts the permissions that are declared but unused, per module'
    - "Sets the permission budget as a team decision and classifies the modules' overrun"
    - 'Shows that the overrun closes once the unused permissions are removed'
---

# Store Review and Policies

Counts, per module, the permissions the app declares but does not use, and compares the count with a budget. Shows how the overrun closes once the unused permissions are removed, and that the rule does not see the weight of a finding.

The previous lesson widened the release stage by stage and looked at the crash count at every stage. But before a build reaches users, it also passes the app store's own inspection. The store reviews every uploaded release against its policies (store review): are the permissions the app declares really used, how is user data handled, does the description on the store page match what the app does.

The details of the review are set by policies that change from one store to another and from one time to another; this lesson neither lists the content of the policies nor counts how many there are. Instead it measures something the team itself holds: the permissions the app declares but does not use. The course's question is the same here: **in how many samples does this measurement exceed its budget?** The measurement is the number of permissions in a module that are declared but unused (`policyFindings`), the budget is the number the team accepts, and each module is a sample.

```kotlin
// Overrun.kt — classifies in how many samples a measurement exceeds its budget
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "within"
        over == total -> "all"
        over * 2 < total -> "minority"
        else -> "majority"
    }

    fun line() {
        println("$label: budget=${budget.name}<=${budget.limit} | over=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("within", "minority", "majority", "all")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}
```

`Overrun` counts the samples that strictly exceed the limit and gives the prevalence with four names: within, minority, majority, all. The rule sees how many modules overrun; whether a module carries one or three unused permissions is the same for every sample that overruns.

## Permission Declaration

When an app wants to reach the user's data or a piece of the device's hardware, it declares this request in advance in a manifest. The declaration is the contract that tells the user and the store what the app can do.

```xml
<!-- Platform code: permission declaration -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```

This snippet is a configuration file; it is not run, and we claim no output. The model sets up the rule for this measurement; the model is not the tool itself. In the model a permission is a name; a module carries both the set of permissions it declares and the set it actually uses in its code. The finding is the difference between the two: a permission that is declared and not used.

It is easy to see why this finding matters. An unused permission looks to the user like an unnecessary request and calls for an explanation in review. This is why teams try to declare permissions only as far as they actually use them.

The declaration is addressed to two separate readers, and they look for different things. The user sees the declaration as a request: the app wants to reach the camera, should that be allowed? The store reads the declaration as a question of consistency: does what is declared match the app's description and behavior? The two-line snippet above is therefore not only a configuration; it is a promise the team makes to the user and to the store. If the promise is made wider than needed, the user sees an unnecessary permission and the store asks for an explanation. If it is made too narrow, the feature does not work. The way between the two is to make the declared set equal to the used set, and this lesson's measurement counts that inequality.

## Setting Up the Model

The helper file sets up eight modules, the permissions each declares and uses, and the finding count. The permission names are plain words; they are the short labels used in the lesson, not the real names on the platform.

```kotlin
// PolicyModel.kt — models, per module, the permissions that are declared but unused
class AppModule(val label: String, val declared: Set<String>, val used: Set<String>)

fun unusedPermissions(module: AppModule): List<String> =
    (module.declared - module.used).sorted()

fun policyFindings(module: AppModule): Int = unusedPermissions(module).size

fun trimmed(module: AppModule): AppModule =
    AppModule(module.label, module.declared.intersect(module.used), module.used)

fun appModules(): List<AppModule> = listOf(
    AppModule("main", setOf("location", "camera"), setOf("location")),
    AppModule("login", setOf("contacts"), setOf("contacts")),
    AppModule("payment", setOf("camera", "location", "contacts"), setOf("camera")),
    AppModule("map", setOf("location", "storage"), setOf("location")),
    AppModule("profile", setOf("camera", "storage", "contacts"), setOf("camera", "storage")),
    AppModule("search", setOf("microphone"), setOf("microphone")),
    AppModule("help", setOf("location", "calendar"), emptySet()),
    AppModule("settings", setOf("storage", "calendar"), setOf("calendar")),
)
```

## Measuring the Findings

The team's rule is simple: a module must not declare any permission it does not use. The budget is zero: even a single unused permission is an overrun.

```kotlin
// Main.kt — the unused permissions of each module are counted, budget zero
fun main() {
    val modules = appModules()
    for (module in modules) {
        println("${module.label}: unused=${unusedPermissions(module)}")
    }
    val samples = modules.map { policyFindings(it) }
    println("findings: $samples")
    Overrun("unused-permission", Budget("finding", 0), samples).line()
}
```

```text
main: unused=[camera]
login: unused=[]
payment: unused=[contacts, location]
map: unused=[storage]
profile: unused=[contacts]
search: unused=[]
help: unused=[calendar, location]
settings: unused=[storage]
findings: [1, 0, 2, 1, 1, 0, 2, 1]
unused-permission: budget=finding<=0 | over=6/8 -> majority
```

Six modules exceeded while the budget was zero; only `login` and `search` came out clean. Because more than half of the eight exceeded, the answer is **majority**. This is the lesson's overrun.

**DI3.** The model counts a permission as a name. In reality a permission may be used not by one module but by several, a permission may come together with another permission, or a library may add a permission without the app knowing. The model does not set up these three cases. The declared and used sets are also hand-written inputs; in a real app the usage comes from scanning the code.

## The Budget Is a Decision

Measure the same eight modules with three separate budgets: two, one and zero unused permissions.

```kotlin
// Main.kt — the same eight modules give three different overruns under three different budgets
fun main() {
    val samples = appModules().map { policyFindings(it) }
    val roomy = Overrun("two-permissions", Budget("finding", 2), samples)
    val normal = Overrun("one-permission", Budget("finding", 1), samples)
    val strict = Overrun("zero-permissions", Budget("finding", 0), samples)
    listOf(roomy, normal, strict).forEach { it.line() }
    countOverruns(listOf(roomy, normal, strict))
}
```

```text
two-permissions: budget=finding<=2 | over=0/8 -> within
one-permission: budget=finding<=1 | over=2/8 -> minority
zero-permissions: budget=finding<=0 | over=6/8 -> majority
within: 1
minority: 1
majority: 1
all: 0
```

With a budget of two, no module exceeded. Dropping to one, only `payment` and `help`, which have two unused permissions, exceeded. At zero, six modules exceeded. The three results came from the same measurement; they do not refute one another, they only show what the budget accepts. Setting the budget to zero is a strict team rule, and as long as the rule is not loosened the answer cannot come out as all, because two modules carry no findings at all.

## The Count Does Not See Weight

The count treats two findings as equal, yet not all findings carry the same weight. The camera, location, contacts and microphone permissions reach more sensitive user data; the storage and calendar permissions will be assumed less sensitive in the lesson. This is a model assumption, not the platform's classification.

```kotlin
// Main.kt — the count does not see weight, the number of sensitive permissions is measured separately
fun main() {
    val sensitive = setOf("camera", "location", "contacts", "microphone")
    val modules = appModules()
    for (module in modules) {
        val unused = unusedPermissions(module)
        val heavy = unused.count { it in sensitive }
        println("${module.label}: findings=${unused.size} sensitive=$heavy")
    }
    val samples = modules.map { policyFindings(it) }
    Overrun("unused-permission", Budget("finding", 0), samples).line()
}
```

```text
main: findings=1 sensitive=1
login: findings=0 sensitive=0
payment: findings=2 sensitive=2
map: findings=1 sensitive=0
profile: findings=1 sensitive=1
search: findings=0 sensitive=0
help: findings=2 sensitive=1
settings: findings=1 sensitive=0
unused-permission: budget=finding<=0 | over=6/8 -> majority
```

The `map` and `settings` modules carry one finding but have no sensitive findings; `main` and `profile` also carry one finding, and that finding turns out to be sensitive. The rule counts the four the same: all four are samples that overrun. The category is **majority**, and this category does not say which module is riskier in review.

The verdict of the review does not come out of this count. The store may reject a release or ask for a fix; this decision depends not on the category but on the review's own assessment. The model does not set up the rejection decision, and here we claim neither a rejection nor an acceptance.

**DI4.** The sensitivity distinction in this lesson is an assumed set; which permission counts as sensitive is determined separately in a real store policy and changes over time. The model takes this set as an input.

## Difference Between Releases

The declaration is not written once and left; a new permission may be added in every release. A permission added in a new release is a new declaration whose justification may be asked for in review. So for the team the real question is not only today's surplus but what was added in this release. The measurement below takes the previous release's declaration as an input for each module and gives two counts: the number of added permissions and the number of added permissions that are unused. The budget is zero in both.

```kotlin
// Main.kt — how many of the permissions added in the new release are unused
fun main() {
    val previous = mapOf(
        "main" to setOf("location"),
        "login" to setOf("contacts"),
        "payment" to setOf("camera", "location"),
        "map" to setOf("location", "storage"),
        "profile" to setOf("camera", "storage", "contacts"),
        "search" to emptySet<String>(),
        "help" to setOf("location"),
        "settings" to setOf("storage", "calendar"),
    )
    val modules = appModules()
    val added = modules.map { it.declared - previous.getValue(it.label) }
    val addedUnused = modules.mapIndexed { index, module -> (added[index] - module.used).size }
    println("added: ${added.map { it.size }}")
    println("added and unused: $addedUnused")
    val budget = Budget("permission", 0)
    Overrun("added", budget, added.map { it.size }).line()
    Overrun("added-unused", budget, addedUnused).line()
}
```

```text
added: [1, 0, 1, 0, 0, 1, 1, 0]
added and unused: [1, 0, 1, 0, 0, 0, 1, 0]
added: budget=permission<=0 | over=4/8 -> majority
added-unused: budget=permission<=0 | over=3/8 -> minority
```

DI5. The previous release's declaration is a hand-written input, and the module names are assumed not to change between releases. In reality a module may be split, merged or renamed; the model does not set up these cases. It also does not tell whether an added permission came for a new feature or as a side effect of a library.

## The Bounding Measurement

Remove the unused permissions: each module's declaration comes down to only the permissions it uses. This is exactly the case in which the finding closes. The bounding measurement therefore shows the best answer that can be given to the lesson's question, and tells whether the budget is reachable.

```kotlin
// Main.kt — once the unused permissions are removed, all modules fit the budget
fun main() {
    val before = appModules()
    val after = before.map { trimmed(it) }
    val budget = Budget("finding", 0)

    val beforeSamples = before.map { policyFindings(it) }
    val afterSamples = after.map { policyFindings(it) }
    Overrun("before", budget, beforeSamples).line()
    Overrun("after", budget, afterSamples).line()
    println("declared before: ${before.sumOf { it.declared.size }}")
    println("declared after: ${after.sumOf { it.declared.size }}")
}
```

```text
before: budget=finding<=0 | over=6/8 -> majority
after: budget=finding<=0 | over=0/8 -> within
declared before: 16
declared after: 8
```

Once the unused permissions were removed, the overrun dropped from the **majority** category to the **within** category; the number of declared permissions fell from sixteen to eight. This result is a count: eight permissions were never used in the app's code, and removing them did not change the functionality. If it turns out that one of the removed permissions was used in the code in some other way, the usage set was wrong. Then what needs fixing is not the declaration but how the usage was detected.
This result shows that the budget is reachable, but it does not show the cost of reaching it. Removing eight permissions did not change the functionality, but saying that a permission is really unused depends on how usage is detected. The model took the usage set as an input; in a real app this set is derived from the code, and the derivation may miss indirect uses, for example calls a library makes on the app's behalf. The result of the bounding measurement is therefore read on the condition that usage is verified by an independent path before the removal.

## Where the Usage Set Comes From

The lesson's measurement rests on the difference between two sets, but only one of the sets is written in a file. The declared set is read from the manifest and is explicit. The usage set is the result of which permission-requiring calls the code makes, and it needs a scan or a compiler output. The reliability of these two sources is not the same: the declaration is exact, the usage is an inference. The difference comes from this asymmetry. If the usage set is computed too small, the surplus looks larger than it is, and a permission that is removed breaks a feature. If the usage set is computed too large, the surplus looks smaller than it is, and an unnecessary permission stays. The first error shows up to the user, the second in review.

For this reason the team tests both directions separately: for each removed permission it verifies that there really is no feature that uses it, and for each remaining permission that there is a feature that uses it. The model does not set up this verification; it takes the usage set as correct and only counts the difference.

## Common Misconceptions

**"If there are no findings, the review passes."** It does not. The count measures only the difference between declaration and usage; the review checks other policies too.

**"If the finding count is low, the risk is low."** The number does not show the weight of a finding. One sensitive finding can matter more than several non-sensitive ones.

**"Adding a new permission is always a problem."** The problem is a permission that is added and not used. The `search` module added a new permission in this release, and the permission was used in the code; it appeared as an overrun in the first count but not in the second.

**"Shrinking the declaration does not affect features."** It does not if the usage set is correct. If the usage set is incomplete, that is, if a permission is used in the code in some other way, shrinking the declaration breaks that feature. Before shrinking, it is necessary to verify that usage detection is reliable.

**"Every permission should be removed."** Only the unused ones are removed. Removing a permission that is used makes that feature stop working.

**"A non-sensitive permission is not a problem."** The sensitivity set was an assumption in this lesson. A permission not counted as sensitive also exceeds the budget if it is unused, and calls for an explanation in review. Sensitivity only serves to order which finding is handled first; it does not remove the existence of the finding.

**"Keeping the declaration clean is a one-time job."** In every release a new feature may bring a new permission, and when a feature is removed its permission may stay in the declaration. This is why the count runs again at a release gate, before every release; an audit that runs once and closes accumulates surplus again in the next release.

**"Keeping the budget at zero is always realistic."** Zero was a strict rule the team chose in this lesson. The three budgets showed that the same eight modules come out majority at zero, minority at one and within at two. A realistic budget depends on which findings the team can close before review.

## Summary

- This lesson's overrun: majority. Six of eight modules carried an unused permission under a zero budget; because more than half exceeded, the answer is majority.
- Store review checks against policies; this lesson did not measure the content of the policies but the finding count in the team's hands.
- The same eight modules came out within, minority and majority under budgets of two, one and zero, in that order.
- The rule counts four findings the same, but two of them were sensitive permissions; the count shows neither weight nor the rejection decision.
- The fault of the permissions added relative to the previous release is not in their being added but in their being unused; two counts separate the two.
- Once the unused permissions were removed, the declaration fell from sixteen to eight and the overrun became within.

## Next Step

The app passed review, widened stage by stage and reached users. But the release is not over; the real data arrives once users begin to use the app under real conditions. The next lesson, `Post-Release Monitoring`, compares the crash counts of the days after release with a budget.
