Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a8c599e
Add new network monitor and network sample UI in core
aleksandar-apostolov Oct 16, 2025
6cee404
Spotless
aleksandar-apostolov Oct 16, 2025
078cb67
Update test
aleksandar-apostolov Oct 16, 2025
edac873
Missing permission check
aleksandar-apostolov Oct 16, 2025
b25c1a3
Missing permission check
aleksandar-apostolov Oct 16, 2025
4b54265
Add tests for the stream network monitor utils and processing
aleksandar-apostolov Oct 16, 2025
bf78bb9
Move network monitor initialization after connection is established, …
aleksandar-apostolov Oct 16, 2025
c010b50
Stop network monitor on Disconnect
aleksandar-apostolov Oct 16, 2025
e945bfa
Refactor the callback into separate delegate and add tests
aleksandar-apostolov Oct 22, 2025
fce3cfa
Fix lint errors and run spotless
aleksandar-apostolov Oct 22, 2025
3ff9bb1
Refactor snapshot builder and add tests
aleksandar-apostolov Oct 22, 2025
dd99226
Refactor signal processing and add tests
aleksandar-apostolov Oct 22, 2025
c9f8003
Add more tests
aleksandar-apostolov Oct 22, 2025
d1188f6
Spotless
aleksandar-apostolov Oct 22, 2025
b91fcdc
Fix algebra
aleksandar-apostolov Oct 22, 2025
77f24ef
Update wrapper is now more generic
aleksandar-apostolov Oct 22, 2025
693328e
Spotless
aleksandar-apostolov Oct 22, 2025
e0e234c
Change state update mechanism
aleksandar-apostolov Oct 22, 2025
c2c3cef
Spotless
aleksandar-apostolov Oct 22, 2025
3b30b03
Update network info UI in sample
aleksandar-apostolov Oct 22, 2025
bace614
Update client test
aleksandar-apostolov Oct 22, 2025
f484eb4
Spotless
aleksandar-apostolov Oct 22, 2025
76f90b8
Update stream-android-core/src/main/java/io/getstream/android/core/in…
aleksandar-apostolov Oct 23, 2025
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
102 changes: 66 additions & 36 deletions app/src/main/java/io/getstream/android/core/sample/SampleActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,83 +21,101 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import io.getstream.android.core.api.StreamClient
import io.getstream.android.core.api.authentication.StreamTokenProvider
import io.getstream.android.core.api.model.connection.network.StreamNetworkState
import io.getstream.android.core.api.model.value.StreamApiKey
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
import io.getstream.android.core.api.model.value.StreamToken
import io.getstream.android.core.api.model.value.StreamUserId
import io.getstream.android.core.api.model.value.StreamWsUrl
import io.getstream.android.core.sample.client.createStreamClient
import io.getstream.android.core.sample.ui.ConnectionStateCard
import io.getstream.android.core.sample.ui.NetworkInfoCard
import io.getstream.android.core.sample.ui.theme.StreamandroidcoreTheme
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

class SampleActivity : ComponentActivity() {

val userId = StreamUserId.fromString("petar")
val streamClient =
createStreamClient(
scope = lifecycleScope,
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
userId = userId,
wsUrl =
StreamWsUrl.fromString(
"wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect"
),
clientInfoHeader =
StreamHttpClientInfoHeader.create(
product = "android-core",
productVersion = "1.0.0",
os = "Android",
apiLevel = Build.VERSION.SDK_INT,
deviceModel = "Pixel 7 Pro",
app = "Stream Android Core Sample",
appVersion = "1.0.0",
),
tokenProvider =
object : StreamTokenProvider {
override suspend fun loadToken(userId: StreamUserId): StreamToken {
return StreamToken.fromString(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicGV0YXIifQ.mZFi4iSblaIoyo9JDdcxIkGkwI-tuApeSBawxpz42rs"
)
}
},
)
var streamClient: StreamClient? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val streamClient2 =
createStreamClient(
context = this.applicationContext,
scope = lifecycleScope,
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
userId = userId,
wsUrl =
StreamWsUrl.fromString(
"wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect"
),
clientInfoHeader =
StreamHttpClientInfoHeader.create(
product = "android-core",
productVersion = "1.0.0",
os = "Android",
apiLevel = Build.VERSION.SDK_INT,
deviceModel = "Pixel 7 Pro",
app = "Stream Android Core Sample",
appVersion = "1.0.0",
),
tokenProvider =
object : StreamTokenProvider {
override suspend fun loadToken(userId: StreamUserId): StreamToken {
return StreamToken.fromString(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicGV0YXIifQ.mZFi4iSblaIoyo9JDdcxIkGkwI-tuApeSBawxpz42rs"
)
}
},
)
streamClient = streamClient2
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { streamClient.connect() }
repeatOnLifecycle(Lifecycle.State.RESUMED) { streamClient?.connect() }
}
enableEdgeToEdge()
setContent {
StreamandroidcoreTheme {
val scrollState = rememberScrollState()
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column {
Greeting(name = "Android", modifier = Modifier.padding(innerPadding))
ClientInfo(streamClient = streamClient)
Column(
modifier =
Modifier.fillMaxSize()
.padding(innerPadding)
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Greeting(name = "Android")
ClientInfo(streamClient = streamClient2)
}
}
}
}
}

override fun onPause() {
runBlocking { streamClient.disconnect() }
super.onPause()
override fun onStop() {
runBlocking { streamClient?.disconnect() }
super.onStop()
}
}

Expand All @@ -115,6 +133,18 @@ fun GreetingPreview() {
@Composable
fun ClientInfo(streamClient: StreamClient) {
val state = streamClient.connectionState.collectAsStateWithLifecycle()
val networkSnapshot = streamClient.networkState.collectAsStateWithLifecycle()
Log.d("SampleActivity", "Client state: ${state.value}")
Text(text = "Client state: ${state.value}")
val networkState = networkSnapshot.value
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
ConnectionStateCard(state = state.value)
when (networkState) {
is StreamNetworkState.Available -> {
NetworkInfoCard(snapshot = networkState.snapshot)
}
else -> {
NetworkInfoCard(snapshot = null)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@
*/
package io.getstream.android.core.sample.client

import android.content.Context
import io.getstream.android.core.api.StreamClient
import io.getstream.android.core.api.authentication.StreamTokenManager
import io.getstream.android.core.api.authentication.StreamTokenProvider
import io.getstream.android.core.api.components.StreamAndroidComponentsProvider
import io.getstream.android.core.api.log.StreamLogger
import io.getstream.android.core.api.log.StreamLoggerProvider
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
import io.getstream.android.core.api.model.value.StreamApiKey
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
import io.getstream.android.core.api.model.value.StreamUserId
import io.getstream.android.core.api.model.value.StreamWsUrl
import io.getstream.android.core.api.observers.network.StreamNetworkMonitor
import io.getstream.android.core.api.processing.StreamBatcher
import io.getstream.android.core.api.processing.StreamRetryProcessor
import io.getstream.android.core.api.processing.StreamSerialProcessingQueue
Expand All @@ -49,6 +52,7 @@ import kotlinx.coroutines.CoroutineScope
* @return A new [createStreamClient] instance.
*/
fun createStreamClient(
context: Context,
scope: CoroutineScope,
apiKey: StreamApiKey,
userId: StreamUserId,
Expand Down Expand Up @@ -88,6 +92,23 @@ fun createStreamClient(
maxDelayMs = 1_000L,
)

val androidComponentsProvider = StreamAndroidComponentsProvider(context)
val connectivityManager = androidComponentsProvider.connectivityManager().getOrThrow()
val wifiManager = androidComponentsProvider.wifiManager().getOrThrow()
val telephonyManager = androidComponentsProvider.telephonyManager().getOrThrow()
val networkMonitor =
StreamNetworkMonitor(
logger = logProvider.taggedLogger("SCNetworkMonitor"),
scope = scope,
connectivityManager = connectivityManager,
wifiManager = wifiManager,
telephonyManager = telephonyManager,
subscriptionManager =
StreamSubscriptionManager(
logger = logProvider.taggedLogger("SCNetworkMonitorSubscriptions")
),
)

return StreamClient(
scope = scope,
apiKey = apiKey,
Expand All @@ -105,6 +126,7 @@ fun createStreamClient(
connectionIdHolder = connectionIdHolder,
socketFactory = socketFactory,
healthMonitor = healthMonitor,
networkMonitor = networkMonitor,
serializationConfig =
StreamClientSerializationConfig.default(
object : StreamEventSerialization<Unit> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-core-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.android.core.sample.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.getstream.android.core.api.model.connection.StreamConnectedUser
import io.getstream.android.core.api.model.connection.StreamConnectionState
import io.getstream.android.core.sample.ui.theme.StreamandroidcoreTheme
import java.util.Date

@Composable
public fun ConnectionStateCard(state: StreamConnectionState) {
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "Connection",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)

val statusLabel = connectionStatusLabel(state)
val statusState = connectionStatusState(state)
val statusAlert = statusState == false

NetworkFactRow(
label = "Status",
value = statusLabel,
state = statusState,
alert = statusAlert,
)

when (state) {
is StreamConnectionState.Connected -> {
Divider()
NetworkFactRow(
label = "User",
value = state.connectedUser.displayName(),
state = null,
)
NetworkFactRow(
label = "Connection ID",
value = state.connectionId,
state = null,
)
}

is StreamConnectionState.Connecting.Opening -> {
Divider()
NetworkFactRow(label = "Stage", value = "Opening socket", state = null)
NetworkFactRow(label = "User", value = state.userId, state = null)
}

is StreamConnectionState.Connecting.Authenticating -> {
Divider()
NetworkFactRow(label = "Stage", value = "Authenticating", state = null)
NetworkFactRow(label = "User", value = state.userId, state = null)
}

is StreamConnectionState.Disconnected -> {
Divider()
NetworkFactRow(
label = "Cause",
value = state.cause?.localizedMessage ?: "No details",
state = false,
alert = state.cause != null,
)
}

StreamConnectionState.Idle -> {
Divider()
NetworkFactRow(label = "Details", value = "Client idle", state = null)
}
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun ConnectionStateCardPreview() {
StreamandroidcoreTheme {
ConnectionStateCard(
StreamConnectionState.Connected(
connectedUser = sampleConnectedUser(),
connectionId = "conn-1234",
)
)
}
}

private fun sampleConnectedUser(): StreamConnectedUser =
StreamConnectedUser(
createdAt = Date(),
id = "petar",
language = "en",
role = "user",
updatedAt = Date(),
teams = emptyList(),
name = "Petar",
)
Loading