Problem: Grant GRANTED ≠ Feature Working
When a user grants location grant, it doesn't mean location features will work. The GPS service might be disabled. Same for Bluetooth, WiFi, and other system services.
Real-world scenarios:
- ✅ Location grant: GRANTED
- ❌ GPS service: DISABLED
- 🔴 Result: Location features don't work!
This library provides a complete solution: check both grants AND services.
class LocationViewModel(
private val checker: GrantAndServiceChecker
) : ViewModel() {
fun startLocationTracking() = viewModelScope.launch {
when (val status = checker.checkLocationReady()) {
is LocationReadyStatus.Ready -> {
// ✅ Everything is ready!
startGpsTracking()
}
is LocationReadyStatus.GrantDenied -> {
// ❌ Grant denied
showGrantDialog()
}
is LocationReadyStatus.ServiceDisabled -> {
// ❌ GPS disabled
showEnableGpsDialog()
}
is LocationReadyStatus.BothRequired -> {
// ❌ Both grant denied AND GPS disabled
showBothRequiredDialog()
}
is LocationReadyStatus.Unknown -> {
// ⚠️ Unable to determine status
showErrorDialog()
}
}
}
}fun startBluetoothScan() = viewModelScope.launch {
when (checker.checkBluetoothReady()) {
is BluetoothReadyStatus.Ready -> startBleScanning()
is BluetoothReadyStatus.GrantDenied -> showGrantDialog()
is BluetoothReadyStatus.ServiceDisabled -> showEnableBluetoothDialog()
is BluetoothReadyStatus.BothRequired -> showBothRequiredDialog()
else -> showErrorDialog()
}
}// Check any grant + service combination
val status = checker.checkReady(
grant = AppGrant.LOCATION,
serviceType = ServiceType.LOCATION_GPS
)
if (status.isReady) {
startFeature()
} else {
showDialog(status.message) // "Grant and service both required"
}The library supports checking these system services:
enum class ServiceType {
LOCATION_GPS, // GPS/Location service
BLUETOOTH, // Bluetooth service
WIFI, // Wi-Fi service
NFC, // NFC service (Android only)
CAMERA_HARDWARE // Camera hardware availability
}Each service can have one of these states:
enum class ServiceStatus {
ENABLED, // ✅ Service is ready to use
DISABLED, // ❌ Service disabled by user (e.g., GPS off)
NOT_AVAILABLE, // ⚠️ Service not available on device (e.g., no NFC chip)
UNKNOWN // ❓ Unable to determine status
}For more control, use ServiceManager directly:
class MyViewModel(
private val serviceManager: ServiceManager
) : ViewModel() {
suspend fun checkGps() {
val status = serviceManager.checkServiceStatus(ServiceType.LOCATION_GPS)
when (status) {
ServiceStatus.ENABLED -> {
// GPS is on
}
ServiceStatus.DISABLED -> {
// Prompt user to enable GPS
if (serviceManager.openServiceSettings(ServiceType.LOCATION_GPS)) {
// Settings opened successfully
}
}
ServiceStatus.NOT_AVAILABLE -> {
// Device has no GPS
showNoGpsDialog()
}
ServiceStatus.UNKNOWN -> {
// Unable to check
showErrorDialog()
}
}
}
suspend fun isWifiEnabled(): Boolean {
return serviceManager.isServiceEnabled(ServiceType.WIFI)
}
}Service checking requires Android Context. Initialize in your Application class:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// REQUIRED: Initialize ServiceFactory with context
ServiceFactory.init(this)
// Start Koin
startKoin {
modules(
grantModule, // Includes ServiceManager
grantPlatformModule
)
}
}
}No initialization needed! ServiceFactory works automatically on iOS.
The library provides automatic DI setup:
val grantModule = module {
// Grant Manager
single<grantManager> { MyGrantManager(...) }
// Service Manager (automatically included)
single<ServiceManager> { ServiceFactory.createServiceManager() }
// Combined Checker (automatically included)
single { GrantAndServiceChecker(grantManager = get(), serviceManager = get()) }
}Just inject where needed:
class MyViewModel(
private val checker: GrantAndServiceChecker // Auto-injected
) : ViewModel()| Service | Can Check | Can Open Settings |
|---|---|---|
| Location/GPS | ✅ | ✅ |
| Bluetooth | ✅ | ✅ |
| Wi-Fi | ✅ | ✅ |
| NFC | ✅ | ✅ |
| Camera Hardware | ✅ | ✅ (General) |
| Service | Can Check | Can Open Settings |
|---|---|---|
| Location/GPS | ✅ | ✅ (Main Settings) |
| Bluetooth | ✅ (Main Settings) | |
| Wi-Fi | ❌ | ✅ (Main Settings) |
| NFC | ❌ | ✅ (Main Settings) |
| Camera Hardware | ❌ | ✅ (Main Settings) |
iOS Limitations:
- Bluetooth: Checking requires CBCentralManager, which is complex. Currently returns
ENABLEDby default. - Wi-Fi/NFC: iOS doesn't provide APIs to check these programmatically.
- Settings: iOS can only open the main Settings app, not specific service settings.
- openServiceSettings() return value: On iOS, this method returns
trueimmediately without waiting for the Settings app to actually open. The return value indicates whether the Settings URL is valid, not whether Settings opened successfully. This is an iOS platform limitation due to async callback behavior.
// ❌ BAD: Only checking grant
if (grantManager.checkStatus(AppGrant.LOCATION) == GrantStatus.GRANTED) {
startTracking() // Might fail if GPS is off!
}
// ✅ GOOD: Check both grant and service
when (checker.checkLocationReady()) {
LocationReadyStatus.Ready -> startTracking()
else -> showAppropriateDialog()
}when (checker.checkLocationReady()) {
is LocationReadyStatus.ServiceDisabled -> {
showDialog(
title = "GPS is Off",
message = "Please turn on GPS to use location features",
action = "Enable GPS"
) {
serviceManager.openServiceSettings(ServiceType.LOCATION_GPS)
}
}
// ...
}Services can be enabled/disabled at any time. Check before each use:
fun startFeature() = viewModelScope.launch {
// Always check before using the feature
if (checker.checkLocationReady() is LocationReadyStatus.Ready) {
startGpsTracking()
} else {
showEnableGpsPrompt()
}
}class LocationFeatureViewModel(
private val grantHandler: GrantHandler,
private val checker: GrantAndServiceChecker
) : ViewModel() {
fun enableLocationFeature() {
// Step 1: Request grant
grantHandler.request {
// Step 2: Check service after grant granted
viewModelScope.launch {
when (checker.checkLocationReady()) {
LocationReadyStatus.Ready -> startFeature()
LocationReadyStatus.ServiceDisabled -> promptEnableGps()
else -> showError()
}
}
}
}
}@Composable
fun LocationReadyHandler(checker: GrantAndServiceChecker, onReady: () -> Unit) {
val status by rememberUpdatedState(checker.checkLocationReady())
when (val s = status) {
LocationReadyStatus.Ready -> {
LaunchedEffect(Unit) { onReady() }
}
is LocationReadyStatus.ServiceDisabled -> {
AlertDialog(
onDismissRequest = { /* dismiss */ },
title = { Text("GPS is Off") },
text = { Text("Please enable GPS to continue") },
confirmButton = {
Button(onClick = {
serviceManager.openServiceSettings(ServiceType.LOCATION_GPS)
}) {
Text("Enable GPS")
}
}
)
}
// ... handle other cases
}
}private fun showEnableGpsDialog() {
MaterialAlertDialogBuilder(context)
.setTitle("GPS Required")
.setMessage("Location features require GPS to be enabled")
.setPositiveButton("Enable GPS") { _, _ ->
lifecycleScope.launch {
serviceManager.openServiceSettings(ServiceType.LOCATION_GPS)
}
}
.setNegativeButton("Cancel", null)
.show()
}class MockServiceManager : ServiceManager {
var gpsEnabled = true
var bluetoothEnabled = true
override suspend fun checkServiceStatus(service: ServiceType): ServiceStatus {
return when (service) {
ServiceType.LOCATION_GPS ->
if (gpsEnabled) ServiceStatus.ENABLED else ServiceStatus.DISABLED
ServiceType.BLUETOOTH ->
if (bluetoothEnabled) ServiceStatus.ENABLED else ServiceStatus.DISABLED
else -> ServiceStatus.NOT_AVAILABLE
}
}
override suspend fun isServiceEnabled(service: ServiceType): Boolean {
return checkServiceStatus(service) == ServiceStatus.ENABLED
}
override suspend fun openServiceSettings(service: ServiceType): Boolean = true
}@Test
fun `when GPS disabled should show service disabled status`() = runTest {
val mockServiceManager = MockServiceManager().apply {
gpsEnabled = false
}
val checker = GrantAndServiceChecker(
grantManager = mockgrantManager,
serviceManager = mockServiceManager
)
val status = checker.checkLocationReady()
assertTrue(status is LocationReadyStatus.ServiceDisabled)
}Error: IllegalStateException: ServiceFactory not initialized
Solution: Call ServiceFactory.init(context) in Application.onCreate():
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
ServiceFactory.init(this) // Add this!
}
}This is expected. iOS doesn't provide a simple API to check Bluetooth status. To properly check Bluetooth on iOS, you need to use CBCentralManager, which is more complex and beyond the scope of this library's service checking.
This means the library couldn't determine the service status. Possible causes:
- Security restrictions
- Device-specific limitations
- Platform API unavailable
Treat UNKNOWN as "might not work" and provide appropriate fallback UI.
- Grants Guide - Grant management
- Quick Start - Basic setup
- Architecture - System design