Runtime Permissions
API Summary (shared)
internal expect object AFPermissions {
@Throws(Throwable::class)
suspend fun isPermissionGranted(permission: Permission): Boolean
@Throws(Throwable::class)
suspend fun requestPermission(permission: Permission)
}
Supported logical permissions (enum Permission): CAMERA, COARSE_LOCATION, FINE_LOCATION, BACKGROUND_LOCATION, PHYSICAL_ACTIVITY, BLUETOOTH_LE, NOTIFICATIONS.
Android Usage
✅ Lifecycle requirement
Register the permission host in your foregroundComponentActivity.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 hasCamera = AFPermissions.isPermissionGranted(Permission.CAMERA)
if (!hasCamera) {
AFPermissions.requestPermission(Permission.CAMERA)
}
}
Request multiple permissions (one sheet where possible)
lifecycleScope.launch {
val statuses = AFPermissions.requestAndGetStatuses(
listOf(
Permission.FINE_LOCATION,
Permission.PHYSICAL_ACTIVITY,
Permission.NOTIFICATIONS // Android 13+
)
)
// Handle statuses[Permission.X] (GRANTED, LIMITED, DENIED, RESTRICTED)
}
Background Location (2-step flow on Android 10+)
lifecycleScope.launch {
val status = AFPermissions.requestAndGetStatus(Permission.BACKGROUND_LOCATION)
// Internally ensures foreground location first, then requests background.
}
BLE / Proximity
lifecycleScope.launch {
val status = AFPermissions.requestAndGetStatus(Permission.BLUETOOTH_LE)
// Android 12L and below maps to Location; Android 12S+ asks for SCAN+CONNECT.
}
Settings shortcuts
AFPermissions.openAppSettings()
AFPermissions.openNotificationsSettings()
iOS Usage
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 granted = try await AFPermissions.shared.isPermissionGranted(permission: .camera)
if !granted {
try await AFPermissions.shared.requestPermission(permission: .camera)
}
} 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, SDK reports 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+) | SDK 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 authorization here, then complete FCM/APNs setup in Push Notifications.
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.