Skip to content

Commit 6bf37a4

Browse files
committed
Modify state management so that the app's screen can reconnect no matter what event causes it to regenerate.
1 parent 8f68a48 commit 6bf37a4

File tree

7 files changed

+109
-85
lines changed

7 files changed

+109
-85
lines changed

thunder-okhttp/src/main/java/com/jeremy/thunder/OkHttpWebSocket.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class OkHttpWebSocket internal constructor(
1919
private val scope: CoroutineScope
2020
) : WebSocket {
2121

22-
private val _event = MutableStateFlow<com.jeremy.thunder.event.WebSocketEvent?>(null)
22+
private val _event = MutableStateFlow<WebSocketEvent?>(null)
2323
override fun open() {
2424
socketListener.collectEvent().onStart {
2525
provider.provide(socketListener)
@@ -34,7 +34,7 @@ class OkHttpWebSocket internal constructor(
3434
}.launchIn(scope)
3535
}
3636

37-
override fun events(): Flow<com.jeremy.thunder.event.WebSocketEvent> {
37+
override fun events(): Flow<WebSocketEvent> {
3838
return _event.asSharedFlow().filterNotNull()
3939
}
4040

thunder/src/main/java/com/jeremy/thunder/connection/AppConnectionProvider.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import android.app.Activity
44
import android.app.Application
55
import android.os.Bundle
66
import com.jeremy.thunder.state.ActivityState
7-
import com.jeremy.thunder.state.GetReady
7+
import com.jeremy.thunder.state.Background
8+
import com.jeremy.thunder.state.Foreground
89
import com.jeremy.thunder.state.Initialize
910
import com.jeremy.thunder.state.ShutDown
1011
import kotlinx.coroutines.flow.MutableStateFlow
@@ -20,22 +21,24 @@ class AppConnectionProvider : AppConnectionListener, Application.ActivityLifecyc
2021
}
2122

2223
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
23-
_eventFlow.tryEmit(GetReady)
24+
_eventFlow.tryEmit(Initialize)
2425
}
2526

2627
override fun onActivityStarted(p0: Activity) {
27-
_eventFlow.tryEmit(GetReady)
28+
_eventFlow.tryEmit(Foreground)
2829
}
2930

3031
override fun onActivityResumed(p0: Activity) {
31-
_eventFlow.tryEmit(GetReady)
32+
_eventFlow.tryEmit(Foreground)
3233
}
3334

3435
override fun onActivityPaused(p0: Activity) {}
3536

3637
override fun onActivityStopped(p0: Activity) {}
3738

38-
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
39+
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
40+
_eventFlow.tryEmit(Background)
41+
}
3942

4043
override fun onActivityDestroyed(p0: Activity) {
4144
_eventFlow.tryEmit(ShutDown)

thunder/src/main/java/com/jeremy/thunder/internal/ThunderStateManager.kt

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import com.jeremy.thunder.connection.AppConnectionListener
77
import com.jeremy.thunder.coroutine.CoroutineScope.scope
88
import com.jeremy.thunder.event.WebSocketEvent
99
import com.jeremy.thunder.network.NetworkConnectivityService
10-
import com.jeremy.thunder.state.GetReady
10+
import com.jeremy.thunder.state.Background
11+
import com.jeremy.thunder.state.Foreground
1112
import com.jeremy.thunder.state.Initialize
1213
import com.jeremy.thunder.state.NetworkState
1314
import com.jeremy.thunder.state.ShutDown
1415
import com.jeremy.thunder.state.ThunderError
1516
import com.jeremy.thunder.state.ThunderState
17+
import com.jeremy.thunder.thunderLog
1618
import com.jeremy.thunder.ws.WebSocket
19+
import kotlinx.coroutines.CoroutineExceptionHandler
1720
import kotlinx.coroutines.CoroutineScope
1821
import kotlinx.coroutines.Job
1922
import kotlinx.coroutines.delay
@@ -22,9 +25,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
2225
import kotlinx.coroutines.flow.asSharedFlow
2326
import kotlinx.coroutines.flow.asStateFlow
2427
import kotlinx.coroutines.flow.combine
25-
import kotlinx.coroutines.flow.getAndUpdate
2628
import kotlinx.coroutines.flow.launchIn
2729
import kotlinx.coroutines.flow.onEach
30+
import kotlinx.coroutines.flow.update
31+
import kotlinx.coroutines.plus
2832

2933
/**
3034
* Manage ThunderState as SocketState using NetworkState
@@ -39,19 +43,23 @@ class ThunderStateManager private constructor(
3943
private val webSocketCore: WebSocket.Factory,
4044
private val scope: CoroutineScope
4145
) {
46+
private val innerScope = scope + CoroutineExceptionHandler { _, throwable ->
47+
thunderLog("[ThunderStateManager] = ${throwable.message}")
48+
}
49+
4250
private var socket: WebSocket? = null
4351

4452
private val _socketState = MutableStateFlow<ThunderState>(ThunderState.IDLE)
4553

4654
private val _events = MutableSharedFlow<WebSocketEvent>(replay = 1)
4755

48-
private var _lastSocketState: ThunderState = ThunderState.IDLE
49-
5056
/**
5157
* If the device loses the network or the socket connection fails, it enters the error state below.
5258
* This is used to use the cache for recovery when a [ThunderState.CONNECTED] is reached.
5359
* */
54-
private var isFromError = false
60+
private var isReSubscription = false
61+
62+
private val _retryNeedFlag = MutableStateFlow<Boolean>(false)
5563

5664
fun thunderStateAsFlow() = _socketState.asStateFlow()
5765

@@ -61,85 +69,98 @@ class ThunderStateManager private constructor(
6169

6270
init {
6371
/**
64-
* The following code is used to open the valve based on the socket state.
65-
* */
66-
_socketState.onEach {
67-
if (it is ThunderState.ERROR && networkState.hasAvailableNetworks()) {
68-
closeConnection()
69-
delay(500)
70-
openConnection()
71-
}
72-
valveCache.onUpdateValveState(it)
73-
}.launchIn(scope)
74-
75-
/**
76-
* When an app is present in a process but offscreen, it automatically controls the connection based on two states to maintain the connection in the meantime.
72+
* Update SocketState as ThunderState.
7773
* */
78-
connectionListener.collectState().onEach {
79-
when(it) {
80-
Initialize -> Unit
81-
GetReady -> openConnection()
82-
ShutDown -> closeConnection()
74+
_events.onEach { event ->
75+
when (event) {
76+
is WebSocketEvent.OnConnectionOpen -> {
77+
_socketState.update { ThunderState.CONNECTED }
78+
}
79+
80+
is WebSocketEvent.OnMessageReceived -> Unit
81+
82+
WebSocketEvent.OnConnectionClosed -> {
83+
_socketState.update { ThunderState.DISCONNECTED }
84+
}
85+
86+
is WebSocketEvent.OnConnectionError -> {
87+
isReSubscription = true
88+
_socketState.update { ThunderState.ERROR(ThunderError.SocketLoss(event.error)) }
89+
}
8390
}
84-
}.launchIn(scope)
91+
}.launchIn(innerScope)
8592

8693
/**
87-
* Used to change the ThunderState based on the device's network connection status.
88-
* */
89-
networkState.networkStatus.onEach {
90-
when (it) {
94+
* Open, Retry Connection work as network state.
95+
* */
96+
combine(
97+
_retryNeedFlag,
98+
networkState.networkStatus
99+
) { retry, network ->
100+
when (network) {
91101
NetworkState.Available -> {
92-
openConnection()
102+
if (retry) {
103+
retryConnection()
104+
} else {
105+
openConnection()
106+
}
93107
}
94-
95108
NetworkState.Unavailable -> {
96-
isFromError = true
97-
_socketState.updateThunderState(ThunderState.ERROR(ThunderError.NetworkLoss(null)))
98-
closeConnection()
109+
_socketState.update { ThunderState.ERROR(ThunderError.NetworkLoss) }
99110
}
100111
}
101-
}.launchIn(scope)
112+
}.launchIn(innerScope)
102113

103-
_events.onEach {
104-
when (it) {
105-
is WebSocketEvent.OnConnectionOpen -> {
106-
_socketState.updateThunderState(ThunderState.CONNECTED)
114+
/**
115+
* Update RetryFlag And Valve And request socket message as upstream state.
116+
* */
117+
combine(
118+
_socketState,
119+
connectionListener.collectState()
120+
) { socketState, appState ->
121+
when (appState) {
122+
Initialize -> {
123+
openConnection()
107124
}
108-
109-
is WebSocketEvent.OnMessageReceived -> Unit
110-
111-
WebSocketEvent.OnConnectionClosed -> {
112-
_socketState.updateThunderState(ThunderState.DISCONNECTED)
125+
Foreground -> {
126+
if (socketState is ThunderState.ERROR) {
127+
_retryNeedFlag.update { true }
128+
}
113129
}
114-
115-
is WebSocketEvent.OnConnectionError -> {
116-
isFromError = true
117-
_socketState.updateThunderState(ThunderState.ERROR(ThunderError.SocketLoss(it.error)))
130+
Background -> {}
131+
ShutDown -> {
132+
closeConnection()
118133
}
119134
}
120-
}.launchIn(scope)
135+
valveCache.onUpdateValveState(socketState)
136+
}.launchIn(innerScope)
121137

122138
combine(
123139
_socketState,
124140
valveCache.emissionOfValveFlow()
125-
) { currentState, request ->
126-
when (currentState) {
127-
ThunderState.IDLE -> Unit
128-
ThunderState.CONNECTING -> {}
141+
) { currentSocketState, request ->
142+
when (currentSocketState) {
129143
ThunderState.CONNECTED -> {
130-
if (isFromError && recoveryCache.hasCache()) {
144+
if (isReSubscription && recoveryCache.hasCache()) {
131145
recoveryCache.get()?.let { requestSendMessage(it) }
132146
recoveryCache.clear()
133-
isFromError = false
147+
isReSubscription = false
134148
} else {
135149
request.forEach(::requestSendMessage)
136150
}
137151
}
138-
ThunderState.DISCONNECTING -> {}
139-
ThunderState.DISCONNECTED -> {}
140-
is ThunderState.ERROR -> {}
152+
153+
else -> Unit
141154
}
142-
}.launchIn(scope)
155+
}.launchIn(innerScope)
156+
}
157+
158+
private suspend fun retryConnection() {
159+
thunderLog("Thunder retry connection work.")
160+
closeConnection()
161+
delay(RETRY_CONNECTION_GAP)
162+
openConnection()
163+
_retryNeedFlag.update { false }
143164
}
144165

145166
/**
@@ -152,20 +173,22 @@ class ThunderStateManager private constructor(
152173
}
153174

154175
private lateinit var connectionJob: Job
155-
private fun openConnection() {
176+
private fun openConnection() = synchronized(this) {
156177
if (socket == null) {
157-
_socketState.updateThunderState(ThunderState.CONNECTING)
158178
socket = webSocketCore.create()
159179
socket?.let { webSocket ->
160180
webSocket.open()
181+
thunderLog("Thunder open connection work.")
161182
if (::connectionJob.isInitialized) connectionJob.cancel()
162-
connectionJob = webSocket.events().onEach { _events.tryEmit(it) }.launchIn(scope)
183+
connectionJob = webSocket.events().onEach { _events.tryEmit(it) }.launchIn(innerScope)
163184
}
164185
}
165186
}
166187

167-
private fun closeConnection() {
188+
private fun closeConnection() = synchronized(this) {
168189
socket?.let {
190+
thunderLog("Thunder close connection work.")
191+
_socketState.update { ThunderState.ERROR() }
169192
it.close(1000, "shutdown")
170193
if (::connectionJob.isInitialized) connectionJob.cancel()
171194
socket = null
@@ -176,10 +199,6 @@ class ThunderStateManager private constructor(
176199
valveCache.requestToValve(key to message)
177200
}
178201

179-
private fun MutableStateFlow<ThunderState>.updateThunderState(state: ThunderState) {
180-
_lastSocketState = getAndUpdate { state }
181-
}
182-
183202
class Factory(
184203
private val connectionListener: AppConnectionListener,
185204
private val networkStatus: NetworkConnectivityService,
@@ -197,4 +216,8 @@ class ThunderStateManager private constructor(
197216
)
198217
}
199218
}
219+
220+
companion object {
221+
private const val RETRY_CONNECTION_GAP = 500L
222+
}
200223
}

thunder/src/main/java/com/jeremy/thunder/state/ActivityState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ sealed interface ActivityState
44

55
object Initialize: ActivityState
66

7-
object GetReady: ActivityState
7+
object Foreground: ActivityState
8+
9+
object Background: ActivityState
810

911
object ShutDown: ActivityState

thunder/src/main/java/com/jeremy/thunder/state/ThunderError.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
package com.jeremy.thunder.state
22

33
sealed interface ThunderError {
4-
data class NetworkLoss(
5-
val message: String?
6-
) : ThunderError
4+
object General : ThunderError
75

8-
data class SocketLoss(
9-
val message: String?
10-
) : ThunderError
6+
object NetworkLoss : ThunderError
117

12-
data class Else(
8+
data class SocketLoss(
139
val message: String?
1410
) : ThunderError
1511
}

thunder/src/main/java/com/jeremy/thunder/state/ThunderState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ sealed interface ThunderState {
3535
* Error State and [ThunderError] must be defined.
3636
* */
3737
data class ERROR(
38-
val error: ThunderError
38+
val error: ThunderError = ThunderError.General
3939
) : ThunderState
4040
}

thunder/src/main/java/com/jeremy/thunder/ws/WebSocket.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ interface WebSocket {
1010

1111
fun events(): Flow<WebSocketEvent>
1212

13-
//websocket 메세지 전송
13+
//Websocket send message
1414
fun send(data: String): Boolean
1515

16-
//websocket 연결 종료 - code & reason
16+
//Websocket Connection close - code & reason
1717
fun close(code: Int, reason: String)
1818

1919
fun cancel()
2020

21-
//websocket 연결 오류
21+
//Websocket Connection failure
2222
fun error(t: String)
2323

2424
interface Factory{

0 commit comments

Comments
 (0)