Back to Course
Security

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.

Production Reality: Permission handling is one of the most critical aspects of Android development. A poorly implemented permission flow leads to app crashes, frustrated users, and negative reviews. Mastering runtime permissions is essential for building professional, user-friendly apps.

1. Types of Android Permissions

Android permissions are categorized into three levels of protection:

TypeDescriptionExamples
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

PermissionManifest DeclarationUse 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

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
    }
}
Pro Tip: Request related permissions together (e.g., camera and storage) to reduce the number of prompts. Users appreciate fewer permission dialogs.

7. Android 13+ Changes — Granular Media Permissions

Android 13 introduced granular media permissions, replacing READ_EXTERNAL_STORAGE with specific permissions:

  • READ_MEDIA_IMAGES — Access images
  • READ_MEDIA_VIDEO — Access videos
  • READ_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)
}
Production Warning: Background location permission requires additional review from Google Play. You must provide a valid reason for using background location in your app's Play Store listing.

9. Common Pitfalls & Solutions

Common Mistakes to Avoid:

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

MethodDescription
checkSelfPermission()Check if a permission is granted
requestPermissions()Request one or more permissions (deprecated)
ActivityResultContracts.RequestPermissionModern API for single permission
ActivityResultContracts.RequestMultiplePermissionsModern API for multiple permissions
shouldShowRequestPermissionRationale()Check if rationale should be shown
onRequestPermissionsResult()Handle permission results (deprecated)
Key Takeaway: Permission handling is essential for user trust and app functionality. Use the modern ActivityResult API, show rationales, handle denials gracefully, and always check permissions before accessing protected features. Testing all permission scenarios (granted, denied, permanently denied) is critical for a production-ready app.

Ready to master Android Development?

Build real-world Android apps with hands-on projects, mentor-led sessions, and placement support.

Explore Course