Permissions, storage and publishing
Requesting runtime permissions, choosing where files belong, and the steps to put a signed build on Google Play.
Requesting permissions
Normal permissions are granted at install time just by declaring them. Dangerous permissions — camera, location, contacts, microphone — must additionally be requested at runtime, and the user can refuse.
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.camera" android:required="false" />private val askCamera = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) openCamera() else showRationale()
}
fun ensureCamera() {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED -> openCamera()
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> showRationale()
else -> askCamera.launch(Manifest.permission.CAMERA)
}
}- Check
shouldShowRequestPermissionRationaleto decide whether to explain first instead of firing the system dialog blindly. - A refusal is permanent after two denials on modern Android: the app must route the user to system settings rather than asking again.
- Declare
android:requiredon<uses-feature>so devices without the hardware are not filtered out of the store listing.
Where files belong
| Need | Use | Cleared when |
|---|---|---|
| Small key/value settings | DataStore (or SharedPreferences) | App data is cleared |
| Private files, only your app reads them | internal storage via context.filesDir | App is uninstalled |
| Temporary scratch data | context.cacheDir | The system may reclaim it any time |
| Photos, music, documents shared with others | MediaStore / Storage Access Framework | User deletes them |
| Structured, queryable data | Room (SQLite) | App is uninstalled |
| Secrets and tokens | Encrypted storage or the Keystore | App is uninstalled |
// private file inside the app sandbox
val notes = File(context.filesDir, "notes.txt")
notes.writeText("remember to ship")
val restored = notes.readText()
// let the user pick a destination for an export, no storage permission needed
val create = registerForActivityResult(
ActivityResultContracts.CreateDocument("text/plain")
) { uri -> uri?.let { context.contentResolver.openOutputStream(it)?.use { s ->
s.write(restored.toByteArray())
} } }
create.launch("notes.txt")Since Android 10 the scoped storage rules mean you no longer ask for broad storage permission to write a file the user chose. Direct paths like /sdcard/Download are not yours to write; go through MediaStore or the Storage Access Framework.
Signing and shipping
android {
defaultConfig {
applicationId = "com.example.notes"
versionCode = 12
versionName = "1.4.0"
minSdk = 24
targetSdk = 35
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
// ./gradlew bundleRelease -> app/build/outputs/bundle/release/app-release.aab- Play distributes App Bundles (
.aab); Google generates per-device APKs from it. APKs are still accepted for direct distribution. versionCodemust increase with every upload;versionNameis the string users see.- Keep the upload keystore and its passwords in a safe place. Losing it means you cannot ship updates to that listing unless Play App Signing can reset it for you.
- Ship to a closed testing track first. Internal testing is near-instant; production review of a new app or a sensitive permission can take days.
⚠️
Never commit a keystore or its passwords to version control. Read them from environment variables or a local properties file that is listed in
.gitignore, and enable Play App Signing so the upload key can be rotated if it leaks.FAQ
Do I really need a permission to save a file?
Only for files the user did not explicitly choose. Anything your app creates in its own sandbox or writes through the Storage Access Framework needs no storage permission at all.
Why was my release build rejected?
Common causes are a
targetSdk below the current Play requirement, a debuggable or unoptimised build, a missing privacy policy for data-collecting permissions, and placeholder or duplicate content in the listing.Related
Layouts and Jetpack Compose Android projects and activities
Last refreshed 2026-09-18.