Android Permissions — Security Best Practices
Android permissions are the gatekeepers of user data and device features. They protect users by ensuring apps can only access sensitive data and system features with explicit user consent. Starting with Android 6.0 (API 23), Android introduced the runtime permissions model, where dangerous permissions must be requested at runtime rather than at install time.
1. Types of Android Permissions
Android permissions are categorized into three levels of protection:
| Type | Description | Examples |
|---|---|---|
| Normal | Low-risk permissions that don't significantly impact user privacy. ✅ Automatically granted — no user prompt. |
INTERNET, ACCESS_NETWORK_STATE, VIBRATE, SET_ALARM, WAKE_LOCK |
| Dangerous | High-risk permissions that access user data or device features. ⚠️ Must be requested at runtime — user must grant explicitly. |
CAMERA, READ_CONTACTS, ACCESS_FINE_LOCATION, RECORD_AUDIO, READ_EXTERNAL_STORAGE |
| Signature | Only granted to apps signed with the same certificate. 🔐 Automatically granted — for system apps or same developer apps. |
System-level permissions (custom permissions defined by apps) |
2. Common Dangerous Permissions
| Permission | Manifest Declaration | Use Case |
|---|---|---|
| Camera | android.permission.CAMERA |
Take photos, scan QR codes |
| Location | android.permission.ACCESS_FINE_LOCATION android.permission.ACCESS_COARSE_LOCATION |
GPS, maps, location-based services |
| Contacts | android.permission.READ_CONTACTS android.permission.WRITE_CONTACTS |
Access device contacts, sync |
| Storage | android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE |
Read/write files, images, documents |
| Microphone | android.permission.RECORD_AUDIO |
Voice recording, audio calls |
| Phone | android.permission.READ_PHONE_STATE android.permission.CALL_PHONE |
Read device ID, make calls |
| SMS | android.permission.READ_SMS android.permission.SEND_SMS |
Read and send SMS messages |
| Calendar | android.permission.READ_CALENDAR android.permission.WRITE_CALENDAR |
Access calendar events |
3. The Runtime Permission Flow
│ 1. Check if permission is already granted │
│ →
checkSelfPermission() │└─────────────────────────────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ ✅ Already Granted │ ❌ Not Granted │
│ → Proceed with │ → Request permission │
│ operation │
requestPermissions() │└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Handle User Response │
│ →
onRequestPermissionsResult() │└─────────────────────────────────────────────────────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ ✅ Granted │ ❌ Denied │
│ → Proceed with │ → Show rationale │
│ operation │ → Or gracefully degrade │
└─────────────────────────────────────────────────────────────┘
4. Declaring Permissions in Manifest
All permissions must be declared in AndroidManifest.xml:
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<!-- Normal permissions (auto-granted) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Dangerous permissions (runtime required) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Optional: Declare optional hardware features -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application ...>
...
</application>
</manifest>
5. Complete Permission Implementation
Step 1: Create Permission Helper
// PermissionHelper.kt
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
object PermissionHelper {
// List of permissions required by the app
val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.RECORD_AUDIO
)
// Check if a single permission is granted
fun isPermissionGranted(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
// Check if all permissions are granted
fun areAllPermissionsGranted(context: Context): Boolean {
return REQUIRED_PERMISSIONS.all { permission ->
isPermissionGranted(context, permission)
}
}
// Get list of denied permissions
fun getDeniedPermissions(context: Context): List<String> {
return REQUIRED_PERMISSIONS.filter { permission ->
!isPermissionGranted(context, permission)
}
}
}
Step 2: Activity with Permission Handling
// MainActivity.kt
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
private lateinit var cameraButton: Button
private lateinit var locationButton: Button
// Permission launcher (modern approach)
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
// Handle all permission results
val allGranted = permissions.values.all { it }
if (allGranted) {
// All permissions granted — proceed
Toast.makeText(this, "All permissions granted!", Toast.LENGTH_SHORT).show()
} else {
// Some permissions were denied
val deniedPermissions = permissions.filter { !it.value }.keys
handleDeniedPermissions(deniedPermissions)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cameraButton = findViewById(R.id.cameraButton)
locationButton = findViewById(R.id.locationButton)
// Check if all permissions are already granted
if (PermissionHelper.areAllPermissionsGranted(this)) {
// All permissions granted — proceed
enableFeatures(true)
} else {
// Request missing permissions
requestPermissions()
}
cameraButton.setOnClickListener {
if (PermissionHelper.isPermissionGranted(this, Manifest.permission.CAMERA)) {
openCamera()
} else {
requestPermissions()
}
}
locationButton.setOnClickListener {
if (PermissionHelper.isPermissionGranted(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
getLocation()
} else {
requestPermissions()
}
}
}
private fun requestPermissions() {
val deniedPermissions = PermissionHelper.getDeniedPermissions(this)
if (deniedPermissions.isNotEmpty()) {
// Show rationale for Android 12+ (optional)
if (shouldShowRationale()) {
showPermissionRationale()
} else {
requestPermissionLauncher.launch(deniedPermissions.toTypedArray())
}
}
}
private fun shouldShowRationale(): Boolean {
return PermissionHelper.REQUIRED_PERMISSIONS.any { permission ->
shouldShowRequestPermissionRationale(permission)
}
}
private fun showPermissionRationale() {
// Show a dialog explaining why permissions are needed
AlertDialog.Builder(this)
.setTitle("Permissions Required")
.setMessage("This app needs camera, storage, and location permissions to function properly. Please grant them to continue.")
.setPositiveButton("Grant") { _, _ ->
requestPermissionLauncher.launch(PermissionHelper.getDeniedPermissions(this).toTypedArray())
}
.setNegativeButton("Cancel") { _, _ ->
Toast.makeText(this, "Permissions required for full functionality", Toast.LENGTH_LONG).show()
enableFeatures(false)
}
.show()
}
private fun handleDeniedPermissions(deniedPermissions: Set<String>) {
// Check if permissions are permanently denied
val permanentlyDenied = deniedPermissions.any { permission ->
!shouldShowRequestPermissionRationale(permission)
}
if (permanentlyDenied) {
// Show settings dialog
showSettingsDialog()
} else {
Toast.makeText(this, "Permissions denied. Please grant them to use this feature.", Toast.LENGTH_LONG).show()
enableFeatures(false)
}
}
private fun showSettingsDialog() {
AlertDialog.Builder(this)
.setTitle("Permissions Permanently Denied")
.setMessage("You have permanently denied some permissions. To use all features, please enable them in app settings.")
.setPositiveButton("Open Settings") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun enableFeatures(enabled: Boolean) {
cameraButton.isEnabled = enabled
locationButton.isEnabled = enabled
}
private fun openCamera() {
// Camera is available — open it
Toast.makeText(this, "📷 Opening Camera", Toast.LENGTH_SHORT).show()
}
private fun getLocation() {
// Location is available — get it
Toast.makeText(this, "📍 Getting Location", Toast.LENGTH_SHORT).show()
}
}
6. Permission Groups (Android 12+)
Android 12 introduced the ability to request permissions in groups, allowing users to grant permissions with a single click.
// Request multiple permissions in a group
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
// Handle results
val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
val storageGranted = permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: false
if (cameraGranted && storageGranted) {
// Both granted
} else if (cameraGranted) {
// Only camera granted
} else if (storageGranted) {
// Only storage granted
}
}
7. Android 13+ Changes — Granular Media Permissions
Android 13 introduced granular media permissions, replacing READ_EXTERNAL_STORAGE with specific permissions:
READ_MEDIA_IMAGES— Access imagesREAD_MEDIA_VIDEO— Access videosREAD_MEDIA_AUDIO— Access audio files
// Android 13+ media permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Request specific media permissions
val mediaPermissions = arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
)
requestPermissionLauncher.launch(mediaPermissions)
} else {
// Android 12 and below
requestPermissionLauncher.launch(
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
)
}
8. Background Location Permissions
For apps that need location in the background, you must request ACCESS_BACKGROUND_LOCATION:
// AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
// In Activity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Request background location permission
val permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
requestPermissionLauncher.launch(permissions)
}
9. Common Pitfalls & Solutions
Pitfall 1: Not checking permissions before using features
Problem: Accessing a feature without checking permissions causes a SecurityException.
Solution: Always check checkSelfPermission() before accessing protected features.
Pitfall 2: Not handling "Never Ask Again" state
Problem: Users can permanently deny permissions, and requesting again won't show the dialog.
Solution: Check shouldShowRequestPermissionRationale() to detect permanent denial and guide users to settings.
Pitfall 3: Not providing explanation for permissions
Problem: Users may deny permissions if they don't understand why they're needed.
Solution: Show a rationale dialog explaining why each permission is needed before requesting.
Pitfall 4: Requesting all permissions at once
Problem: Asking for many permissions at once overwhelms users and leads to denials.
Solution: Request permissions only when needed (just-in-time).
10. Production-Ready Checklist
- ✅ Declare permissions — All permissions must be in
AndroidManifest.xml. - ✅ Use runtime permissions — Request dangerous permissions at runtime.
- ✅ Check before use — Always check
checkSelfPermission()before accessing protected features. - ✅ Handle denial — Show rationale for denied permissions.
- ✅ Handle permanent denial — Guide users to settings for permanently denied permissions.
- ✅ Use AndroidX — Use
ActivityResultContracts.RequestMultiplePermissions. - ✅ Request only when needed — Just-in-time permission requests.
- ✅ Show rationale — Explain why permissions are needed.
- ✅ Test denial scenarios — Test with permissions denied and permanently denied.
- ✅ Handle Android 13+ — Use granular media permissions.
- ✅ Handle background location — Request separately if needed.
- ✅ Document permissions — Add to Play Store listing.
11. Quick Reference — Permission Methods
| Method | Description |
|---|---|
checkSelfPermission() | Check if a permission is granted |
requestPermissions() | Request one or more permissions (deprecated) |
ActivityResultContracts.RequestPermission | Modern API for single permission |
ActivityResultContracts.RequestMultiplePermissions | Modern API for multiple permissions |
shouldShowRequestPermissionRationale() | Check if rationale should be shown |
onRequestPermissionsResult() | Handle permission results (deprecated) |
Ready to master Android Development?
Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.
.png)