Skip to content

Commit 4dd9109

Browse files
authored
fix(android): inline VpnTileService IO/Main dispatcher switch and guard on isActive (#16)
- Replace nested `launch(Dispatchers.Main)` inside IO coroutine with a single `tileScope.launch` using `withContext(Dispatchers.IO)` for the Room DB read - Add `isActive` guard after the DB read returns to abort if the tile service was torn down (onDestroy cancelled the scope while reading) - Extract `TileAction` enum + `tileActionForSelectedProfile()` as a testable pure function (decision logic separated from Android framework) - Add VpnTileServiceConnectLogicTest with 2 JUnit4+Truth test cases covering both null and non-null profile paths
1 parent bc00926 commit 4dd9109

2 files changed

Lines changed: 54 additions & 11 deletions

File tree

android/app/src/main/java/com/gooserelay/gooserelayvpn/service/VpnTileService.kt

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import kotlinx.coroutines.CoroutineScope
1414
import kotlinx.coroutines.Dispatchers
1515
import kotlinx.coroutines.SupervisorJob
1616
import kotlinx.coroutines.cancel
17+
import kotlinx.coroutines.isActive
1718
import kotlinx.coroutines.launch
19+
import kotlinx.coroutines.withContext
1820

1921
@RequiresApi(Build.VERSION_CODES.N)
2022
class VpnTileService : TileService() {
@@ -49,22 +51,41 @@ class VpnTileService : TileService() {
4951
return
5052
}
5153

52-
tileScope.launch(Dispatchers.IO) {
53-
val selectedProfile = AppDatabase.getInstance(this@VpnTileService)
54-
.profileDao()
55-
.getSelectedProfile()
54+
tileScope.launch {
55+
// Read the selected profile on a background thread — Room
56+
// call must not block the system's main thread or ANR the
57+
// quick-settings shade.
58+
val selectedProfile = withContext(Dispatchers.IO) {
59+
AppDatabase.getInstance(this@VpnTileService)
60+
.profileDao()
61+
.getSelectedProfile()
62+
}
63+
64+
// If the tile service was torn down while we were reading
65+
// the DB (user dismissed the shade, or Android killed the
66+
// service), abandon the connect — tileScope is cancelled
67+
// in onDestroy, but cancellation is cooperative.
68+
if (!isActive) return@launch
5669

57-
launch(Dispatchers.Main) {
58-
if (selectedProfile != null) {
59-
VpnManager.connect(this@VpnTileService, selectedProfile)
60-
updateTile()
61-
} else {
62-
openApp()
63-
}
70+
// Back on Dispatchers.Main (the scope's default dispatcher).
71+
if (selectedProfile != null) {
72+
VpnManager.connect(this@VpnTileService, selectedProfile)
73+
updateTile()
74+
} else {
75+
openApp()
6476
}
6577
}
6678
}
6779

80+
/**
81+
* Test-only: returns the action the tile should take given the current
82+
* selected profile. Pure function — does not touch Android framework.
83+
*/
84+
internal enum class TileAction { CONNECT, OPEN_APP }
85+
86+
internal fun tileActionForSelectedProfile(selectedProfile: com.gooserelay.gooserelayvpn.data.local.ProfileEntity?): TileAction =
87+
if (selectedProfile != null) TileAction.CONNECT else TileAction.OPEN_APP
88+
6889
private fun openApp() {
6990
val intent = Intent(this, MainActivity::class.java)
7091
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.gooserelay.gooserelayvpn
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import com.gooserelay.gooserelayvpn.data.local.ProfileEntity
5+
import com.gooserelay.gooserelayvpn.service.VpnTileService
6+
import org.junit.Test
7+
8+
class VpnTileServiceConnectLogicTest {
9+
10+
@Test
11+
fun `null profile returns OPEN_APP`() {
12+
val action = VpnTileService().tileActionForSelectedProfile(null)
13+
assertThat(action).isEqualTo(VpnTileService.TileAction.OPEN_APP)
14+
}
15+
16+
@Test
17+
fun `non-null profile returns CONNECT`() {
18+
val profile = ProfileEntity(name = "test", tunnelKey = "x")
19+
val action = VpnTileService().tileActionForSelectedProfile(profile)
20+
assertThat(action).isEqualTo(VpnTileService.TileAction.CONNECT)
21+
}
22+
}

0 commit comments

Comments
 (0)