diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index c91eb2b..e9bcdb1 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -22,6 +22,11 @@ jobs: distribution: 'zulu' java-version: '17' + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + - name: Bundle Install run: bundle install diff --git a/CHANGELOG.md b/CHANGELOG.md index c4e6003..ff5e130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.2] + +### 2026-08-04 + +- Fix: The result of the enable-location dialog was delivered only to the most recent request, leaving earlier concurrent requests waiting forever [RMET-5383](https://outsystemsrd.atlassian.net/browse/RMET-5383). + ## [2.2.1] ### 2026-06-15 diff --git a/README.md b/README.md index 79247fd..5aed36e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ In your app-level gradle file, import the `ion-android-geolocation` library like ``` dependencies { - implementation("io.ionic.libs:iongeolocation-android:2.2.1") + implementation("io.ionic.libs:iongeolocation-android:2.2.2") } ``` diff --git a/build.gradle b/build.gradle index c0e3f23..22db03a 100644 --- a/build.gradle +++ b/build.gradle @@ -46,8 +46,8 @@ android { defaultConfig { minSdk 23 targetSdk 36 - versionCode 4 - versionName "2.2.1" + versionCode 5 + versionName "2.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/pom.xml b/pom.xml index f95b78a..11665ef 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs iongeolocation-android - 2.2.1 + 2.2.2 \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/iongeolocationlib/controller/IONGLOCController.kt b/src/main/kotlin/io/ionic/libs/iongeolocationlib/controller/IONGLOCController.kt index 160ed46..663610b 100644 --- a/src/main/kotlin/io/ionic/libs/iongeolocationlib/controller/IONGLOCController.kt +++ b/src/main/kotlin/io/ionic/libs/iongeolocationlib/controller/IONGLOCController.kt @@ -70,7 +70,7 @@ class IONGLOCController internal constructor( sensorHandler = IONGLOCSensorHandler(context) ) - private lateinit var resolveLocationSettingsResultFlow: MutableSharedFlow> + private val resolveLocationSettingsResultFlow = MutableSharedFlow>() private val watchLocationHandlers: MutableMap = mutableMapOf() private val watchIdsBlacklist: MutableList = mutableListOf() @@ -275,7 +275,6 @@ class IONGLOCController internal constructor( return Result.failure(playServicesResult.exceptionOrNull() ?: NullPointerException()) } - resolveLocationSettingsResultFlow = MutableSharedFlow() val locationSettingsResult = googleServicesHelper.checkLocationSettings( activity, options.copy(timeout = if (isSingleLocationRequest) 0 else options.timeout), diff --git a/src/test/java/io/ionic/libs/iongeolocationlib/controller/IONGLOCControllerTest.kt b/src/test/java/io/ionic/libs/iongeolocationlib/controller/IONGLOCControllerTest.kt index aa665c0..36b1636 100644 --- a/src/test/java/io/ionic/libs/iongeolocationlib/controller/IONGLOCControllerTest.kt +++ b/src/test/java/io/ionic/libs/iongeolocationlib/controller/IONGLOCControllerTest.kt @@ -272,6 +272,74 @@ class IONGLOCControllerTest { } } + @Test + fun `given two concurrent getCurrentPosition calls both need location settings resolved, when user accepts, both calls receive the location`() = + runTest { + givenSuccessConditions() + coEvery { locationSettingsTask.await() } throws mockk { + every { resolution } returns mockk(relaxed = true) + } + // Simulate Android behaviour: only one activity result callback fires regardless of + // how many times the launcher is invoked (e.g. a second overlapping call). + var launched = false + val testScope = this + coEvery { activityResultLauncher.launch(any()) } coAnswers { + if (!launched) { + launched = true + testScope.launch { + delay(DELAY) + sut.onResolvableExceptionResult(Activity.RESULT_OK) + } + } + } + + val deferred1 = async { sut.getCurrentPosition(mockk(), locationOptions) } + val deferred2 = async { sut.getCurrentPosition(mockk(), locationOptions) } + runCurrent() // let both coroutines start and suspend on resolveLocationSettingsResultFlow.first() + advanceTimeBy(DELAY) // fire onResolvableExceptionResult; shared flow notifies both + + val result1 = deferred1.await() + val result2 = deferred2.await() + + assertTrue(result1.isSuccess) + assertEquals(locationResult, result1.getOrNull()) + assertTrue(result2.isSuccess) + assertEquals(locationResult, result2.getOrNull()) + } + + @Test + fun `given two concurrent getCurrentPosition calls both need location settings resolved, when user denies, both calls receive IONGLOCRequestDeniedException`() = + runTest { + givenSuccessConditions() + coEvery { locationSettingsTask.await() } throws mockk { + every { resolution } returns mockk(relaxed = true) + } + var launched = false + val testScope = this + coEvery { activityResultLauncher.launch(any()) } coAnswers { + if (!launched) { + launched = true + testScope.launch { + delay(DELAY) + sut.onResolvableExceptionResult(Activity.RESULT_CANCELED) + } + } + } + + val deferred1 = async { sut.getCurrentPosition(mockk(), locationOptions) } + val deferred2 = async { sut.getCurrentPosition(mockk(), locationOptions) } + runCurrent() + advanceTimeBy(DELAY) + + val result1 = deferred1.await() + val result2 = deferred2.await() + + assertTrue(result1.isFailure) + assertTrue(result1.exceptionOrNull() is IONGLOCException.IONGLOCRequestDeniedException) + assertTrue(result2.isFailure) + assertTrue(result2.exceptionOrNull() is IONGLOCException.IONGLOCRequestDeniedException) + } + // endregion getCurrentLocation tests // region addWatch tests