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
- Android (Kotlin)
- iOS (Swift)
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
iOS uses the
actualimplementation that bridges to native frameworks (CoreLocation,CoreBluetooth,CMMotion,AVFoundation,UserNotifications,HealthKit). Swift Concurrency is supported: call fromTask {}usingtry await.
Check / Request
import AFCore
Task {
do {
let status = try await AFPermissions.shared.getPermissionStatus(permission: .camera)
if status != .granted {
let result = try await AFPermissions.shared.requestPermission(permission: .camera)
// result is .granted, .denied, .limited, or .restricted
}
} catch {
// Handle errors surfaced from the platform implementation
print("Permissions error: \(error)")
}
}
Background vs When-In-Use Location
Task {
// Request Always (the implementation internally sequences WhenInUse → Always when needed)
try await AFPermissions.shared.requestPermission(permission: .backgroundLocation)
}
Notifications
Task {
try await AFPermissions.shared.requestPermission(permission: .notifications)
// For push, also see: Device Utilities → Push Notifications
}
Open Settings
AFPermissions.shared.openAppSettings()
Permission Reference
| Logical Permission | Android Runtime Gate (by API level) | iOS Info.plist Keys | Notes |
|---|---|---|---|
| CAMERA | android.permission.CAMERA | NSCameraUsageDescription | Show a user rationale before prompting. |
| COARSE_LOCATION | ACCESS_COARSE_LOCATION | NSLocationWhenInUseUsageDescription | Coarse ≈ approx location on Android; on iOS use When-In-Use. |
| FINE_LOCATION | ACCESS_FINE_LOCATION | NSLocationWhenInUseUsageDescription | If only coarse is granted on Android, getPermissionStatus returns LIMITED. |
| BACKGROUND_LOCATION | ACCESS_BACKGROUND_LOCATION (API 29+) | NSLocationAlwaysAndWhenInUseUsageDescription (also WhenInUse) | Android is 2-step (FG → BG). iOS maps When-In-Use → Always where applicable. |
| PHYSICAL_ACTIVITY | ACTIVITY_RECOGNITION (API 29+) | NSMotionUsageDescription | Pre-29 Android has no runtime gate (treated as granted). |
| BLUETOOTH_LE | BLUETOOTH_SCAN, BLUETOOTH_CONNECT (API 31+). On 29–30: Location. | NSBluetoothAlwaysUsageDescription (iOS 13+) | getPermissionStatus normalizes to GRANTED / LIMITED / DENIED / RESTRICTED. |
| NOTIFICATIONS | POST_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 foregroundActivity.onCreate(). - iOS: no prompt shows → Missing Info.plist key; see the table above.