Skip to content

v1.4.2 — Issue #33 Root Cause Fixed

Choose a tag to compare

@vietnguyentuan2019 vietnguyentuan2019 released this 13 May 18:30

What's Fixed

This release resolves the root cause of issue #33LOCATION_ALWAYS requests silently timing out or returning DENIED on Android. The v1.4.1 release addressed symptoms but the underlying wiring was still broken in production.


🔴 Critical Bug Fixes

1. AndroidGrantLauncher.launch() — Callback Never Invoked

Impact: Every request() call on Android (Camera, Microphone, Location, etc.) would hang for the full 5-minute timeout and return DENIED.

Root cause: AndroidGrantLauncher.from() registered a fixed callback at Activity creation time. The per-call onResult passed to launch() was discarded. Since PlatformGrantDelegate uses the per-call onResult to call deferred.complete(), the coroutine suspended forever.

// Before (broken):
override fun launch(permissions: List<String>, onResult: (Map<String, Boolean>) -> Unit) {
    launcher.launch(permissions.toTypedArray())
    // onResult is ignored → deferred.complete() never called → 5-min timeout
}

// After (fixed):
override fun launch(permissions: List<String>, onResult: (Map<String, Boolean>) -> Unit) {
    pendingCallback = onResult          // stored per-call
    launcher.launch(permissions.toTypedArray())
    // registered callback invokes pendingCallback → deferred.complete() called correctly
}

2. BindGrantsController — No-Op on Android (Launcher Never Wired)

Impact: PlatformGrantDelegate.setLauncher() was never called. The launcher == null guard returned DENIED immediately for every request() call.

Fix: BindGrantsController now uses rememberLauncherForActivityResult + SideEffect to wire the launcher into GrantManager on every composition, including after Activity recreation.

3. ReentrantMutex — Context Key Collision on iOS

Impact: Concurrent batch request() calls on iOS could silently deadlock.

Fix: Each ReentrantMutex instance now creates its own unique context key.


✅ Why Tests Were Passing (But Production Was Broken)

All Android unit tests used mock GrantLauncher implementations that correctly called onResult. The production AndroidGrantLauncher did not. This masked both bugs through v1.4.1 — tests green, production broken.


✅ Verified on Device

Full LOCATION_ALWAYS end-to-end flow on Pixel 6 Pro (Android 16):

Step Action Result
1 Tap Request for LOCATION_ALWAYS (NOT_DETERMINED) System foreground dialog appears
2 Grant "While using the app" Library detects PARTIAL_GRANTED, auto-triggers step 2
3 GrantRequestActivity launches background step Android Location Settings opens
4 Select "Allow all the time" → Back App receives result
5 Final status LOCATION_ALWAYS = GRANTED ✓

No timeout. No crash. No DENIED short-circuit.


🏗️ New APIs

GrantLauncher interface

interface GrantLauncher {
    fun launch(permissions: List<String>, onResult: (Map<String, Boolean>) -> Unit)
}

AndroidGrantLauncher

// In Activity.onCreate() — for non-Compose integration:
val launcher = AndroidGrantLauncher.from(this)
grantManager.setLauncher(launcher)

// In Compose — BindGrantsController() handles this automatically

📦 Dependency

// grant-core
implementation("dev.brewkits:grant-core:1.4.2")

// grant-compose (optional)
implementation("dev.brewkits:grant-compose:1.4.2")

Upgrading from 1.4.1

Compose users: No changes needed. BindGrantsController() now does real work on Android — make sure it is called in your root composable.

Non-Compose users: Call grantManager.setLauncher(AndroidGrantLauncher.from(this)) in Activity.onCreate().

AndroidGrantLauncher.from() signature change: The onResult parameter has been removed (it was non-functional in 1.4.1). Update call sites:

// Before (1.4.1 — broken, onResult was never called):
AndroidGrantLauncher.from(activity) { results -> ... }

// After (1.4.2):
AndroidGrantLauncher.from(activity)