From 55bb97b673fdccc29d71de5321c0695494d6e888 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:10:12 +0200 Subject: [PATCH 1/5] feat: use TokenSource in sample apps with a development token server option The connect screens of sample-app and sample-app-compose get two tabs: the existing literal URL/token entry and a new development token server mode that takes a token server ID. Both paths now go through the SDK's TokenSource API: the shared CallViewModel receives a parcelable TokenSourceArgs and fetches connection details at connect time via TokenSource.fromLiteral or TokenSource.fromDevelopmentTokenServer before calling room.connect. The token server ID is persisted alongside the existing url/token preferences and can be defaulted via the livekitSampleTokenServerId gradle property. Co-Authored-By: Claude Fable 5 --- sample-app-common/build.gradle | 7 + .../livekit/android/sample/CallViewModel.kt | 18 ++- .../livekit/android/sample/MainViewModel.kt | 25 ++++ .../android/sample/model/TokenSourceArgs.kt | 38 +++++ .../android/composesample/CallActivity.kt | 7 +- .../android/composesample/MainActivity.kt | 130 ++++++++++++------ .../io/livekit/android/sample/CallActivity.kt | 7 +- .../io/livekit/android/sample/MainActivity.kt | 40 +++++- .../src/main/res/layout/main_activity.xml | 32 +++++ sample-app/src/main/res/values/strings.xml | 3 + 10 files changed, 253 insertions(+), 54 deletions(-) create mode 100644 sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt diff --git a/sample-app-common/build.gradle b/sample-app-common/build.gradle index 851579647..ce856ad54 100644 --- a/sample-app-common/build.gradle +++ b/sample-app-common/build.gradle @@ -12,8 +12,13 @@ def getDefaultToken() { return hasProperty('livekitSampleToken') ? livekitSampleToken : "" } +def getDefaultTokenServerId() { + return hasProperty('livekitSampleTokenServerId') ? livekitSampleTokenServerId : "" +} + final url = getDefaultUrl() final token = getDefaultToken() +final tokenServerId = getDefaultTokenServerId() android { namespace "io.livekit.android.sample.common" @@ -34,11 +39,13 @@ android { buildConfigField "String", "DEFAULT_URL", "\"$url\"" buildConfigField "String", "DEFAULT_TOKEN", "\"$token\"" + buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\"" } debug { buildConfigField "String", "DEFAULT_URL", "\"$url\"" buildConfigField "String", "DEFAULT_TOKEN", "\"$token\"" + buildConfigField "String", "DEFAULT_TOKEN_SERVER_ID", "\"$tokenServerId\"" } } buildFeatures { diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt index b218278af..2ae963f39 100644 --- a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt +++ b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt @@ -51,7 +51,10 @@ import io.livekit.android.room.track.screencapture.ScreenCaptureParams import io.livekit.android.room.track.video.CameraCapturerUtils import io.livekit.android.rpc.RpcError import io.livekit.android.sample.model.StressTest +import io.livekit.android.sample.model.TokenSourceArgs import io.livekit.android.sample.service.ForegroundService +import io.livekit.android.token.TokenSource +import io.livekit.android.token.TokenSourceResponse import io.livekit.android.util.LKLog import io.livekit.android.util.flow import kotlinx.coroutines.Dispatchers @@ -68,8 +71,7 @@ import livekit.org.webrtc.CameraXHelper @OptIn(ExperimentalCamera2Interop::class) class CallViewModel( - val url: String, - val token: String, + val tokenSourceArgs: TokenSourceArgs, application: Application, val e2ee: Boolean = false, val e2eeKey: String? = "", @@ -255,12 +257,18 @@ class CallViewModel( } } + private suspend fun fetchConnectionDetails(): Result = when (tokenSourceArgs) { + is TokenSourceArgs.Literal -> TokenSource.fromLiteral(tokenSourceArgs.url, tokenSourceArgs.token).fetch() + is TokenSourceArgs.DevTokenServer -> TokenSource.fromDevelopmentTokenServer(tokenSourceArgs.tokenServerId).fetch() + } + private suspend fun connectToRoom() { try { room.e2eeOptions = getE2EEOptions() + val connectionDetails = fetchConnectionDetails().getOrThrow() room.connect( - url = url, - token = token, + url = connectionDetails.serverUrl, + token = connectionDetails.participantToken, ) mutableEnhancedNsEnabled.postValue(room.audioProcessorIsEnabled) @@ -495,6 +503,8 @@ class CallViewModel( private suspend fun quickConnectToRoom(token: String) { try { + // The stress test is only reachable with literal credentials. + val url = (tokenSourceArgs as TokenSourceArgs.Literal).url room.connect( url = url, token = token, diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/MainViewModel.kt b/sample-app-common/src/main/java/io/livekit/android/sample/MainViewModel.kt index 8afad859f..3ce0981b9 100644 --- a/sample-app-common/src/main/java/io/livekit/android/sample/MainViewModel.kt +++ b/sample-app-common/src/main/java/io/livekit/android/sample/MainViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.livekit.android.sample import android.app.Application @@ -12,6 +28,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun getSavedUrl() = preferences.getString(PREFERENCES_KEY_URL, URL) as String fun getSavedToken() = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String + fun getSavedTokenServerId() = preferences.getString(PREFERENCES_KEY_TOKEN_SERVER_ID, TOKEN_SERVER_ID) as String fun getE2EEOptionsOn() = preferences.getBoolean(PREFERENCES_KEY_E2EE_ON, false) fun getSavedE2EEKey() = preferences.getString(PREFERENCES_KEY_E2EE_KEY, E2EE_KEY) as String @@ -27,6 +44,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } } + fun setSavedTokenServerId(tokenServerId: String) { + preferences.edit { + putString(PREFERENCES_KEY_TOKEN_SERVER_ID, tokenServerId) + } + } + fun setSavedE2EEOn(yesno: Boolean) { preferences.edit { putBoolean(PREFERENCES_KEY_E2EE_ON, yesno) @@ -46,11 +69,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { companion object { private const val PREFERENCES_KEY_URL = "url" private const val PREFERENCES_KEY_TOKEN = "token" + private const val PREFERENCES_KEY_TOKEN_SERVER_ID = "token_server_id" private const val PREFERENCES_KEY_E2EE_ON = "enable_e2ee" private const val PREFERENCES_KEY_E2EE_KEY = "e2ee_key" const val URL = BuildConfig.DEFAULT_URL const val TOKEN = BuildConfig.DEFAULT_TOKEN + const val TOKEN_SERVER_ID = BuildConfig.DEFAULT_TOKEN_SERVER_ID const val E2EE_KEY = "12345678" } } diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt b/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt new file mode 100644 index 000000000..2584a2a2e --- /dev/null +++ b/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.livekit.android.sample.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Describes which token source to connect with, in a parcelable form + * so it can be passed through activity intents. + */ +sealed class TokenSourceArgs : Parcelable { + + @Parcelize + data class Literal( + val url: String, + val token: String, + ) : TokenSourceArgs() + + @Parcelize + data class DevTokenServer( + val tokenServerId: String, + ) : TokenSourceArgs() +} diff --git a/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt b/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt index 82523a6be..da4f81081 100644 --- a/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt +++ b/sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt @@ -76,6 +76,7 @@ import io.livekit.android.room.participant.Participant import io.livekit.android.sample.CallViewModel import io.livekit.android.sample.common.R import io.livekit.android.sample.model.StressTest +import io.livekit.android.sample.model.TokenSourceArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -86,8 +87,7 @@ class CallActivity : AppCompatActivity() { val args = intent.getParcelableExtra(KEY_ARGS) ?: throw NullPointerException("args is null!") CallViewModel( - url = args.url, - token = args.token, + tokenSourceArgs = args.tokenSourceArgs, e2ee = args.e2eeOn, e2eeKey = args.e2eeKey, stressTest = args.stressTest, @@ -511,8 +511,7 @@ class CallActivity : AppCompatActivity() { @Parcelize data class BundleArgs( - val url: String, - val token: String, + val tokenSourceArgs: TokenSourceArgs, val e2eeKey: String, val e2eeOn: Boolean, val stressTest: StressTest, diff --git a/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt b/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt index f0e4a506c..58ee09e5e 100644 --- a/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt +++ b/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 LiveKit, Inc. + * Copyright 2023-2026 LiveKit, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,8 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Switch +import androidx.compose.material.Tab +import androidx.compose.material.TabRow import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -47,17 +49,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.ExperimentalPagerApi import io.livekit.android.composesample.ui.theme.AppTheme import io.livekit.android.sample.MainViewModel import io.livekit.android.sample.common.R import io.livekit.android.sample.model.StressTest +import io.livekit.android.sample.model.TokenSourceArgs import io.livekit.android.sample.util.requestNeededPermissions -@ExperimentalPagerApi +private enum class TokenMode { + Literal, + DevServer, +} + class MainActivity : ComponentActivity() { private val viewModel by viewModels() @@ -69,15 +76,15 @@ class MainActivity : ComponentActivity() { MainContent( defaultUrl = viewModel.getSavedUrl(), defaultToken = viewModel.getSavedToken(), + defaultTokenServerId = viewModel.getSavedTokenServerId(), defaultE2eeKey = viewModel.getSavedE2EEKey(), defaultE2eeOn = viewModel.getE2EEOptionsOn(), - onConnect = { url, token, e2eeKey, e2eeOn, stressTest -> + onConnect = { tokenSourceArgs, e2eeKey, e2eeOn, stressTest -> val intent = Intent(this@MainActivity, CallActivity::class.java).apply { putExtra( CallActivity.KEY_ARGS, CallActivity.BundleArgs( - url, - token, + tokenSourceArgs, e2eeKey, e2eeOn, stressTest, @@ -86,9 +93,10 @@ class MainActivity : ComponentActivity() { } startActivity(intent) }, - onSave = { url, token, e2eeKey, e2eeOn -> + onSave = { url, token, tokenServerId, e2eeKey, e2eeOn -> viewModel.setSavedUrl(url) viewModel.setSavedToken(token) + viewModel.setSavedTokenServerId(tokenServerId) viewModel.setSavedE2EEKey(e2eeKey) viewModel.setSavedE2EEOn(e2eeOn) @@ -119,15 +127,18 @@ class MainActivity : ComponentActivity() { defaultUrl: String = MainViewModel.URL, defaultToken: String = MainViewModel.TOKEN, defaultSecondToken: String = MainViewModel.TOKEN, + defaultTokenServerId: String = MainViewModel.TOKEN_SERVER_ID, defaultE2eeKey: String = MainViewModel.E2EE_KEY, defaultE2eeOn: Boolean = false, - onConnect: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean, stressTest: StressTest) -> Unit = { _, _, _, _, _ -> }, - onSave: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean) -> Unit = { _, _, _, _ -> }, + onConnect: (tokenSourceArgs: TokenSourceArgs, e2eeKey: String, e2eeOn: Boolean, stressTest: StressTest) -> Unit = { _, _, _, _ -> }, + onSave: (url: String, token: String, tokenServerId: String, e2eeKey: String, e2eeOn: Boolean) -> Unit = { _, _, _, _, _ -> }, onReset: () -> Unit = {}, ) { AppTheme { + var tokenMode by remember { mutableStateOf(TokenMode.Literal) } var url by remember { mutableStateOf(defaultUrl) } var token by remember { mutableStateOf(defaultToken) } + var tokenServerId by remember { mutableStateOf(defaultTokenServerId) } var e2eeKey by remember { mutableStateOf(defaultE2eeKey) } var e2eeOn by remember { mutableStateOf(defaultE2eeOn) } var stressTest by remember { mutableStateOf(false) } @@ -154,19 +165,49 @@ class MainActivity : ComponentActivity() { contentDescription = "", ) Spacer(modifier = Modifier.height(20.dp)) - OutlinedTextField( - value = url, - onValueChange = { url = it }, - label = { Text("URL") }, - modifier = Modifier.fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(20.dp)) - OutlinedTextField( - value = token, - onValueChange = { token = it }, - label = { Text("Token") }, - modifier = Modifier.fillMaxWidth(), - ) + TabRow( + selectedTabIndex = tokenMode.ordinal, + backgroundColor = Color.Transparent, + ) { + Tab( + selected = tokenMode == TokenMode.Literal, + onClick = { tokenMode = TokenMode.Literal }, + text = { Text("URL & Token") }, + ) + Tab( + selected = tokenMode == TokenMode.DevServer, + onClick = { tokenMode = TokenMode.DevServer }, + text = { Text("Dev Token Server") }, + ) + } + when (tokenMode) { + TokenMode.Literal -> { + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text("URL") }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = token, + onValueChange = { token = it }, + label = { Text("Token") }, + modifier = Modifier.fillMaxWidth(), + ) + } + + TokenMode.DevServer -> { + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = tokenServerId, + onValueChange = { tokenServerId = it }, + label = { Text("Token Server ID") }, + modifier = Modifier.fillMaxWidth(), + ) + } + } if (e2eeOn) { Spacer(modifier = Modifier.height(20.dp)) @@ -179,7 +220,7 @@ class MainActivity : ComponentActivity() { ) } - if (stressTest) { + if (tokenMode == TokenMode.Literal && stressTest) { Spacer(modifier = Modifier.height(20.dp)) OutlinedTextField( value = secondToken, @@ -203,34 +244,44 @@ class MainActivity : ComponentActivity() { ) } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Text("Stress test") - Switch( - checked = stressTest, - onCheckedChange = { stressTest = it }, - ) + if (tokenMode == TokenMode.Literal) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Stress test") + Switch( + checked = stressTest, + onCheckedChange = { stressTest = it }, + ) + } } Spacer(modifier = Modifier.height(40.dp)) Button( onClick = { - val stressTestCmd = if (stressTest) { - StressTest.SwitchRoom(token, secondToken) - } else { - StressTest.None + when (tokenMode) { + TokenMode.Literal -> { + val stressTestCmd = if (stressTest) { + StressTest.SwitchRoom(token, secondToken) + } else { + StressTest.None + } + onConnect(TokenSourceArgs.Literal(url, token), e2eeKey, e2eeOn, stressTestCmd) + } + + TokenMode.DevServer -> { + onConnect(TokenSourceArgs.DevTokenServer(tokenServerId), e2eeKey, e2eeOn, StressTest.None) + } } - onConnect(url, token, e2eeKey, e2eeOn, stressTestCmd) }, ) { Text("Connect") } Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { onSave(url, token, e2eeKey, e2eeOn) }) { + Button(onClick = { onSave(url, token, tokenServerId, e2eeKey, e2eeOn) }) { Text("Save Values") } @@ -240,6 +291,7 @@ class MainActivity : ComponentActivity() { onReset() url = MainViewModel.URL token = MainViewModel.TOKEN + tokenServerId = MainViewModel.TOKEN_SERVER_ID }, ) { Text("Reset Values") diff --git a/sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt b/sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt index 2cd2b85d5..98ee95708 100644 --- a/sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt +++ b/sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt @@ -40,6 +40,7 @@ import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog import io.livekit.android.sample.dialog.showDebugMenuDialog import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog import io.livekit.android.sample.model.StressTest +import io.livekit.android.sample.model.TokenSourceArgs import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -51,8 +52,7 @@ class CallActivity : AppCompatActivity() { ?: throw NullPointerException("args is null!") CallViewModel( - url = args.url, - token = args.token, + tokenSourceArgs = args.tokenSourceArgs, e2ee = args.e2eeOn, e2eeKey = args.e2eeKey, stressTest = args.stressTest, @@ -262,8 +262,7 @@ class CallActivity : AppCompatActivity() { @Parcelize data class BundleArgs( - val url: String, - val token: String, + val tokenSourceArgs: TokenSourceArgs, val e2eeKey: String, val e2eeOn: Boolean, val stressTest: StressTest, diff --git a/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt b/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt index 409d24a16..20af3388d 100644 --- a/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt +++ b/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 LiveKit, Inc. + * Copyright 2024-2026 LiveKit, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,14 @@ package io.livekit.android.sample import android.content.Intent import android.os.Bundle import android.text.SpannableStringBuilder +import android.view.View import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.tabs.TabLayout import io.livekit.android.sample.databinding.MainActivityBinding import io.livekit.android.sample.model.StressTest +import io.livekit.android.sample.model.TokenSourceArgs import io.livekit.android.sample.util.requestNeededPermissions class MainActivity : AppCompatActivity() { @@ -36,21 +39,46 @@ class MainActivity : AppCompatActivity() { val urlString = viewModel.getSavedUrl() val tokenString = viewModel.getSavedToken() + val tokenServerIdString = viewModel.getSavedTokenServerId() val e2EEOn = viewModel.getE2EEOptionsOn() val e2EEKey = viewModel.getSavedE2EEKey() binding.run { url.editText?.text = SpannableStringBuilder(urlString) token.editText?.text = SpannableStringBuilder(tokenString) + tokenServerId.editText?.text = SpannableStringBuilder(tokenServerIdString) e2eeEnabled.isChecked = e2EEOn e2eeKey.editText?.text = SpannableStringBuilder(e2EEKey) + + tokenModeTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + val literalMode = tab.position == TAB_POSITION_LITERAL + url.visibility = if (literalMode) View.VISIBLE else View.GONE + token.visibility = if (literalMode) View.VISIBLE else View.GONE + tokenServerId.visibility = if (literalMode) View.GONE else View.VISIBLE + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + + override fun onTabReselected(tab: TabLayout.Tab) {} + }) + connectButton.setOnClickListener { + val tokenSourceArgs = if (tokenModeTabs.selectedTabPosition == TAB_POSITION_LITERAL) { + TokenSourceArgs.Literal( + url = url.editText?.text.toString(), + token = token.editText?.text.toString(), + ) + } else { + TokenSourceArgs.DevTokenServer( + tokenServerId = tokenServerId.editText?.text.toString(), + ) + } val intent = Intent(this@MainActivity, CallActivity::class.java).apply { putExtra( CallActivity.KEY_ARGS, CallActivity.BundleArgs( - url = url.editText?.text.toString(), - token = token.editText?.text.toString(), + tokenSourceArgs = tokenSourceArgs, e2eeOn = e2eeEnabled.isChecked, e2eeKey = e2eeKey.editText?.text.toString(), stressTest = StressTest.None, @@ -64,6 +92,7 @@ class MainActivity : AppCompatActivity() { saveButton.setOnClickListener { viewModel.setSavedUrl(url.editText?.text?.toString() ?: "") viewModel.setSavedToken(token.editText?.text?.toString() ?: "") + viewModel.setSavedTokenServerId(tokenServerId.editText?.text?.toString() ?: "") viewModel.setSavedE2EEOn(e2eeEnabled.isChecked) viewModel.setSavedE2EEKey(e2eeKey.editText?.text?.toString() ?: "") @@ -78,6 +107,7 @@ class MainActivity : AppCompatActivity() { viewModel.reset() url.editText?.text = SpannableStringBuilder(MainViewModel.URL) token.editText?.text = SpannableStringBuilder(MainViewModel.TOKEN) + tokenServerId.editText?.text = SpannableStringBuilder(MainViewModel.TOKEN_SERVER_ID) e2eeEnabled.isChecked = false e2eeKey.editText?.text = SpannableStringBuilder("") @@ -93,4 +123,8 @@ class MainActivity : AppCompatActivity() { requestNeededPermissions() } + + companion object { + private const val TAB_POSITION_LITERAL = 0 + } } diff --git a/sample-app/src/main/res/layout/main_activity.xml b/sample-app/src/main/res/layout/main_activity.xml index 03df63682..2e7089e11 100644 --- a/sample-app/src/main/res/layout/main_activity.xml +++ b/sample-app/src/main/res/layout/main_activity.xml @@ -12,6 +12,24 @@ android:layout_marginTop="50dp" android:src="@drawable/banner_dark" /> + + + + + + + + + + + + + + E2EE Key Enable E2EE URL + Token Server ID + URL & Token + Dev Token Server Save Values Reset Values From 9d07c8a8146a73ff9343323a89e8d5dede9d8b60 Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:20:45 +0200 Subject: [PATCH 2/5] feat: add connection option fields to the development token server tab The dev token server tab in both sample apps now takes optional room name, participant name, participant identity, and agent name inputs. They travel through TokenSourceArgs.DevTokenServer and are passed to the token source fetch as TokenRequestOptions; blank fields are treated as unset so the server defaults apply. Co-Authored-By: Claude Fable 5 --- .../livekit/android/sample/CallViewModel.kt | 10 +- .../android/sample/model/TokenSourceArgs.kt | 4 + .../android/composesample/MainActivity.kt | 41 ++++++- .../io/livekit/android/sample/MainActivity.kt | 9 +- .../src/main/res/layout/main_activity.xml | 114 ++++++++++++++---- sample-app/src/main/res/values/strings.xml | 4 + 6 files changed, 154 insertions(+), 28 deletions(-) diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt index 2ae963f39..18fb84864 100644 --- a/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt +++ b/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt @@ -53,6 +53,7 @@ import io.livekit.android.rpc.RpcError import io.livekit.android.sample.model.StressTest import io.livekit.android.sample.model.TokenSourceArgs import io.livekit.android.sample.service.ForegroundService +import io.livekit.android.token.TokenRequestOptions import io.livekit.android.token.TokenSource import io.livekit.android.token.TokenSourceResponse import io.livekit.android.util.LKLog @@ -259,7 +260,14 @@ class CallViewModel( private suspend fun fetchConnectionDetails(): Result = when (tokenSourceArgs) { is TokenSourceArgs.Literal -> TokenSource.fromLiteral(tokenSourceArgs.url, tokenSourceArgs.token).fetch() - is TokenSourceArgs.DevTokenServer -> TokenSource.fromDevelopmentTokenServer(tokenSourceArgs.tokenServerId).fetch() + is TokenSourceArgs.DevTokenServer -> TokenSource.fromDevelopmentTokenServer(tokenSourceArgs.tokenServerId).fetch( + TokenRequestOptions( + roomName = tokenSourceArgs.roomName, + participantName = tokenSourceArgs.participantName, + participantIdentity = tokenSourceArgs.participantIdentity, + agentName = tokenSourceArgs.agentName, + ), + ) } private suspend fun connectToRoom() { diff --git a/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt b/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt index 2584a2a2e..afc6e14de 100644 --- a/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt +++ b/sample-app-common/src/main/java/io/livekit/android/sample/model/TokenSourceArgs.kt @@ -34,5 +34,9 @@ sealed class TokenSourceArgs : Parcelable { @Parcelize data class DevTokenServer( val tokenServerId: String, + val roomName: String? = null, + val participantName: String? = null, + val participantIdentity: String? = null, + val agentName: String? = null, ) : TokenSourceArgs() } diff --git a/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt b/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt index 58ee09e5e..194b5ba70 100644 --- a/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt +++ b/sample-app-compose/src/main/java/io/livekit/android/composesample/MainActivity.kt @@ -139,6 +139,10 @@ class MainActivity : ComponentActivity() { var url by remember { mutableStateOf(defaultUrl) } var token by remember { mutableStateOf(defaultToken) } var tokenServerId by remember { mutableStateOf(defaultTokenServerId) } + var roomName by remember { mutableStateOf("") } + var participantName by remember { mutableStateOf("") } + var participantIdentity by remember { mutableStateOf("") } + var agentName by remember { mutableStateOf("") } var e2eeKey by remember { mutableStateOf(defaultE2eeKey) } var e2eeOn by remember { mutableStateOf(defaultE2eeOn) } var stressTest by remember { mutableStateOf(false) } @@ -206,6 +210,34 @@ class MainActivity : ComponentActivity() { label = { Text("Token Server ID") }, modifier = Modifier.fillMaxWidth(), ) + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = roomName, + onValueChange = { roomName = it }, + label = { Text("Room Name (optional)") }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = participantName, + onValueChange = { participantName = it }, + label = { Text("Participant Name (optional)") }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = participantIdentity, + onValueChange = { participantIdentity = it }, + label = { Text("Participant Identity (optional)") }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = agentName, + onValueChange = { agentName = it }, + label = { Text("Agent Name (optional)") }, + modifier = Modifier.fillMaxWidth(), + ) } } @@ -272,7 +304,14 @@ class MainActivity : ComponentActivity() { } TokenMode.DevServer -> { - onConnect(TokenSourceArgs.DevTokenServer(tokenServerId), e2eeKey, e2eeOn, StressTest.None) + val devTokenServerArgs = TokenSourceArgs.DevTokenServer( + tokenServerId = tokenServerId, + roomName = roomName.ifBlank { null }, + participantName = participantName.ifBlank { null }, + participantIdentity = participantIdentity.ifBlank { null }, + agentName = agentName.ifBlank { null }, + ) + onConnect(devTokenServerArgs, e2eeKey, e2eeOn, StressTest.None) } } }, diff --git a/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt b/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt index 20af3388d..a8d619779 100644 --- a/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt +++ b/sample-app/src/main/java/io/livekit/android/sample/MainActivity.kt @@ -53,9 +53,8 @@ class MainActivity : AppCompatActivity() { tokenModeTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { val literalMode = tab.position == TAB_POSITION_LITERAL - url.visibility = if (literalMode) View.VISIBLE else View.GONE - token.visibility = if (literalMode) View.VISIBLE else View.GONE - tokenServerId.visibility = if (literalMode) View.GONE else View.VISIBLE + literalFields.visibility = if (literalMode) View.VISIBLE else View.GONE + devTokenServerFields.visibility = if (literalMode) View.GONE else View.VISIBLE } override fun onTabUnselected(tab: TabLayout.Tab) {} @@ -72,6 +71,10 @@ class MainActivity : AppCompatActivity() { } else { TokenSourceArgs.DevTokenServer( tokenServerId = tokenServerId.editText?.text.toString(), + roomName = roomName.editText?.text?.toString()?.ifBlank { null }, + participantName = participantName.editText?.text?.toString()?.ifBlank { null }, + participantIdentity = participantIdentity.editText?.text?.toString()?.ifBlank { null }, + agentName = agentName.editText?.text?.toString()?.ifBlank { null }, ) } val intent = Intent(this@MainActivity, CallActivity::class.java).apply { diff --git a/sample-app/src/main/res/layout/main_activity.xml b/sample-app/src/main/res/layout/main_activity.xml index 2e7089e11..dae88bd9c 100644 --- a/sample-app/src/main/res/layout/main_activity.xml +++ b/sample-app/src/main/res/layout/main_activity.xml @@ -30,45 +30,113 @@ - + android:orientation="vertical"> - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/url"> - + - + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/token"> - + - + + + + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/token_server_id"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enable E2EE URL Token Server ID + Room Name (optional) + Participant Name (optional) + Participant Identity (optional) + Agent Name (optional) URL & Token Dev Token Server Save Values From 2100a68bde67592ad470d2cf9394dceab016ec3d Mon Sep 17 00:00:00 2001 From: Max Heimbrock <43608204+MaxHeimbrock@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:56:33 +0200 Subject: [PATCH 3/5] Add empty changeset, make list a scrolling list for token source tab --- .changeset/sample-apps-token-source.md | 4 + .../src/main/res/layout/main_activity.xml | 243 +++++++++--------- 2 files changed, 129 insertions(+), 118 deletions(-) create mode 100644 .changeset/sample-apps-token-source.md diff --git a/.changeset/sample-apps-token-source.md b/.changeset/sample-apps-token-source.md new file mode 100644 index 000000000..9e5542f71 --- /dev/null +++ b/.changeset/sample-apps-token-source.md @@ -0,0 +1,4 @@ +--- +--- + +Use the TokenSource implementations in the sample apps, with a development token server option. Sample-app-only change; no SDK release needed. diff --git a/sample-app/src/main/res/layout/main_activity.xml b/sample-app/src/main/res/layout/main_activity.xml index dae88bd9c..416dfec20 100644 --- a/sample-app/src/main/res/layout/main_activity.xml +++ b/sample-app/src/main/res/layout/main_activity.xml @@ -1,134 +1,154 @@ - + android:fillViewport="true"> - - - + android:gravity="center_horizontal" + android:orientation="vertical" + android:padding="10dp"> - + android:layout_marginTop="50dp" + android:src="@drawable/banner_dark" /> - + android:layout_marginTop="20dp"> - + - + - + + + android:orientation="vertical"> - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/url"> - + - + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/token"> - + - + - + - + android:orientation="vertical" + android:visibility="gone"> - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/token_server_id"> - + - + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/room_name"> - + - + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/participant_name"> - + - + - + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:hint="@string/participant_identity"> - + + + + + + + + + + + + android:hint="@string/e2ee_key_str"> - - - - - - - + android:layout_height="wrap_content" + android:text="@string/e2ee_enabled_str" /> - +