Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
```

Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.ionic.libs</groupId>
<artifactId>iongeolocation-android</artifactId>
<version>2.2.1</version>
<version>2.2.2</version>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class IONGLOCController internal constructor(
sensorHandler = IONGLOCSensorHandler(context)
)

private lateinit var resolveLocationSettingsResultFlow: MutableSharedFlow<Result<Unit>>
private val resolveLocationSettingsResultFlow = MutableSharedFlow<Result<Unit>>()
private val watchLocationHandlers: MutableMap<String, LocationHandler> = mutableMapOf()
private val watchIdsBlacklist: MutableList<String> = mutableListOf()

Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResolvableApiException> {
every { resolution } returns mockk<PendingIntent>(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<Activity>(), locationOptions) }
val deferred2 = async { sut.getCurrentPosition(mockk<Activity>(), 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<ResolvableApiException> {
every { resolution } returns mockk<PendingIntent>(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<Activity>(), locationOptions) }
val deferred2 = async { sut.getCurrentPosition(mockk<Activity>(), 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
Expand Down
Loading