Skip to content

Commit 9e504b5

Browse files
Update feedback, license
1 parent 6583ba7 commit 9e504b5

File tree

65 files changed

+1265
-1638
lines changed

Some content is hidden

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

65 files changed

+1265
-1638
lines changed

LICENSE

Lines changed: 219 additions & 201 deletions
Large diffs are not rendered by default.

app/src/androidTest/java/io/getstream/android/core/ExampleInstrumentedTest.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

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

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package io.getstream.android.core.sample
1717

1818
import android.os.Build
1919
import android.os.Bundle
20+
import android.util.Log
2021
import androidx.activity.ComponentActivity
2122
import androidx.activity.compose.setContent
2223
import androidx.activity.enableEdgeToEdge
@@ -33,38 +34,35 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
3334
import androidx.lifecycle.lifecycleScope
3435
import androidx.lifecycle.repeatOnLifecycle
3536
import io.getstream.android.core.api.StreamClient
37+
import io.getstream.android.core.sample.client.createStreamClient
3638
import io.getstream.android.core.api.authentication.StreamTokenProvider
37-
import io.getstream.android.core.api.model.config.StreamEndpointConfig
3839
import io.getstream.android.core.api.model.value.StreamApiKey
3940
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
4041
import io.getstream.android.core.api.model.value.StreamToken
4142
import io.getstream.android.core.api.model.value.StreamUserId
42-
import io.getstream.android.core.api.state.StreamClientState
43+
import io.getstream.android.core.api.model.value.StreamWsUrl
4344
import io.getstream.android.core.sample.ui.theme.StreamandroidcoreTheme
4445
import kotlinx.coroutines.launch
4546
import kotlinx.coroutines.runBlocking
4647

4748
class SampleActivity : ComponentActivity() {
49+
50+
val userId = StreamUserId.fromString("petar")
4851
val streamClient =
49-
StreamClient(
50-
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
51-
userId = StreamUserId.fromString("petar"),
52+
createStreamClient(
5253
scope = lifecycleScope,
53-
endpoint =
54-
StreamEndpointConfig(
55-
httpUrl = "https://chat-edge-frankfurt-ce1.stream-io-api.com",
56-
wsUrl = "wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect",
57-
clientInfoHeader =
58-
StreamHttpClientInfoHeader.create(
59-
product = "feeds-android-sdk",
60-
productVersion = "0.0.1",
61-
app = "Sample App",
62-
appVersion = "1.0",
63-
os = "Android ${Build.VERSION.RELEASE}",
64-
apiLevel = Build.VERSION.SDK_INT,
65-
deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}",
66-
),
67-
),
54+
apiKey = StreamApiKey.fromString("pd67s34fzpgw"),
55+
userId = userId,
56+
wsUrl = StreamWsUrl.fromString("wss://chat-edge-frankfurt-ce1.stream-io-api.com/api/v2/connect"),
57+
clientInfoHeader = StreamHttpClientInfoHeader.create(
58+
product = "android-core",
59+
productVersion = "1.0.0",
60+
os = "Android",
61+
apiLevel = Build.VERSION.SDK_INT,
62+
deviceModel = "Pixel 7 Pro",
63+
app = "Stream Android Core Sample",
64+
appVersion = "1.0.0",
65+
),
6866
tokenProvider =
6967
object : StreamTokenProvider {
7068
override suspend fun loadToken(userId: StreamUserId): StreamToken {
@@ -111,7 +109,8 @@ fun GreetingPreview() {
111109
}
112110

113111
@Composable
114-
fun ClientInfo(streamClient: StreamClient<StreamClientState>) {
115-
val state = streamClient.state.connectionState.collectAsStateWithLifecycle()
112+
fun ClientInfo(streamClient: StreamClient) {
113+
val state = streamClient.connectionState.collectAsStateWithLifecycle()
114+
Log.d("SampleActivity", "Client state: ${state.value}")
116115
Text(text = "Client state: ${state.value}")
117116
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package io.getstream.android.core.sample.client
2+
3+
import io.getstream.android.core.api.StreamClient
4+
import io.getstream.android.core.api.authentication.StreamTokenManager
5+
import io.getstream.android.core.api.authentication.StreamTokenProvider
6+
import io.getstream.android.core.api.log.StreamLogger
7+
import io.getstream.android.core.api.log.StreamLoggerProvider
8+
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
9+
import io.getstream.android.core.api.model.value.StreamApiKey
10+
import io.getstream.android.core.api.model.value.StreamHttpClientInfoHeader
11+
import io.getstream.android.core.api.model.value.StreamUserId
12+
import io.getstream.android.core.api.model.value.StreamWsUrl
13+
import io.getstream.android.core.api.processing.StreamBatcher
14+
import io.getstream.android.core.api.processing.StreamRetryProcessor
15+
import io.getstream.android.core.api.processing.StreamSerialProcessingQueue
16+
import io.getstream.android.core.api.processing.StreamSingleFlightProcessor
17+
import io.getstream.android.core.api.serialization.StreamJsonSerialization
18+
import io.getstream.android.core.api.socket.StreamConnectionIdHolder
19+
import io.getstream.android.core.api.socket.StreamWebSocketFactory
20+
import io.getstream.android.core.api.socket.listeners.StreamClientListener
21+
import io.getstream.android.core.api.socket.monitor.StreamHealthMonitor
22+
import io.getstream.android.core.api.subscribe.StreamSubscriptionManager
23+
import kotlinx.coroutines.CoroutineScope
24+
25+
26+
/**
27+
* Creates a [createStreamClient] instance with the given configuration and dependencies.
28+
*
29+
* @param scope The coroutine scope.
30+
* @param apiKey The API key.
31+
* @param userId The user ID.
32+
* @param wsUrl The WebSocket URL.
33+
* @param clientInfoHeader The client info header.
34+
* @param tokenProvider The token provider.
35+
* @param module The client module.
36+
* @return A new [createStreamClient] instance.
37+
*/
38+
fun createStreamClient(
39+
scope: CoroutineScope,
40+
apiKey: StreamApiKey,
41+
userId: StreamUserId,
42+
wsUrl: StreamWsUrl,
43+
clientInfoHeader: StreamHttpClientInfoHeader,
44+
tokenProvider: StreamTokenProvider,
45+
module: StreamClientModule = StreamClientModule.defaults(scope, userId, tokenProvider),
46+
): StreamClient = StreamClient(
47+
scope = scope,
48+
apiKey = apiKey,
49+
userId = userId,
50+
wsUrl = wsUrl,
51+
products = listOf("feeds"),
52+
clientInfoHeader = clientInfoHeader,
53+
tokenProvider = tokenProvider,
54+
logProvider = module.logProvider,
55+
clientSubscriptionManager = module.clientSubscriptionManager,
56+
tokenManager = module.tokenManager,
57+
singleFlight = module.singleFlight,
58+
serialQueue = module.serialQueue,
59+
retryProcessor = module.retryProcessor,
60+
connectionIdHolder = module.connectionIdHolder,
61+
socketFactory = module.socketFactory,
62+
healthMonitor = module.healthMonitor,
63+
batcher = module.batcher,
64+
)
65+
66+
/**
67+
* Holds configuration and dependencies for the Stream client, including logging, coroutine scope,
68+
* subscription managers, token management, processing queues, retry logic, connection handling,
69+
* socket factory, health monitoring, event debouncing, and JSON serialization.
70+
*
71+
* This module is intended to be used as a central place for client-wide resources and processors.
72+
*
73+
* @property logProvider Provides logging functionality for the client.
74+
* @property scope Coroutine scope used for async operations.
75+
* @property clientSubscriptionManager Manages subscriptions for client listeners.
76+
* @property tokenManager Handles authentication tokens.
77+
* @property singleFlight Ensures single execution of concurrent requests.
78+
* @property serialQueue Serializes processing of tasks.
79+
* @property retryProcessor Handles retry logic for failed operations.
80+
* @property connectionIdHolder Stores the current connection ID.
81+
* @property socketFactory Creates WebSocket connections.
82+
* @property healthMonitor Monitors connection health.
83+
* @property batcher Batches socket events for batch processing.
84+
*/
85+
@ConsistentCopyVisibility
86+
data class StreamClientModule private constructor(
87+
val logProvider: StreamLoggerProvider = StreamLoggerProvider.Companion.defaultAndroidLogger(
88+
minLevel = StreamLogger.LogLevel.Verbose,
89+
honorAndroidIsLoggable = false,
90+
),
91+
val scope: CoroutineScope,
92+
val clientSubscriptionManager: StreamSubscriptionManager<StreamClientListener> = StreamSubscriptionManager(
93+
logger = logProvider.taggedLogger("SCClientSubscriptions"),
94+
maxStrongSubscriptions = 250,
95+
maxWeakSubscriptions = 250,
96+
),
97+
val tokenManager: StreamTokenManager,
98+
val singleFlight: StreamSingleFlightProcessor = StreamSingleFlightProcessor(
99+
scope = scope,
100+
),
101+
val serialQueue: StreamSerialProcessingQueue = StreamSerialProcessingQueue(
102+
logger = logProvider.taggedLogger("SCSerialProcessing"),
103+
scope = scope,
104+
),
105+
val retryProcessor: StreamRetryProcessor = StreamRetryProcessor(
106+
logger = logProvider.taggedLogger("SCRetryProcessor")
107+
),
108+
val connectionIdHolder: StreamConnectionIdHolder = StreamConnectionIdHolder(),
109+
val socketFactory: StreamWebSocketFactory = StreamWebSocketFactory(
110+
logger = logProvider.taggedLogger("SCWebSocketFactory")
111+
),
112+
val healthMonitor: StreamHealthMonitor = StreamHealthMonitor(
113+
logger = logProvider.taggedLogger("SCHealthMonitor"),
114+
scope = scope,
115+
),
116+
val batcher: StreamBatcher<String> =
117+
StreamBatcher(
118+
scope = scope,
119+
batchSize = 10,
120+
initialDelayMs = 100L,
121+
maxDelayMs = 1_000L,
122+
)
123+
) {
124+
125+
companion object {
126+
/**
127+
* Creates a default [StreamClientModule] instance with recommended settings and dependencies.
128+
*
129+
* @param scope Coroutine scope for async operations.
130+
* @param userId The user ID for authentication.
131+
* @param tokenProvider Provider for authentication tokens.
132+
* @return A configured [StreamClientModule] instance.
133+
*/
134+
fun defaults(
135+
scope: CoroutineScope,
136+
userId: StreamUserId,
137+
tokenProvider: StreamTokenProvider
138+
): StreamClientModule {
139+
val singleFlight = StreamSingleFlightProcessor(scope)
140+
return StreamClientModule(
141+
scope = scope,
142+
singleFlight = singleFlight,
143+
tokenManager = StreamTokenManager(userId, tokenProvider, singleFlight),
144+
)
145+
}
146+
}
147+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[versions]
22
agp = "8.11.1"
33
core = "1.7.0"
4-
detektFormatting = "1.23.8"
54
kotlin = "2.2.0"
65
coreKtx = "1.17.0"
76
junit = "4.13.2"
@@ -30,7 +29,7 @@ klint = "13.0.0"
3029
[libraries]
3130
androidx-core = { module = "androidx.test:core", version.ref = "core" }
3231
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
33-
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detektFormatting" }
32+
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
3433
junit = { group = "junit", name = "junit", version.ref = "junit" }
3534
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
3635
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }

lint.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
<issue id="ExposeAsStateFlow" severity="error"/>
1111
<issue id="StreamCoreApiMissing" severity="error">
1212
<option name="packages" value="io.getstream.android.core.api.*,io.getstream.android.core.api" />
13+
<option name="exclude_packages" value="io.getstream.android.core.api.model.value.*" />
1314
</issue>
1415
</lint>

stream-android-core-lint/src/main/java/io/getstream/android/core/lint/detectors/StreamCoreApiDetector.kt

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ class StreamCoreApiDetector : Detector(), Detector.UastScanner {
127127
private fun JavaContext.packageMatchesConfig(pkg: String): Boolean {
128128
val patterns = configuredPackageGlobs()
129129
if (patterns.isEmpty()) return false
130+
val included = patterns.any { pkgMatchesGlob(pkg, it) }
131+
val excluded = packageMatchesExcludeConfig(pkg)
132+
return included && !excluded
133+
}
134+
135+
private fun JavaContext.packageMatchesExcludeConfig(pkg: String): Boolean {
136+
val raw = configuration.getOption(ISSUE, OPTION_PACKAGES_EXCLUDE.name, "")?.trim().orEmpty()
137+
if (raw.isEmpty()) return false
138+
val patterns = raw.split(',').map { it.trim() }.filter { it.isNotEmpty() }
139+
if (patterns.isEmpty()) return false
130140
return patterns.any { pkgMatchesGlob(pkg, it) }
131141
}
132142

@@ -181,9 +191,9 @@ class StreamCoreApiDetector : Detector(), Detector.UastScanner {
181191
// top-level: public if no visibility modifier or explicit `public`
182192
val isPublic =
183193
hasModifier(KtTokens.PUBLIC_KEYWORD) ||
184-
(!hasModifier(KtTokens.INTERNAL_KEYWORD) &&
185-
!hasModifier(KtTokens.PRIVATE_KEYWORD) &&
186-
!hasModifier(KtTokens.PROTECTED_KEYWORD))
194+
(!hasModifier(KtTokens.INTERNAL_KEYWORD) &&
195+
!hasModifier(KtTokens.PRIVATE_KEYWORD) &&
196+
!hasModifier(KtTokens.PROTECTED_KEYWORD))
187197
return isPublic
188198
}
189199

@@ -195,6 +205,7 @@ class StreamCoreApiDetector : Detector(), Detector.UastScanner {
195205
kt.isEnum() -> "enum class"
196206
else -> "class"
197207
}
208+
198209
is KtObjectDeclaration -> "object"
199210
else -> "class"
200211
}
@@ -222,22 +233,38 @@ class StreamCoreApiDetector : Detector(), Detector.UastScanner {
222233
.trimIndent(),
223234
)
224235

236+
private val OPTION_PACKAGES_EXCLUDE =
237+
StringOption(
238+
name = "exclude_packages",
239+
description =
240+
"Comma-separated package **glob** patterns where top-level public APIs are excluded from the check.",
241+
explanation =
242+
"""
243+
Supports wildcards: '*' (any sequence) and '?' (single char).
244+
Examples:
245+
- 'io.getstream.android.core.api'
246+
- 'io.getstream.android.core.*.api'
247+
- 'io.getstream.android.core.api*'
248+
"""
249+
.trimIndent(),
250+
)
251+
225252
@JvmField
226253
val ISSUE: Issue =
227254
Issue.create(
228-
id = "StreamCoreApiMissing",
229-
briefDescription = "Missing @StreamCoreApi on public API",
230-
explanation =
231-
"""
255+
id = "StreamCoreApiMissing",
256+
briefDescription = "Missing @StreamCoreApi on public API",
257+
explanation =
258+
"""
232259
Top-level public declarations in configured packages must be annotated \
233260
with @StreamCoreApi to indicate they are part of the Stream Core API surface.
234261
"""
235-
.trimIndent(),
236-
category = Category.CORRECTNESS,
237-
priority = 7,
238-
severity = Severity.ERROR,
239-
implementation = IMPLEMENTATION,
240-
)
241-
.setOptions(listOf(OPTION_PACKAGES))
262+
.trimIndent(),
263+
category = Category.CORRECTNESS,
264+
priority = 7,
265+
severity = Severity.ERROR,
266+
implementation = IMPLEMENTATION,
267+
)
268+
.setOptions(listOf(OPTION_PACKAGES, OPTION_PACKAGES_EXCLUDE))
242269
}
243270
}

0 commit comments

Comments
 (0)