Skip to content

Commit 9a40273

Browse files
wojciechowskiradekOliver-Zimmermanai-swe-agentopenhands-agent
authored
WEBRTC-2699: Call Quality Metrics UI Redesign (#546)
* chore: bump SDK version and add changelog * chore: add PR release checklist to PR markdown template * chore: fix typo in PR template * Update CHANGELOG.md * WEBRTC-2677: Fix infinite loading dialog on login error (#526) * WEBRTC-2677: Fix infinite loading dialog on login error * feat: separating session error propagation from session state --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: wojciechowskiradek <radoslaw@telnyx.com> * Fix: save voiceSdkId after login (#525) * WEBRTC-2676: [Android] Navigate to login / connect screen and clear call states after DROPPED (#527) * WEBRTC-2676: Navigate to login/connect screen and clear call states after DROPPED * fix: set disconnected state when call has a error state * chore: description of handleError function --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: wojciechowskiradek <radoslaw@telnyx.com> * WEBRTC-2678: Fix app not disconnecting after rejecting push notification (#528) Co-authored-by: openhands <openhands@all-hands.dev> * fix: login with last used profile when rejecting a push notification call * Fix: Push Notification call reject failed (#540) * fix: reject incoming push notification call when app is in background * fix: App not disconnecting after rejecting push notification * fix: debug stats are created when call metrics are enabled (#541) * WEBRTC-2699: Redesign Call Quality Metrics UI - Added a call quality summary section with colored dot and quality name - Added 'View all call metrics' button with transparent background and rounded corners - Moved detailed CallQualityDisplay to a ModalBottomSheet - Implemented bottom sheet to show detailed metrics when button is pressed * feat: updated Call Quality Metrics UI for compose app * WEBRTC-2699: Implement Call Quality Metrics UI Redesign for XML app - Added call quality summary view with colored dot and quality name - Added 'View all call metrics' button with transparent background and rounded corners - Implemented bottom sheet to show detailed metrics when button is pressed - Maintained the same design pattern as the Compose app implementation * feat: updated Call Quality Metrics UI for XML app * chore: audio waves moved to the top of the screen * fix: call metrics for incoming push call --------- Co-authored-by: Oliver Zimmerman <oezimmerman@gmail.com> Co-authored-by: ai-swe-agent <ai.swe.agent@telnyx.com> Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 9af778a commit 9a40273

File tree

22 files changed

+985
-394
lines changed

22 files changed

+985
-394
lines changed

samples/compose_app/src/main/java/org/telnyx/webrtc/compose_app/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class MainActivity : ComponentActivity(), DefaultLifecycleObserver {
8686
Timber.d("Action: $action ${txPushMetaData ?: "No Metadata"}")
8787
when (action) {
8888
MyFirebaseMessagingService.ACT_ANSWER_CALL -> {
89-
viewModel.answerIncomingPushCall(this, txPushMetaData)
89+
viewModel.answerIncomingPushCall(this, txPushMetaData, true)
9090
}
9191
MyFirebaseMessagingService.ACT_REJECT_CALL -> {
9292
viewModel.rejectIncomingPushCall(this, txPushMetaData)
Lines changed: 110 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.telnyx.webrtc.compose_app.ui.components
22

33
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
45
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.Column
67
import androidx.compose.foundation.layout.Row
@@ -9,7 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
910
import androidx.compose.foundation.layout.height
1011
import androidx.compose.foundation.layout.padding
1112
import androidx.compose.foundation.layout.width
13+
import androidx.compose.foundation.rememberScrollState
1214
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.foundation.verticalScroll
1316
import androidx.compose.material3.Card
1417
import androidx.compose.material3.CardDefaults
1518
import androidx.compose.material3.CircularProgressIndicator
@@ -18,12 +21,20 @@ import androidx.compose.material3.Text
1821
import androidx.compose.runtime.Composable
1922
import androidx.compose.ui.Alignment
2023
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
2125
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.res.stringResource
2227
import androidx.compose.ui.text.font.FontWeight
2328
import androidx.compose.ui.unit.dp
2429
import com.telnyx.webrtc.sdk.stats.CallQuality
2530
import com.telnyx.webrtc.sdk.stats.CallQualityMetrics
31+
import org.telnyx.webrtc.compose_app.R
32+
import org.telnyx.webrtc.compose_app.ui.theme.Dimens
33+
import org.telnyx.webrtc.compose_app.ui.theme.secondary_background_color
2634
import org.telnyx.webrtc.compose_app.ui.viewcomponents.AudioWaveform
35+
import org.telnyx.webrtc.compose_app.ui.viewcomponents.MediumTextBold
36+
import org.telnyx.webrtc.compose_app.ui.viewcomponents.RegularText
37+
import org.telnyx.webrtc.compose_app.utils.capitalizeFirstChar
2738

2839
/**
2940
* A composable that displays call quality metrics.
@@ -42,106 +53,95 @@ fun CallQualityDisplay(
4253
) {
4354
if (metrics == null && inboundLevels.isEmpty() && outboundLevels.isEmpty()) return
4455

45-
Card(
46-
modifier = modifier
56+
Column(
57+
modifier = Modifier
4758
.fillMaxWidth()
48-
.padding(16.dp),
49-
shape = RoundedCornerShape(8.dp),
50-
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
59+
.padding(16.dp)
60+
.verticalScroll(rememberScrollState()),
61+
verticalArrangement = Arrangement.spacedBy(Dimens.spacing16dp)
5162
) {
52-
Column(
53-
modifier = Modifier
54-
.fillMaxWidth()
55-
.padding(16.dp)
56-
) {
57-
// Show detailed metrics once quality is known
58-
Text(
59-
text = "Call Quality Metrics",
60-
style = MaterialTheme.typography.titleMedium,
61-
fontWeight = FontWeight.Bold
63+
if (metrics?.quality == CallQuality.UNKNOWN) {
64+
// Show loading indicator if quality is unknown
65+
Box(
66+
modifier = Modifier.fillMaxWidth(),
67+
contentAlignment = Alignment.Center
68+
) {
69+
CircularProgressIndicator(modifier = Modifier.width(24.dp)) // Adjust size as needed
70+
}
71+
} else {
72+
// Inbound Waveform
73+
RegularText(text = stringResource(R.string.call_quality_metrics_inbound_audio_level),
74+
size = Dimens.textSize16sp,
75+
fontWeight = FontWeight.SemiBold)
76+
77+
Spacer(modifier = Modifier.height(4.dp))
78+
79+
AudioWaveform(
80+
audioLevels = inboundLevels,
81+
barColor = MaterialTheme.colorScheme.primary,
82+
modifier = Modifier
83+
.fillMaxWidth()
84+
.height(50.dp)
6285
)
6386

6487
Spacer(modifier = Modifier.height(16.dp))
6588

66-
if (metrics?.quality == CallQuality.UNKNOWN) {
67-
// Show loading indicator if quality is unknown
68-
Box(
69-
modifier = Modifier.fillMaxWidth(),
70-
contentAlignment = Alignment.Center
71-
) {
72-
CircularProgressIndicator(modifier = Modifier.width(24.dp)) // Adjust size as needed
73-
}
74-
} else {
75-
76-
// Quality indicator
77-
Row(
78-
verticalAlignment = Alignment.CenterVertically,
79-
modifier = Modifier.fillMaxWidth()
80-
) {
81-
Text(
82-
text = "Quality:",
83-
style = MaterialTheme.typography.bodyMedium,
84-
fontWeight = FontWeight.Bold
85-
)
86-
87-
Spacer(modifier = Modifier.width(8.dp))
88-
89-
QualityIndicator(quality = metrics?.quality ?: CallQuality.UNKNOWN)
90-
}
91-
92-
Spacer(modifier = Modifier.height(8.dp))
93-
94-
// MOS score
95-
MetricRow(
96-
label = "MOS Score:",
97-
value = String.format("%.2f", metrics?.mos ?: 0.0)
98-
)
99-
100-
// Jitter
101-
MetricRow(
102-
label = "Jitter:",
103-
value = String.format("%.2f ms", metrics?.jitter?.times(1000) ?: 0.0)
104-
)
105-
106-
// Round-trip time
107-
MetricRow(
108-
label = "Round-trip Time:",
109-
value = String.format("%.2f ms", metrics?.rtt?.times(1000) ?: 0.0)
110-
)
111-
112-
Spacer(modifier = Modifier.height(16.dp))
113-
114-
// Inbound Waveform
115-
Text(
116-
text = "Inbound Level:",
117-
style = MaterialTheme.typography.bodyMedium,
118-
fontWeight = FontWeight.Bold
119-
)
120-
Spacer(modifier = Modifier.height(4.dp))
121-
AudioWaveform(
122-
audioLevels = inboundLevels,
123-
barColor = MaterialTheme.colorScheme.primary,
124-
modifier = Modifier
125-
.fillMaxWidth()
126-
.height(50.dp)
127-
)
128-
129-
Spacer(modifier = Modifier.height(16.dp))
130-
131-
// Outbound Waveform
132-
Text(
133-
text = "Outbound Level:",
134-
style = MaterialTheme.typography.bodyMedium,
135-
fontWeight = FontWeight.Bold
136-
)
137-
Spacer(modifier = Modifier.height(4.dp))
138-
AudioWaveform(
139-
audioLevels = outboundLevels,
140-
barColor = MaterialTheme.colorScheme.primary,
141-
modifier = Modifier
142-
.fillMaxWidth()
143-
.height(50.dp)
144-
)
89+
// Outbound Waveform
90+
RegularText(text = stringResource(R.string.call_quality_metrics_outbound_audio_level),
91+
size = Dimens.textSize16sp,
92+
fontWeight = FontWeight.SemiBold)
93+
94+
Spacer(modifier = Modifier.height(4.dp))
95+
96+
AudioWaveform(
97+
audioLevels = outboundLevels,
98+
barColor = MaterialTheme.colorScheme.primary,
99+
modifier = Modifier
100+
.fillMaxWidth()
101+
.height(50.dp)
102+
)
103+
104+
// Jitter
105+
MetricRow(
106+
label = stringResource(R.string.call_quality_metrics_jitter),
107+
value = String.format("%.2f ms", metrics?.jitter?.times(1000) ?: 0.0)
108+
)
109+
110+
// MOS score
111+
MetricRow(
112+
label = stringResource(R.string.call_quality_metrics_mos),
113+
value = String.format("%.2f", metrics?.mos ?: 0.0)
114+
)
115+
116+
// Quality
117+
MetricRow(
118+
label = stringResource(R.string.call_quality_metrics_quality),
119+
value = metrics?.quality?.name?.capitalizeFirstChar() ?: stringResource(
120+
R.string.unknown_label)
121+
)
122+
123+
// Round-trip time
124+
MetricRow(
125+
label = stringResource(R.string.call_quality_metrics_round_trip_time),
126+
value = String.format("%.2f ms", metrics?.rtt?.times(1000) ?: 0.0)
127+
)
128+
129+
//Inbound audio
130+
RegularText(text = stringResource(R.string.call_quality_metrics_inbound_audio),
131+
size = Dimens.textSize16sp,
132+
fontWeight = FontWeight.SemiBold)
133+
134+
metrics?.inboundAudio?.forEach { (key, value) ->
135+
MetricRow(key.capitalizeFirstChar() ?: stringResource(R.string.unknown_label), value.toString())
136+
}
137+
138+
//Outbound audio
139+
RegularText(text = stringResource(R.string.call_quality_metrics_outbound_audio),
140+
size = Dimens.textSize16sp,
141+
fontWeight = FontWeight.SemiBold)
142+
143+
metrics?.outboundAudio?.forEach { (key, value) ->
144+
MetricRow(key.capitalizeFirstChar() ?: stringResource(R.string.unknown_label), value.toString())
145145
}
146146
}
147147
}
@@ -161,62 +161,23 @@ private fun MetricRow(
161161
modifier: Modifier = Modifier
162162
) {
163163
Row(
164-
verticalAlignment = Alignment.CenterVertically,
165-
modifier = modifier.fillMaxWidth()
166-
) {
167-
Text(
168-
text = label,
169-
style = MaterialTheme.typography.bodyMedium,
170-
fontWeight = FontWeight.Bold
171-
)
172-
173-
Spacer(modifier = Modifier.width(8.dp))
174-
175-
Text(
176-
text = value,
177-
style = MaterialTheme.typography.bodyMedium
178-
)
179-
}
180-
}
181-
182-
/**
183-
* A visual indicator of call quality.
184-
*
185-
* @param quality The call quality to display.
186-
* @param modifier Optional modifier for the component.
187-
*/
188-
@Composable
189-
private fun QualityIndicator(
190-
quality: CallQuality,
191-
modifier: Modifier = Modifier
192-
) {
193-
val (color, text) = when (quality) {
194-
CallQuality.EXCELLENT -> Color(0xFF4CAF50) to "Excellent"
195-
CallQuality.GOOD -> Color(0xFF8BC34A) to "Good"
196-
CallQuality.FAIR -> Color(0xFFFFC107) to "Fair"
197-
CallQuality.POOR -> Color(0xFFFF9800) to "Poor"
198-
CallQuality.BAD -> Color(0xFFF44336) to "Bad"
199-
CallQuality.UNKNOWN -> Color(0xFF9E9E9E) to "Unknown"
200-
}
201-
202-
Row(
203-
verticalAlignment = Alignment.CenterVertically,
204164
modifier = modifier
165+
.fillMaxWidth()
166+
.clip(RoundedCornerShape(Dimens.size4dp))
167+
.background(secondary_background_color)
168+
.padding(start = Dimens.mediumPadding, end = Dimens.mediumPadding, top = Dimens.smallPadding, bottom = Dimens.smallPadding),
169+
verticalAlignment = Alignment.CenterVertically
205170
) {
206-
// Color indicator
207-
Spacer(
171+
RegularText(
208172
modifier = Modifier
209-
.width(16.dp)
210-
.height(16.dp)
211-
.background(color, RoundedCornerShape(4.dp))
212-
)
213-
214-
Spacer(modifier = Modifier.width(8.dp))
215-
216-
// Quality text
217-
Text(
218-
text = text,
219-
style = MaterialTheme.typography.bodyMedium
220-
)
173+
.padding(end = Dimens.smallPadding),
174+
text = label)
175+
176+
Spacer(modifier = Modifier.weight(1f))
177+
178+
RegularText(text = value,
179+
size = Dimens.textSize16sp,
180+
fontWeight = FontWeight.SemiBold,
181+
maxLines = 1)
221182
}
222-
}
183+
}

0 commit comments

Comments
 (0)