Skip to main content

Runtime Permissions

API Summary (shared)

expect object AFPermissions {
@Throws(Throwable::class)
suspend fun getPermissionStatus(permission: Permission): PermissionStatus

@Throws(Throwable::class)
suspend fun requestPermission(permission: Permission): PermissionStatus
}

PermissionStatus values: GRANTED, DENIED, LIMITED, PROVISIONAL, RESTRICTED, NOT_DETERMINED.

Supported logical permissions (enum Permission): CAMERA, COARSE_LOCATION, FINE_LOCATION, BACKGROUND_LOCATION, PHYSICAL_ACTIVITY, BLUETOOTH_LE, NOTIFICATIONS.


Platform Usage

Lifecycle requirement Register the permission host in your foreground ComponentActivity.onCreate().

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AFPermissions.registerLifecycle(this)
// ... UI setup
}
}

Check / Request a single permission

lifecycleScope.launch {
val status = AFPermissions.getPermissionStatus(Permission.CAMERA)
if (status != PermissionStatus.GRANTED) {
val result = AFPermissions.requestPermission(Permission.CAMERA)
// result is GRANTED, DENIED, LIMITED, or RESTRICTED
}
}

Request multiple permissions sequentially

lifecycleScope.launch {
AFPermissions.requestPermission(Permission.FINE_LOCATION)
AFPermissions.requestPermission(Permission.PHYSICAL_ACTIVITY)
AFPermissions.requestPermission(Permission.NOTIFICATIONS) // Android 13+
}

Background Location (2-step flow on Android 10+)

lifecycleScope.launch {
// Request foreground location first
AFPermissions.requestPermission(Permission.FINE_LOCATION)
// Then request background location
AFPermissions.requestPermission(Permission.BACKGROUND_LOCATION)
}

BLE / Proximity

lifecycleScope.launch {
val status = AFPermissions.requestPermission(Permission.BLUETOOTH_LE)
// Android 12L and below maps to Location; Android 12S+ asks for SCAN+CONNECT.
}

Settings shortcuts

AFPermissions.openAppSettings()
AFPermissions.openNotificationsSettings() // Android only

Permission Reference

Logical PermissionAndroid Runtime Gate (by API level)iOS Info.plist KeysNotes
CAMERAandroid.permission.CAMERANSCameraUsageDescriptionShow a user rationale before prompting.
COARSE_LOCATIONACCESS_COARSE_LOCATIONNSLocationWhenInUseUsageDescriptionCoarse ≈ approx location on Android; on iOS use When-In-Use.
FINE_LOCATIONACCESS_FINE_LOCATIONNSLocationWhenInUseUsageDescriptionIf only coarse is granted on Android, getPermissionStatus returns LIMITED.
BACKGROUND_LOCATIONACCESS_BACKGROUND_LOCATION (API 29+)NSLocationAlwaysAndWhenInUseUsageDescription (also WhenInUse)Android is 2-step (FG → BG). iOS maps When-In-Use → Always where applicable.
PHYSICAL_ACTIVITYACTIVITY_RECOGNITION (API 29+)NSMotionUsageDescriptionPre-29 Android has no runtime gate (treated as granted).
BLUETOOTH_LEBLUETOOTH_SCAN, BLUETOOTH_CONNECT (API 31+). On 29–30: Location.NSBluetoothAlwaysUsageDescription (iOS 13+)getPermissionStatus normalizes to GRANTED / LIMITED / DENIED / RESTRICTED.
NOTIFICATIONSPOST_NOTIFICATIONS (API 33+)(none required)iOS authorization via UNUserNotificationCenter; see Push Notifications for APNs/FCM wiring.

Common Patterns & Tips

  • Just-in-time prompts: Ask only when a feature is used.
  • Rationales: Show an in-app screen explaining "why" before system dialogs.
  • Denied vs Restricted: On Android, "Don't ask again" is mapped to RESTRICTED; on iOS some frameworks return RESTRICTED for parental controls/MDM.
  • Testing flows: Simulators may not exercise all paths (e.g., Bluetooth, Motion). Use real hardware.
  • Push notifications: Request notification authorization to enable push notifications via FCM (Android) or APNs (iOS).

Troubleshooting

  • Background geofencing not firing (iOS) → Ensure Always Location and background modes are enabled; verify region limits.
  • BLE scan returns no results → Check that the correct permission gate is used for the OS version.
  • Android: host not registered → Call AFPermissions.registerLifecycle(activity) in the foreground Activity.onCreate().
  • iOS: no prompt shows → Missing Info.plist key; see the table above.