Skip to content

Commit d142005

Browse files
Introduce a network monitor into core (#22)
Co-authored-by: Gianmarco <[email protected]>
1 parent 916aec5 commit d142005

File tree

42 files changed

+3779
-50
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3779
-50
lines changed

app/src/main/java/io/getstream/android/core/sample/SampleActivity.kt

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,83 +21,101 @@ import android.util.Log
2121
import androidx.activity.ComponentActivity
2222
import androidx.activity.compose.setContent
2323
import androidx.activity.enableEdgeToEdge
24+
import androidx.compose.foundation.layout.Arrangement
2425
import androidx.compose.foundation.layout.Column
2526
import androidx.compose.foundation.layout.fillMaxSize
2627
import androidx.compose.foundation.layout.padding
28+
import androidx.compose.foundation.rememberScrollState
29+
import androidx.compose.foundation.verticalScroll
2730
import androidx.compose.material3.Scaffold
2831
import androidx.compose.material3.Text
2932
import androidx.compose.runtime.Composable
3033
import androidx.compose.ui.Modifier
3134
import androidx.compose.ui.tooling.preview.Preview
35+
import androidx.compose.ui.unit.dp
3236
import androidx.lifecycle.Lifecycle
3337
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3438
import androidx.lifecycle.lifecycleScope
3539
import androidx.lifecycle.repeatOnLifecycle
3640
import io.getstream.android.core.api.StreamClient
3741
import io.getstream.android.core.api.authentication.StreamTokenProvider
42+
import io.getstream.android.core.api.model.connection.network.StreamNetworkState
3843
import io.getstream.android.core.api.model.value.StreamApiKey
3944
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
4045
import io.getstream.android.core.api.model.value.StreamToken
4146
import io.getstream.android.core.api.model.value.StreamUserId
4247
import io.getstream.android.core.api.model.value.StreamWsUrl
4348
import io.getstream.android.core.sample.client.createStreamClient
49+
import io.getstream.android.core.sample.ui.ConnectionStateCard
50+
import io.getstream.android.core.sample.ui.NetworkInfoCard
4451
import io.getstream.android.core.sample.ui.theme.StreamandroidcoreTheme
4552
import kotlinx.coroutines.launch
4653
import kotlinx.coroutines.runBlocking
4754

4855
class SampleActivity : ComponentActivity() {
4956

5057
val userId = StreamUserId.fromString("petar")
51-
val streamClient =
52-
createStreamClient(
53-
scope = lifecycleScope,
54-
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
55-
userId = userId,
56-
wsUrl =
57-
StreamWsUrl.fromString(
58-
"wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect"
59-
),
60-
clientInfoHeader =
61-
StreamHttpClientInfoHeader.create(
62-
product = "android-core",
63-
productVersion = "1.0.0",
64-
os = "Android",
65-
apiLevel = Build.VERSION.SDK_INT,
66-
deviceModel = "Pixel 7 Pro",
67-
app = "Stream Android Core Sample",
68-
appVersion = "1.0.0",
69-
),
70-
tokenProvider =
71-
object : StreamTokenProvider {
72-
override suspend fun loadToken(userId: StreamUserId): StreamToken {
73-
return StreamToken.fromString(
74-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicGV0YXIifQ.mZFi4iSblaIoyo9JDdcxIkGkwI-tuApeSBawxpz42rs"
75-
)
76-
}
77-
},
78-
)
58+
var streamClient: StreamClient? = null
7959

8060
override fun onCreate(savedInstanceState: Bundle?) {
8161
super.onCreate(savedInstanceState)
62+
val streamClient2 =
63+
createStreamClient(
64+
context = this.applicationContext,
65+
scope = lifecycleScope,
66+
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
67+
userId = userId,
68+
wsUrl =
69+
StreamWsUrl.fromString(
70+
"wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect"
71+
),
72+
clientInfoHeader =
73+
StreamHttpClientInfoHeader.create(
74+
product = "android-core",
75+
productVersion = "1.0.0",
76+
os = "Android",
77+
apiLevel = Build.VERSION.SDK_INT,
78+
deviceModel = "Pixel 7 Pro",
79+
app = "Stream Android Core Sample",
80+
appVersion = "1.0.0",
81+
),
82+
tokenProvider =
83+
object : StreamTokenProvider {
84+
override suspend fun loadToken(userId: StreamUserId): StreamToken {
85+
return StreamToken.fromString(
86+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicGV0YXIifQ.mZFi4iSblaIoyo9JDdcxIkGkwI-tuApeSBawxpz42rs"
87+
)
88+
}
89+
},
90+
)
91+
streamClient = streamClient2
8292
lifecycleScope.launch {
83-
repeatOnLifecycle(Lifecycle.State.RESUMED) { streamClient.connect() }
93+
repeatOnLifecycle(Lifecycle.State.RESUMED) { streamClient?.connect() }
8494
}
8595
enableEdgeToEdge()
8696
setContent {
8797
StreamandroidcoreTheme {
98+
val scrollState = rememberScrollState()
8899
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
89-
Column {
90-
Greeting(name = "Android", modifier = Modifier.padding(innerPadding))
91-
ClientInfo(streamClient = streamClient)
100+
Column(
101+
modifier =
102+
Modifier.fillMaxSize()
103+
.padding(innerPadding)
104+
.verticalScroll(scrollState)
105+
.padding(16.dp),
106+
verticalArrangement = Arrangement.spacedBy(16.dp),
107+
) {
108+
Greeting(name = "Android")
109+
ClientInfo(streamClient = streamClient2)
92110
}
93111
}
94112
}
95113
}
96114
}
97115

98-
override fun onPause() {
99-
runBlocking { streamClient.disconnect() }
100-
super.onPause()
116+
override fun onStop() {
117+
runBlocking { streamClient?.disconnect() }
118+
super.onStop()
101119
}
102120
}
103121

@@ -115,6 +133,18 @@ fun GreetingPreview() {
115133
@Composable
116134
fun ClientInfo(streamClient: StreamClient) {
117135
val state = streamClient.connectionState.collectAsStateWithLifecycle()
136+
val networkSnapshot = streamClient.networkState.collectAsStateWithLifecycle()
118137
Log.d("SampleActivity", "Client state: ${state.value}")
119-
Text(text = "Client state: ${state.value}")
138+
val networkState = networkSnapshot.value
139+
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
140+
ConnectionStateCard(state = state.value)
141+
when (networkState) {
142+
is StreamNetworkState.Available -> {
143+
NetworkInfoCard(snapshot = networkState.snapshot)
144+
}
145+
else -> {
146+
NetworkInfoCard(snapshot = null)
147+
}
148+
}
149+
}
120150
}

app/src/main/java/io/getstream/android/core/sample/client/StreamClient.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@
1515
*/
1616
package io.getstream.android.core.sample.client
1717

18+
import android.content.Context
1819
import io.getstream.android.core.api.StreamClient
1920
import io.getstream.android.core.api.authentication.StreamTokenManager
2021
import io.getstream.android.core.api.authentication.StreamTokenProvider
22+
import io.getstream.android.core.api.components.StreamAndroidComponentsProvider
2123
import io.getstream.android.core.api.log.StreamLogger
2224
import io.getstream.android.core.api.log.StreamLoggerProvider
2325
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
2426
import io.getstream.android.core.api.model.value.StreamApiKey
2527
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
2628
import io.getstream.android.core.api.model.value.StreamUserId
2729
import io.getstream.android.core.api.model.value.StreamWsUrl
30+
import io.getstream.android.core.api.observers.network.StreamNetworkMonitor
2831
import io.getstream.android.core.api.processing.StreamBatcher
2932
import io.getstream.android.core.api.processing.StreamRetryProcessor
3033
import io.getstream.android.core.api.processing.StreamSerialProcessingQueue
@@ -49,6 +52,7 @@ import kotlinx.coroutines.CoroutineScope
4952
* @return A new [createStreamClient] instance.
5053
*/
5154
fun createStreamClient(
55+
context: Context,
5256
scope: CoroutineScope,
5357
apiKey: StreamApiKey,
5458
userId: StreamUserId,
@@ -88,6 +92,23 @@ fun createStreamClient(
8892
maxDelayMs = 1_000L,
8993
)
9094

95+
val androidComponentsProvider = StreamAndroidComponentsProvider(context)
96+
val connectivityManager = androidComponentsProvider.connectivityManager().getOrThrow()
97+
val wifiManager = androidComponentsProvider.wifiManager().getOrThrow()
98+
val telephonyManager = androidComponentsProvider.telephonyManager().getOrThrow()
99+
val networkMonitor =
100+
StreamNetworkMonitor(
101+
logger = logProvider.taggedLogger("SCNetworkMonitor"),
102+
scope = scope,
103+
connectivityManager = connectivityManager,
104+
wifiManager = wifiManager,
105+
telephonyManager = telephonyManager,
106+
subscriptionManager =
107+
StreamSubscriptionManager(
108+
logger = logProvider.taggedLogger("SCNetworkMonitorSubscriptions")
109+
),
110+
)
111+
91112
return StreamClient(
92113
scope = scope,
93114
apiKey = apiKey,
@@ -105,6 +126,7 @@ fun createStreamClient(
105126
connectionIdHolder = connectionIdHolder,
106127
socketFactory = socketFactory,
107128
healthMonitor = healthMonitor,
129+
networkMonitor = networkMonitor,
108130
serializationConfig =
109131
StreamClientSerializationConfig.default(
110132
object : StreamEventSerialization<Unit> {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-core-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android.core.sample.ui
17+
18+
import androidx.compose.foundation.layout.Arrangement
19+
import androidx.compose.foundation.layout.Column
20+
import androidx.compose.foundation.layout.fillMaxWidth
21+
import androidx.compose.foundation.layout.padding
22+
import androidx.compose.foundation.shape.RoundedCornerShape
23+
import androidx.compose.material3.CardDefaults
24+
import androidx.compose.material3.Divider
25+
import androidx.compose.material3.MaterialTheme
26+
import androidx.compose.material3.OutlinedCard
27+
import androidx.compose.material3.Text
28+
import androidx.compose.runtime.Composable
29+
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.text.font.FontWeight
31+
import androidx.compose.ui.tooling.preview.Preview
32+
import androidx.compose.ui.unit.dp
33+
import io.getstream.android.core.api.model.connection.StreamConnectedUser
34+
import io.getstream.android.core.api.model.connection.StreamConnectionState
35+
import io.getstream.android.core.sample.ui.theme.StreamandroidcoreTheme
36+
import java.util.Date
37+
38+
@Composable
39+
public fun ConnectionStateCard(state: StreamConnectionState) {
40+
OutlinedCard(
41+
modifier = Modifier.fillMaxWidth(),
42+
shape = RoundedCornerShape(20.dp),
43+
colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
44+
) {
45+
Column(
46+
modifier = Modifier.padding(20.dp),
47+
verticalArrangement = Arrangement.spacedBy(16.dp),
48+
) {
49+
Text(
50+
text = "Connection",
51+
style = MaterialTheme.typography.titleMedium,
52+
fontWeight = FontWeight.SemiBold,
53+
)
54+
55+
val statusLabel = connectionStatusLabel(state)
56+
val statusState = connectionStatusState(state)
57+
val statusAlert = statusState == false
58+
59+
NetworkFactRow(
60+
label = "Status",
61+
value = statusLabel,
62+
state = statusState,
63+
alert = statusAlert,
64+
)
65+
66+
when (state) {
67+
is StreamConnectionState.Connected -> {
68+
Divider()
69+
NetworkFactRow(
70+
label = "User",
71+
value = state.connectedUser.displayName(),
72+
state = null,
73+
)
74+
NetworkFactRow(
75+
label = "Connection ID",
76+
value = state.connectionId,
77+
state = null,
78+
)
79+
}
80+
81+
is StreamConnectionState.Connecting.Opening -> {
82+
Divider()
83+
NetworkFactRow(label = "Stage", value = "Opening socket", state = null)
84+
NetworkFactRow(label = "User", value = state.userId, state = null)
85+
}
86+
87+
is StreamConnectionState.Connecting.Authenticating -> {
88+
Divider()
89+
NetworkFactRow(label = "Stage", value = "Authenticating", state = null)
90+
NetworkFactRow(label = "User", value = state.userId, state = null)
91+
}
92+
93+
is StreamConnectionState.Disconnected -> {
94+
Divider()
95+
NetworkFactRow(
96+
label = "Cause",
97+
value = state.cause?.localizedMessage ?: "No details",
98+
state = false,
99+
alert = state.cause != null,
100+
)
101+
}
102+
103+
StreamConnectionState.Idle -> {
104+
Divider()
105+
NetworkFactRow(label = "Details", value = "Client idle", state = null)
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
@Preview(showBackground = true)
113+
@Composable
114+
private fun ConnectionStateCardPreview() {
115+
StreamandroidcoreTheme {
116+
ConnectionStateCard(
117+
StreamConnectionState.Connected(
118+
connectedUser = sampleConnectedUser(),
119+
connectionId = "conn-1234",
120+
)
121+
)
122+
}
123+
}
124+
125+
private fun sampleConnectedUser(): StreamConnectedUser =
126+
StreamConnectedUser(
127+
createdAt = Date(),
128+
id = "petar",
129+
language = "en",
130+
role = "user",
131+
updatedAt = Date(),
132+
teams = emptyList(),
133+
name = "Petar",
134+
)

0 commit comments

Comments
 (0)