Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
10661de
chore: add endpoint for reportClientCallEvent
rahul-lohra May 15, 2026
cf5c34c
chore: revert deleted files and unnecessary renames
rahul-lohra May 15, 2026
9aa4811
chore: update api file
rahul-lohra May 15, 2026
47c2b79
internal: create call event reporter
rahul-lohra May 18, 2026
30d130d
internal: create call leave reason
rahul-lohra May 18, 2026
5de5a92
chore: create typedalias EventSessionId
rahul-lohra May 19, 2026
713fc0a
chore: update openapi models
rahul-lohra May 19, 2026
b56d3e0
chore: Update CallEventReporter
rahul-lohra May 19, 2026
086d285
chore: Update ClientEventReporter
rahul-lohra May 20, 2026
7f5fe03
chore: update with new client error reporting code
rahul-lohra May 20, 2026
c90823b
chore: add missing values in client event reporting
rahul-lohra May 21, 2026
2e9f641
Merge branch 'develop' into feature/rahullohra/call-event-reporting
rahul-lohra May 21, 2026
ec68870
chore: add todo
rahul-lohra May 21, 2026
801da73
chore: refactor code
rahul-lohra May 21, 2026
5cd656d
chore: add telemetry model
rahul-lohra May 25, 2026
9125e8e
chore: add fault injector
rahul-lohra May 25, 2026
43c875e
chore: refactor code with fault injectors
rahul-lohra May 26, 2026
7b3db27
chore: refactor code with fault injectors
rahul-lohra May 26, 2026
d4dbc92
chore: rename from fault injector to failure injector
rahul-lohra May 26, 2026
36af925
chore: update the hook for join-completed
rahul-lohra May 26, 2026
ef46a33
temp: send `auto` in joinRequest if we fail to fetch location
rahul-lohra May 26, 2026
ae2e77e
use analytics hook class to write implementation of sending events
rahul-lohra May 26, 2026
144ff57
send call leave reason from ui layer
rahul-lohra May 27, 2026
b00efd0
spotless
rahul-lohra May 28, 2026
1198356
api dump
rahul-lohra May 28, 2026
3254302
fix: fix stage attempts
rahul-lohra May 28, 2026
22f85c8
chore: update open api models
rahul-lohra May 28, 2026
5dfce23
chore: spotless
rahul-lohra May 28, 2026
3a5a07e
chore: update failure injector logic
rahul-lohra May 28, 2026
01776cf
chore: add new call leave reason
rahul-lohra May 29, 2026
f08af11
chore: add peer connection hook
rahul-lohra May 29, 2026
690b9ab
chore: spotless
rahul-lohra May 29, 2026
e787c69
chore: add pc analytics observer
rahul-lohra May 29, 2026
a5dd29a
fix: fix 1 self cancelling coroutine code
rahul-lohra Jun 1, 2026
5c125e6
fix: add logic to send pc analytics only once
rahul-lohra Jun 1, 2026
83ee729
feat: add media permission hook
rahul-lohra Jun 1, 2026
47f6d42
fix: correct start subscriber state observer
rahul-lohra Jun 1, 2026
f1a9f7e
temp: add logs
rahul-lohra Jun 1, 2026
3cd0811
temp: spotless
rahul-lohra Jun 1, 2026
ad42270
feat: add coordinator ws analytics
rahul-lohra Jun 2, 2026
3cdbce1
feat: add coordinator ws analytics
rahul-lohra Jun 2, 2026
5d57cc5
feat: add audio analytics, video analytics
rahul-lohra Jun 3, 2026
bd2a2ba
feat: update video analytics
rahul-lohra Jun 3, 2026
4c428a9
chore: replace event_session_id with stage_id
rahul-lohra Jun 3, 2026
e0169e9
chore: send coordinator id
rahul-lohra Jun 3, 2026
291671f
chore: replace event session id with stage id
rahul-lohra Jun 4, 2026
699e6c5
fix: fix sending events for firstVideoFrameRendered
rahul-lohra Jun 4, 2026
732b71b
fix: fix sending events for firstVideoFrameRendered
rahul-lohra Jun 4, 2026
af82b29
fix: fix sending events for firstVideoFrameRendered for view and scre…
rahul-lohra Jun 4, 2026
2700890
fix: fix the logic of resending of events after re-join
rahul-lohra Jun 4, 2026
b9a57fb
chore: change name
rahul-lohra Jun 4, 2026
5fb286e
chore: spotless & apiDump
rahul-lohra Jun 4, 2026
e3dc60d
Merge branch 'refs/heads/develop' into feature/rahullohra/call-event-…
rahul-lohra Jun 4, 2026
b5a3a01
fix: Fix first audio frame
rahul-lohra Jun 4, 2026
1c89a96
fix: spotless
rahul-lohra Jun 4, 2026
1f6d324
chore: move files
rahul-lohra Jun 4, 2026
4b9a26b
chore: refactor client event reporter
rahul-lohra Jun 4, 2026
a16cccc
chore: refactor client event reporter
rahul-lohra Jun 4, 2026
5e31613
chore: refactor client event reporter
rahul-lohra Jun 4, 2026
93d60cd
chore: spotless
rahul-lohra Jun 4, 2026
489816f
chore: send missing call_session_id
rahul-lohra Jun 4, 2026
927166c
chore: rename files, introduce datasource
rahul-lohra Jun 5, 2026
2f81177
chore: refactor models and packages in analytics
rahul-lohra Jun 5, 2026
7edfd08
chore: rename files
rahul-lohra Jun 5, 2026
bdf34cd
chore: send missing call session id
rahul-lohra Jun 5, 2026
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
7 changes: 7 additions & 0 deletions demo-app/src/main/kotlin/io/getstream/video/android/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.getstream.video.android.core.moderations.CallModerationConstants
import io.getstream.video.android.data.model.PolicyViolationUiData
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
import io.getstream.video.android.tooling.util.StreamBuildFlavorUtil
import io.getstream.video.android.ui.FailureInjectorImpl
import io.getstream.video.android.util.StreamVideoInitHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -80,6 +81,12 @@ class App : Application() {

observePolicyViolation()
observeCallReadyToJoin()
injectFault()
}

private fun injectFault() {
val faultInjector = FailureInjectorImpl()
StreamVideo.instanceOrNull()?.state?.failureInjector = faultInjector
Comment on lines +84 to +89
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Install the injector for SDK instances created later.

StreamVideo.instanceOrNull() can still be null here on cold starts without a persisted user, and this file already documents that deferred SDK init is a supported path. In that case the injector is never attached, so the new failure-injection menu silently stops working after login. Please hook this into SDK creation, or observe StreamVideo.instanceState and reapply it whenever a new instance appears.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@demo-app/src/main/kotlin/io/getstream/video/android/App.kt` around lines 84 -
89, The current injectFault() only sets failureInjector on
StreamVideo.instanceOrNull(), which can be null at cold start so later-created
SDK instances never receive the injector; change injectFault() to also observe
StreamVideo.instanceState (or hook into the SDK creation path) and set
state.failureInjector = FailureInjectorImpl() whenever a new instance appears so
every future StreamVideo instance gets the injector; reference the injectFault()
function, FailureInjectorImpl, StreamVideo.instanceOrNull(), and
StreamVideo.instanceState when implementing the observer/creation hook and
ensure you reapply the injector on instance creation events.

}

private fun observeCallReadyToJoin() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class CallActivity : ComposeStreamCallActivity() {
override val uiDelegate: StreamActivityUiDelegate<StreamCallActivity> = StreamDemoUiDelegate()
var observeCallReadyToJoinJob: Job? = null
var observeRingingJob: Job? = null
var isNavigatingBackToMainScreen = false
private val previousRingingStates = ConcurrentHashMap.newKeySet<RingingState>()
override val callJoinInterceptor = DemoCallJoinInterceptor(previousRingingStates)

Expand Down Expand Up @@ -194,6 +195,7 @@ class CallActivity : ComposeStreamCallActivity() {

private fun StreamCallActivity.goBackToMainScreen() {
if (!isFinishing) {
(this as CallActivity).isNavigatingBackToMainScreen = true
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
Expand All @@ -208,5 +210,13 @@ class CallActivity : ComposeStreamCallActivity() {
observeCallReadyToJoinJob?.cancel()
observeRingingJob?.cancel()
previousRingingStates.clear()

if (!isNavigatingBackToMainScreen) {
isNavigatingBackToMainScreen = true
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2014-2026 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-video-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.video.android.ui

import io.getstream.result.Error
import io.getstream.video.android.core.faultinjector.FailureInjector
import io.getstream.video.android.core.faultinjector.FailureKey
import retrofit2.HttpException
import retrofit2.Response

internal class FailureInjectorImpl : FailureInjector {
private val faultCounts = mutableMapOf<FailureKey, Int>()

override fun enable(key: FailureKey) {
if ((faultCounts[key] ?: 0) == 0) faultCounts[key] = 1
}

override fun disable(key: FailureKey) {
faultCounts[key] = 0
}

override fun setEnabled(key: FailureKey, enabled: Boolean) {
if (enabled) enable(key) else disable(key)
}

override fun isEnabled(key: FailureKey): Boolean {
return (faultCounts[key] ?: 0) > 0
}

override fun setCount(key: FailureKey, count: Int) {
faultCounts[key] = count
}

override fun getCount(key: FailureKey): Int {
return faultCounts[key] ?: 0
}

override fun clear() {
faultCounts.clear()
}

override fun throwDebugFault(key: FailureKey) {
val count = faultCounts[key] ?: 0
if (count > 0) {
faultCounts[key] = count - 1
throw when (key) {
FailureKey.FAIL_LOCATION -> HttpException(
Response.error<String>(
100,
okhttp3.ResponseBody.create(null, ""),
),
)
else -> RuntimeException("Failure injected: $key")
}
}
}

override fun sendFailResult(key: FailureKey): io.getstream.result.Result.Failure {
val count = faultCounts[key] ?: 0
if (count > 0) {
faultCounts[key] = count - 1
}
Comment on lines +26 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show FailureInjectorImpl implementation fully (bounded)
FILE="demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorImpl.kt"
echo "== $FILE (relevant excerpt) =="
nl -ba "$FILE" | sed -n '1,220p'

# 2) Find all usages of FailureInjectorImpl / FailureInjector interface
echo "== Searching for FailureInjectorImpl / FailureInjector usage =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector" demo-app/src/main/kotlin || true

# 3) Specifically find calls to throwDebugFault / sendFailResult and show call sites
echo "== Call sites for throwDebugFault / sendFailResult =="
rg -n --hidden --glob '!*build/*' "throwDebugFault\(|sendFailResult\(" demo-app/src/main/kotlin || true

# 4) Also check non-demo modules if FailureInjectorImpl is referenced elsewhere
echo "== Call sites across repository (optional) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|throwDebugFault\(|sendFailResult\(" || true

# 5) Check for coroutine/thread contexts around join/socket/RTC flows mentioning these fault methods
echo "== Looking for join/socket/RTC fault injection references =="
rg -n --hidden --glob '!*build/*' "join|socket|rtc|RT|WebSocket" demo-app/src/main/kotlin/io/getstream/video/android/ui demo-app/src/main/kotlin/io/getstream/video/android || true

Repository: GetStream/stream-video-android

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorImpl.kt"

echo "== File length =="
wc -l "$FILE" || true

echo "== $FILE (lines 1-220) =="
cat -n "$FILE" | sed -n '1,220p'

echo "== Call sites for throwDebugFault / sendFailResult (demo-app) =="
rg -n --hidden --glob '!*build/*' "throwDebugFault\(|sendFailResult\(" demo-app/src/main/kotlin || true

echo "== References to FailureInjector / FailureInjectorImpl (demo-app) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector" demo-app/src/main/kotlin || true

echo "== References across repository (only these symbols) =="
rg -n --hidden --glob '!*build/*' "FailureInjectorImpl|FailureInjector|throwDebugFault\(|sendFailResult\(" || true

Repository: GetStream/stream-video-android

Length of output: 10317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant call-site code around the referenced line numbers
sed -n '200,280p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt | nl -ba | sed -n '1,120p' || true
sed -n '420,520p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt | nl -ba | sed -n '1,140p' || true
sed -n '760,880p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt | nl -ba | sed -n '1,160p' || true
sed -n '1,160p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt | nl -ba | sed -n '1,200p' || true

# Demo UI wiring: how FailureInjectorImpl is created and used
sed -n '60,120p' demo-app/src/main/kotlin/io/getstream/video/android/App.kt | nl -ba | sed -n '1,140p' || true
sed -n '180,280p' demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorUi.kt | nl -ba | sed -n '1,200p' || true
sed -n '80,260p' demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt | nl -ba | sed -n '1,220p' || true

Repository: GetStream/stream-video-android

Length of output: 364


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SfuSocket.kt excerpt around throwDebugFault calls =="

# show a bit around the previously reported line numbers
sed -n '200,270p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt

echo
echo "== StreamVideoClient.kt excerpt around sendFailResult calls =="

sed -n '430,490p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
echo
sed -n '800,860p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt

echo
echo "== ClientState.kt excerpt about failureInjector =="

sed -n '1,160p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt

echo
echo "== demo-app App.kt excerpt where FailureInjectorImpl is created =="

sed -n '60,120p' demo-app/src/main/kotlin/io/getstream/video/android/App.kt

echo
echo "== demo-app FailureInjectorUi.kt excerpt where a FailureInjector is provided =="

sed -n '1,320p' demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorUi.kt | head -n 260

echo
echo "== demo-app CallJoinScreen.kt excerpt wiring FailureInjectorUi =="

sed -n '180,260p' demo-app/src/main/kotlin/io/getstream/video/android/ui/join/CallJoinScreen.kt

Repository: GetStream/stream-video-android

Length of output: 29260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"

echo "== Occurrences of debugFaultInjectors( =="
rg -n "debugFaultInjectors\(" "$FILE"

# show around each occurrence
echo "== Context around debugFaultInjectors call sites =="
# use rg line numbers then sed ranges
python3 - <<'PY'
import re, subprocess, os, sys, textwrap, json
file_path = "stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"
out = subprocess.check_output(["rg","-n","debugFaultInjectors\\(",file_path], text=True)
for line in out.strip().splitlines():
    m = re.match(r".*:(\d+):", line)
    if not m: 
        continue
    ln = int(m.group(1))
    start = max(1, ln-40)
    end = ln+60
    print(f"\n--- {file_path}:{ln} (showing {start}-{end}) ---")
    # use sed to print range
    subprocess.run(["sed","-n",f"{start},{end}p",file_path], check=False)
PY

Repository: GetStream/stream-video-android

Length of output: 320


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/sfu/SfuSocket.kt"

echo "== SfuSocket.kt first 120 lines =="
sed -n '1,140p' "$FILE"

echo
echo "== SfuSocket.kt lines 140-280 =="
sed -n '140,280p' "$FILE"

echo
echo "== Search for userScope / coroutine scope usage in SfuSocket.kt =="
rg -n "userScope|CoroutineScope|launch\(|withContext\(|Dispatchers\." "$FILE"

Repository: GetStream/stream-video-android

Length of output: 13748


Make FailureInjectorImpl.faultCounts thread-safe

faultCounts is an unsynchronized mutableMapOf that’s mutated by Compose UI actions (clear, setCount) and by core async flows (SfuSocket.debugFaultInjectors via throwDebugFault, and StreamVideoClient via sendFailResult). These concurrent reads/writes/decrements can race and lose updates, making failure counts nondeterministic—guard all accesses with a Mutex/synchronized block or switch to an atomic/thread-safe structure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@demo-app/src/main/kotlin/io/getstream/video/android/ui/FailureInjectorImpl.kt`
around lines 26 - 76, The mutable map faultCounts in FailureInjectorImpl is not
thread-safe; guard all accesses/updates (reads, writes, decrement) to
faultCounts used by
enable/disable/setEnabled/isEnabled/setCount/getCount/clear/throwDebugFault/sendFailResult
to avoid races—either replace faultCounts with a thread-safe structure (e.g.,
ConcurrentHashMap<FailureKey, AtomicInteger> with atomic increments/decrements)
or serialize access using a Mutex/synchronized block around each method that
touches faultCounts; pick one approach and apply it consistently to every method
that reads or mutates faultCounts so counts cannot be lost under concurrent
Compose/UI and async flows.

val message = when (key) {
FailureKey.FAIL_JOIN_CALL -> "Unable to resolve host"
else -> "Failure injected: $key"
}
return io.getstream.result.Result.Failure(
Error.ThrowableError(
message,
RuntimeException(message),
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/*
* Copyright (c) 2014-2026 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-video-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.video.android.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.getstream.result.Error
import io.getstream.video.android.compose.theme.VideoTheme
import io.getstream.video.android.core.faultinjector.FailureInjector
import io.getstream.video.android.core.faultinjector.FailureKey
import io.getstream.video.android.core.internal.InternalStreamVideoApi

private val countOptions = listOf(0, 1, 2, 3, 5, 10)

@OptIn(InternalStreamVideoApi::class)
@Composable
fun FailureInjectorUi(
modifier: Modifier = Modifier,
failureInjector: FailureInjector,
onClose: () -> Unit,
) {
val countState = remember {
mutableStateMapOf<FailureKey, Int>().apply {
FailureKey.entries.forEach { key -> put(key, failureInjector.getCount(key)) }
}
}

Column(
modifier = Modifier
.fillMaxSize()
.background(VideoTheme.colors.baseSheetPrimary),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(VideoTheme.colors.baseSheetSecondary)
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Failure Injection",
style = VideoTheme.typography.subtitleM,
color = VideoTheme.colors.basePrimary,
)

Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Clear all",
modifier = Modifier.clickable {
failureInjector.clear()
FailureKey.entries.forEach { key -> countState[key] = 0 }
},
style = VideoTheme.typography.bodyS,
color = VideoTheme.colors.brandPrimary,
)

Box(
modifier = Modifier
.background(
color = VideoTheme.colors.baseSheetTertiary,
shape = RoundedCornerShape(999.dp),
)
.clickable(onClick = onClose)
.padding(8.dp),
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
tint = VideoTheme.colors.basePrimary,
)
}
}
}

LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(FailureKey.entries) { key ->
val count = countState[key] ?: 0
var expanded by remember { mutableStateOf(false) }

Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = key.name,
style = VideoTheme.typography.bodyM,
color = VideoTheme.colors.basePrimary,
modifier = Modifier.weight(1f),
)

Box {
Row(
modifier = Modifier
.background(
color = VideoTheme.colors.baseSheetTertiary,
shape = RoundedCornerShape(6.dp),
)
.clickable { expanded = true }
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "$count",
style = VideoTheme.typography.bodyM,
color = if (count > 0) VideoTheme.colors.brandPrimary else VideoTheme.colors.baseSecondary,
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null,
tint = VideoTheme.colors.baseSecondary,
)
}

DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
countOptions.forEach { option ->
DropdownMenuItem(
onClick = {
countState[key] = option
failureInjector.setCount(key, option)
expanded = false
},
) {
Text(
text = "$option",
style = VideoTheme.typography.bodyM,
color = if (option > 0) VideoTheme.colors.brandPrimary else VideoTheme.colors.basePrimary,
)
}
}
}
}
}
}
}

Box(
modifier = Modifier
.fillMaxWidth()
.background(VideoTheme.colors.baseSheetSecondary)
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Button(
onClick = onClose,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = VideoTheme.colors.brandPrimary,
contentColor = VideoTheme.colors.baseSheetPrimary,
),
) {
Text(
text = "OK",
style = VideoTheme.typography.labelL,
)
}
}
}
}

@Preview
@Composable
fun FaultInjectorUiDemo() {
VideoTheme {
FailureInjectorUi(
Modifier,
object : FailureInjector {
override fun enable(key: FailureKey) {}

override fun disable(key: FailureKey) {}

override fun setEnabled(key: FailureKey, enabled: Boolean) {}

override fun isEnabled(key: FailureKey): Boolean = false

override fun setCount(key: FailureKey, count: Int) {}

override fun getCount(key: FailureKey): Int = 0

override fun clear() {}

override fun throwDebugFault(key: FailureKey) {}

override fun sendFailResult(
key: FailureKey,
): io.getstream.result.Result.Failure {
return io.getstream.result.Result.Failure(
Error.GenericError("Failure injected: $key"),
)
}
},
) {}
}
}
Loading
Loading