davidliu
Committed by GitHub

Fix network request leak on pre-8.1 devices (#389)

* Fix network request leak

* spotless
@@ -20,9 +20,8 @@ package io.livekit.android.room @@ -20,9 +20,8 @@ package io.livekit.android.room
20 20
21 import android.content.Context 21 import android.content.Context
22 import android.net.ConnectivityManager 22 import android.net.ConnectivityManager
  23 +import android.net.ConnectivityManager.NetworkCallback
23 import android.net.Network 24 import android.net.Network
24 -import android.net.NetworkCapabilities  
25 -import android.net.NetworkRequest  
26 import androidx.annotation.VisibleForTesting 25 import androidx.annotation.VisibleForTesting
27 import dagger.assisted.Assisted 26 import dagger.assisted.Assisted
28 import dagger.assisted.AssistedFactory 27 import dagger.assisted.AssistedFactory
@@ -40,6 +39,7 @@ import io.livekit.android.e2ee.E2EEOptions @@ -40,6 +39,7 @@ import io.livekit.android.e2ee.E2EEOptions
40 import io.livekit.android.events.* 39 import io.livekit.android.events.*
41 import io.livekit.android.memory.CloseableManager 40 import io.livekit.android.memory.CloseableManager
42 import io.livekit.android.renderer.TextureViewRenderer 41 import io.livekit.android.renderer.TextureViewRenderer
  42 +import io.livekit.android.room.network.NetworkCallbackManager
43 import io.livekit.android.room.participant.* 43 import io.livekit.android.room.participant.*
44 import io.livekit.android.room.track.* 44 import io.livekit.android.room.track.*
45 import io.livekit.android.util.FlowObservable 45 import io.livekit.android.util.FlowObservable
@@ -371,11 +371,7 @@ constructor( @@ -371,11 +371,7 @@ constructor(
371 ioDispatcher + emptyCoroutineExceptionHandler, 371 ioDispatcher + emptyCoroutineExceptionHandler,
372 ) { 372 ) {
373 engine.join(url, token, options, roomOptions) 373 engine.join(url, token, options, roomOptions)
374 - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
375 - val networkRequest = NetworkRequest.Builder()  
376 - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)  
377 - .build()  
378 - cm.registerNetworkCallback(networkRequest, networkCallback) 374 + networkCallbackManager.registerCallback()
379 375
380 ensureActive() 376 ensureActive()
381 if (options.audio) { 377 if (options.audio) {
@@ -658,12 +654,7 @@ constructor( @@ -658,12 +654,7 @@ constructor(
658 if (state == State.DISCONNECTED) { 654 if (state == State.DISCONNECTED) {
659 return@runBlocking 655 return@runBlocking
660 } 656 }
661 - try {  
662 - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
663 - cm.unregisterNetworkCallback(networkCallback)  
664 - } catch (e: IllegalArgumentException) {  
665 - // do nothing, may happen on older versions if attempting to unregister twice.  
666 - } 657 + networkCallbackManager.unregisterCallback()
667 658
668 state = State.DISCONNECTED 659 state = State.DISCONNECTED
669 cleanupRoom() 660 cleanupRoom()
@@ -763,28 +754,25 @@ constructor( @@ -763,28 +754,25 @@ constructor(
763 } 754 }
764 755
765 // ------------------------------------- NetworkCallback -------------------------------------// 756 // ------------------------------------- NetworkCallback -------------------------------------//
766 - private val networkCallback = object : ConnectivityManager.NetworkCallback() {  
767 - /**  
768 - * @suppress  
769 - */  
770 - override fun onLost(network: Network) {  
771 - // lost connection, flip to reconnecting  
772 - hasLostConnectivity = true  
773 - } 757 + private val networkCallbackManager = NetworkCallbackManager(
  758 + object : NetworkCallback() {
  759 + override fun onLost(network: Network) {
  760 + // lost connection, flip to reconnecting
  761 + hasLostConnectivity = true
  762 + }
774 763
775 - /**  
776 - * @suppress  
777 - */  
778 - override fun onAvailable(network: Network) {  
779 - // only actually reconnect after connection is re-established  
780 - if (!hasLostConnectivity) {  
781 - return 764 + override fun onAvailable(network: Network) {
  765 + // only actually reconnect after connection is re-established
  766 + if (!hasLostConnectivity) {
  767 + return
  768 + }
  769 + LKLog.i { "network connection available, reconnecting" }
  770 + reconnect()
  771 + hasLostConnectivity = false
782 } 772 }
783 - LKLog.i { "network connection available, reconnecting" }  
784 - reconnect()  
785 - hasLostConnectivity = false  
786 - }  
787 - } 773 + },
  774 + connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager,
  775 + ).apply { closeableManager.registerClosable(this) }
788 776
789 // ----------------------------------- RTCEngine.Listener ------------------------------------// 777 // ----------------------------------- RTCEngine.Listener ------------------------------------//
790 778
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.room.network
  18 +
  19 +import android.net.ConnectivityManager
  20 +import android.net.NetworkCapabilities
  21 +import android.net.NetworkRequest
  22 +import io.livekit.android.util.LKLog
  23 +import java.io.Closeable
  24 +import java.util.concurrent.atomic.AtomicBoolean
  25 +
  26 +/**
  27 + * Manages a [ConnectivityManager.NetworkCallback] so that it is never
  28 + * registered multiple times. A NetworkCallback is allowed to be registered
  29 + * multiple times by the ConnectivityService, but the underlying network
  30 + * requests will leak on 8.0 and earlier.
  31 + *
  32 + * There's a 100 request hard limit, so leaks here are particularly dangerous.
  33 + */
  34 +class NetworkCallbackManager(
  35 + private val networkCallback: ConnectivityManager.NetworkCallback,
  36 + private val connectivityManager: ConnectivityManager,
  37 +) : Closeable {
  38 + private val isRegistered = AtomicBoolean(false)
  39 + private val isClosed = AtomicBoolean(false)
  40 +
  41 + @Synchronized
  42 + fun registerCallback() {
  43 + if (!isClosed.get() && isRegistered.compareAndSet(false, true)) {
  44 + try {
  45 + val networkRequest = NetworkRequest.Builder()
  46 + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
  47 + .build()
  48 + connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
  49 + } catch (e: Exception) {
  50 + LKLog.w(e) { "Exception when trying to register network callback, reconnection may be impaired." }
  51 + }
  52 + }
  53 + }
  54 +
  55 + @Synchronized
  56 + fun unregisterCallback() {
  57 + if (!isClosed.get() && isRegistered.compareAndSet(true, false)) {
  58 + try {
  59 + connectivityManager.unregisterNetworkCallback(networkCallback)
  60 + } catch (e: IllegalArgumentException) {
  61 + // do nothing, may happen on older versions if attempting to unregister twice.
  62 + // This shouldn't happen though, so log it just in case.
  63 + LKLog.w { "NetworkCallback was unregistered multiple times?" }
  64 + }
  65 + }
  66 + }
  67 +
  68 + @Synchronized
  69 + override fun close() {
  70 + if (isClosed.get()) {
  71 + return
  72 + }
  73 +
  74 + if (isRegistered.get()) {
  75 + unregisterCallback()
  76 + }
  77 +
  78 + isClosed.set(true)
  79 + }
  80 +}
1 plugins { 1 plugins {
2 id 'com.android.library' 2 id 'com.android.library'
3 id 'kotlin-android' 3 id 'kotlin-android'
  4 + id 'kotlin-parcelize'
4 } 5 }
5 6
6 def getDefaultUrl() { 7 def getDefaultUrl() {
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.sample 17 package io.livekit.android.sample
18 18
  19 +import android.annotation.SuppressLint
19 import android.app.Application 20 import android.app.Application
20 import android.content.Intent 21 import android.content.Intent
21 import android.media.projection.MediaProjectionManager 22 import android.media.projection.MediaProjectionManager
@@ -42,8 +43,11 @@ import io.livekit.android.room.track.CameraPosition @@ -42,8 +43,11 @@ import io.livekit.android.room.track.CameraPosition
42 import io.livekit.android.room.track.LocalScreencastVideoTrack 43 import io.livekit.android.room.track.LocalScreencastVideoTrack
43 import io.livekit.android.room.track.LocalVideoTrack 44 import io.livekit.android.room.track.LocalVideoTrack
44 import io.livekit.android.room.track.Track 45 import io.livekit.android.room.track.Track
  46 +import io.livekit.android.sample.model.StressTest
45 import io.livekit.android.sample.service.ForegroundService 47 import io.livekit.android.sample.service.ForegroundService
  48 +import io.livekit.android.util.LKLog
46 import io.livekit.android.util.flow 49 import io.livekit.android.util.flow
  50 +import kotlinx.coroutines.coroutineScope
47 import kotlinx.coroutines.delay 51 import kotlinx.coroutines.delay
48 import kotlinx.coroutines.flow.Flow 52 import kotlinx.coroutines.flow.Flow
49 import kotlinx.coroutines.flow.MutableSharedFlow 53 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -51,6 +55,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @@ -51,6 +55,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
51 import kotlinx.coroutines.flow.StateFlow 55 import kotlinx.coroutines.flow.StateFlow
52 import kotlinx.coroutines.flow.combine 56 import kotlinx.coroutines.flow.combine
53 import kotlinx.coroutines.flow.map 57 import kotlinx.coroutines.flow.map
  58 +import kotlinx.coroutines.isActive
54 import kotlinx.coroutines.launch 59 import kotlinx.coroutines.launch
55 60
56 class CallViewModel( 61 class CallViewModel(
@@ -60,6 +65,7 @@ class CallViewModel( @@ -60,6 +65,7 @@ class CallViewModel(
60 val e2ee: Boolean = false, 65 val e2ee: Boolean = false,
61 val e2eeKey: String? = "", 66 val e2eeKey: String? = "",
62 val audioProcessorOptions: AudioProcessorOptions? = null, 67 val audioProcessorOptions: AudioProcessorOptions? = null,
  68 + val stressTest: StressTest = StressTest.None,
63 ) : AndroidViewModel(application) { 69 ) : AndroidViewModel(application) {
64 70
65 private fun getE2EEOptions(): E2EEOptions? { 71 private fun getE2EEOptions(): E2EEOptions? {
@@ -169,7 +175,10 @@ class CallViewModel( @@ -169,7 +175,10 @@ class CallViewModel(
169 } 175 }
170 } 176 }
171 177
172 - connectToRoom() 178 + when (stressTest) {
  179 + is StressTest.SwitchRoom -> launch { stressTest.execute() }
  180 + is StressTest.None -> connectToRoom()
  181 + }
173 } 182 }
174 183
175 // Start a foreground service to keep the call from being interrupted if the 184 // Start a foreground service to keep the call from being interrupted if the
@@ -383,6 +392,53 @@ class CallViewModel( @@ -383,6 +392,53 @@ class CallViewModel(
383 connectToRoom() 392 connectToRoom()
384 } 393 }
385 } 394 }
  395 +
  396 + private suspend fun StressTest.SwitchRoom.execute() = coroutineScope {
  397 + launch {
  398 + while (isActive) {
  399 + delay(2000)
  400 + dumpReferenceTables()
  401 + }
  402 + }
  403 +
  404 + while (isActive) {
  405 + Timber.d { "Stress test -> connect to first room" }
  406 + launch { quickConnectToRoom(firstToken) }
  407 + delay(200)
  408 + room.disconnect()
  409 + delay(50)
  410 + Timber.d { "Stress test -> connect to second room" }
  411 + launch { quickConnectToRoom(secondToken) }
  412 + delay(200)
  413 + room.disconnect()
  414 + delay(50)
  415 + }
  416 + }
  417 +
  418 + private suspend fun quickConnectToRoom(token: String) {
  419 + try {
  420 + room.connect(
  421 + url = url,
  422 + token = token,
  423 + )
  424 + } catch (e: Throwable) {
  425 + Timber.e(e) { "Failed to connect to room" }
  426 + }
  427 + }
  428 +
  429 + @SuppressLint("DiscouragedPrivateApi")
  430 + private fun dumpReferenceTables() {
  431 + try {
  432 + val cls = Class.forName("android.os.Debug")
  433 + val method = cls.getDeclaredMethod("dumpReferenceTables")
  434 + val con = cls.getDeclaredConstructor().apply {
  435 + isAccessible = true
  436 + }
  437 + method.invoke(con.newInstance())
  438 + } catch (e: Exception) {
  439 + LKLog.e(e) { "Unable to dump reference tables, you can try `adb shell settings put global hidden_api_policy 1`" }
  440 + }
  441 + }
386 } 442 }
387 443
388 private fun <T> LiveData<T>.hide(): LiveData<T> = this 444 private fun <T> LiveData<T>.hide(): LiveData<T> = this
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.sample.model
  18 +
  19 +import android.os.Parcelable
  20 +import kotlinx.parcelize.Parcelize
  21 +
  22 +sealed class StressTest : Parcelable {
  23 +
  24 + @Parcelize
  25 + data class SwitchRoom(
  26 + val firstToken: String,
  27 + val secondToken: String,
  28 + ) : StressTest()
  29 +
  30 + @Parcelize
  31 + object None : StressTest()
  32 +}
@@ -48,6 +48,7 @@ import io.livekit.android.room.Room @@ -48,6 +48,7 @@ import io.livekit.android.room.Room
48 import io.livekit.android.room.participant.Participant 48 import io.livekit.android.room.participant.Participant
49 import io.livekit.android.sample.CallViewModel 49 import io.livekit.android.sample.CallViewModel
50 import io.livekit.android.sample.common.R 50 import io.livekit.android.sample.common.R
  51 +import io.livekit.android.sample.model.StressTest
51 import kotlinx.coroutines.Dispatchers 52 import kotlinx.coroutines.Dispatchers
52 import kotlinx.coroutines.launch 53 import kotlinx.coroutines.launch
53 import kotlinx.parcelize.Parcelize 54 import kotlinx.parcelize.Parcelize
@@ -62,6 +63,7 @@ class CallActivity : AppCompatActivity() { @@ -62,6 +63,7 @@ class CallActivity : AppCompatActivity() {
62 token = args.token, 63 token = args.token,
63 e2ee = args.e2eeOn, 64 e2ee = args.e2eeOn,
64 e2eeKey = args.e2eeKey, 65 e2eeKey = args.e2eeKey,
  66 + stressTest = args.stressTest,
65 application = application, 67 application = application,
66 ) 68 )
67 } 69 }
@@ -478,5 +480,6 @@ class CallActivity : AppCompatActivity() { @@ -478,5 +480,6 @@ class CallActivity : AppCompatActivity() {
478 val token: String, 480 val token: String,
479 val e2eeKey: String, 481 val e2eeKey: String,
480 val e2eeOn: Boolean, 482 val e2eeOn: Boolean,
  483 + val stressTest: StressTest,
481 ) : Parcelable 484 ) : Parcelable
482 } 485 }
1 /* 1 /*
2 - * Copyright 2023 LiveKit, Inc. 2 + * Copyright 2023-2024 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -17,6 +17,8 @@ @@ -17,6 +17,8 @@
17 package io.livekit.android.composesample 17 package io.livekit.android.composesample
18 18
19 import android.content.Intent 19 import android.content.Intent
  20 +import android.net.ConnectivityManager
  21 +import android.net.Network
20 import android.os.Bundle 22 import android.os.Bundle
21 import android.widget.Toast 23 import android.widget.Toast
22 import androidx.activity.ComponentActivity 24 import androidx.activity.ComponentActivity
@@ -28,7 +30,6 @@ import androidx.compose.foundation.layout.Box @@ -28,7 +30,6 @@ import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Column 30 import androidx.compose.foundation.layout.Column
29 import androidx.compose.foundation.layout.Row 31 import androidx.compose.foundation.layout.Row
30 import androidx.compose.foundation.layout.Spacer 32 import androidx.compose.foundation.layout.Spacer
31 -import androidx.compose.foundation.layout.defaultMinSize  
32 import androidx.compose.foundation.layout.fillMaxSize 33 import androidx.compose.foundation.layout.fillMaxSize
33 import androidx.compose.foundation.layout.fillMaxWidth 34 import androidx.compose.foundation.layout.fillMaxWidth
34 import androidx.compose.foundation.layout.height 35 import androidx.compose.foundation.layout.height
@@ -55,11 +56,28 @@ import com.google.accompanist.pager.ExperimentalPagerApi @@ -55,11 +56,28 @@ import com.google.accompanist.pager.ExperimentalPagerApi
55 import io.livekit.android.composesample.ui.theme.AppTheme 56 import io.livekit.android.composesample.ui.theme.AppTheme
56 import io.livekit.android.sample.MainViewModel 57 import io.livekit.android.sample.MainViewModel
57 import io.livekit.android.sample.common.R 58 import io.livekit.android.sample.common.R
  59 +import io.livekit.android.sample.model.StressTest
58 import io.livekit.android.sample.util.requestNeededPermissions 60 import io.livekit.android.sample.util.requestNeededPermissions
  61 +import io.livekit.android.util.LKLog
59 62
60 @ExperimentalPagerApi 63 @ExperimentalPagerApi
61 class MainActivity : ComponentActivity() { 64 class MainActivity : ComponentActivity() {
62 65
  66 + private val networkCallback = object : ConnectivityManager.NetworkCallback() {
  67 + /**
  68 + * @suppress
  69 + */
  70 + override fun onLost(network: Network) {
  71 + LKLog.i { "network connection lost" }
  72 + }
  73 +
  74 + /**
  75 + * @suppress
  76 + */
  77 + override fun onAvailable(network: Network) {
  78 + LKLog.i { "network connection available, reconnecting" }
  79 + }
  80 + }
63 val viewModel by viewModels<MainViewModel>() 81 val viewModel by viewModels<MainViewModel>()
64 override fun onCreate(savedInstanceState: Bundle?) { 82 override fun onCreate(savedInstanceState: Bundle?) {
65 super.onCreate(savedInstanceState) 83 super.onCreate(savedInstanceState)
@@ -71,7 +89,7 @@ class MainActivity : ComponentActivity() { @@ -71,7 +89,7 @@ class MainActivity : ComponentActivity() {
71 defaultToken = viewModel.getSavedToken(), 89 defaultToken = viewModel.getSavedToken(),
72 defaultE2eeKey = viewModel.getSavedE2EEKey(), 90 defaultE2eeKey = viewModel.getSavedE2EEKey(),
73 defaultE2eeOn = viewModel.getE2EEOptionsOn(), 91 defaultE2eeOn = viewModel.getE2EEOptionsOn(),
74 - onConnect = { url, token, e2eeKey, e2eeOn -> 92 + onConnect = { url, token, e2eeKey, e2eeOn, stressTest ->
75 val intent = Intent(this@MainActivity, CallActivity::class.java).apply { 93 val intent = Intent(this@MainActivity, CallActivity::class.java).apply {
76 putExtra( 94 putExtra(
77 CallActivity.KEY_ARGS, 95 CallActivity.KEY_ARGS,
@@ -80,7 +98,8 @@ class MainActivity : ComponentActivity() { @@ -80,7 +98,8 @@ class MainActivity : ComponentActivity() {
80 token, 98 token,
81 e2eeKey, 99 e2eeKey,
82 e2eeOn, 100 e2eeOn,
83 - ) 101 + stressTest,
  102 + ),
84 ) 103 )
85 } 104 }
86 startActivity(intent) 105 startActivity(intent)
@@ -94,7 +113,7 @@ class MainActivity : ComponentActivity() { @@ -94,7 +113,7 @@ class MainActivity : ComponentActivity() {
94 Toast.makeText( 113 Toast.makeText(
95 this@MainActivity, 114 this@MainActivity,
96 "Values saved.", 115 "Values saved.",
97 - Toast.LENGTH_SHORT 116 + Toast.LENGTH_SHORT,
98 ).show() 117 ).show()
99 }, 118 },
100 onReset = { 119 onReset = {
@@ -102,9 +121,9 @@ class MainActivity : ComponentActivity() { @@ -102,9 +121,9 @@ class MainActivity : ComponentActivity() {
102 Toast.makeText( 121 Toast.makeText(
103 this@MainActivity, 122 this@MainActivity,
104 "Values reset.", 123 "Values reset.",
105 - Toast.LENGTH_SHORT 124 + Toast.LENGTH_SHORT,
106 ).show() 125 ).show()
107 - } 126 + },
108 ) 127 )
109 } 128 }
110 } 129 }
@@ -117,9 +136,10 @@ class MainActivity : ComponentActivity() { @@ -117,9 +136,10 @@ class MainActivity : ComponentActivity() {
117 fun MainContent( 136 fun MainContent(
118 defaultUrl: String = MainViewModel.URL, 137 defaultUrl: String = MainViewModel.URL,
119 defaultToken: String = MainViewModel.TOKEN, 138 defaultToken: String = MainViewModel.TOKEN,
  139 + defaultSecondToken: String = MainViewModel.TOKEN,
120 defaultE2eeKey: String = MainViewModel.E2EE_KEY, 140 defaultE2eeKey: String = MainViewModel.E2EE_KEY,
121 defaultE2eeOn: Boolean = false, 141 defaultE2eeOn: Boolean = false,
122 - onConnect: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean) -> Unit = { _, _, _, _ -> }, 142 + onConnect: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean, stressTest: StressTest) -> Unit = { _, _, _, _, _ -> },
123 onSave: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean) -> Unit = { _, _, _, _ -> }, 143 onSave: (url: String, token: String, e2eeKey: String, e2eeOn: Boolean) -> Unit = { _, _, _, _ -> },
124 onReset: () -> Unit = {}, 144 onReset: () -> Unit = {},
125 ) { 145 ) {
@@ -128,21 +148,23 @@ class MainActivity : ComponentActivity() { @@ -128,21 +148,23 @@ class MainActivity : ComponentActivity() {
128 var token by remember { mutableStateOf(defaultToken) } 148 var token by remember { mutableStateOf(defaultToken) }
129 var e2eeKey by remember { mutableStateOf(defaultE2eeKey) } 149 var e2eeKey by remember { mutableStateOf(defaultE2eeKey) }
130 var e2eeOn by remember { mutableStateOf(defaultE2eeOn) } 150 var e2eeOn by remember { mutableStateOf(defaultE2eeOn) }
  151 + var stressTest by remember { mutableStateOf(false) }
  152 + var secondToken by remember { mutableStateOf(defaultSecondToken) }
131 val scrollState = rememberScrollState() 153 val scrollState = rememberScrollState()
132 // A surface container using the 'background' color from the theme 154 // A surface container using the 'background' color from the theme
133 Surface( 155 Surface(
134 color = MaterialTheme.colors.background, 156 color = MaterialTheme.colors.background,
135 modifier = Modifier 157 modifier = Modifier
136 - .fillMaxSize() 158 + .fillMaxSize(),
137 ) { 159 ) {
138 Box( 160 Box(
139 modifier = Modifier 161 modifier = Modifier
140 - .verticalScroll(scrollState) 162 + .verticalScroll(scrollState),
141 ) { 163 ) {
142 Column( 164 Column(
143 horizontalAlignment = Alignment.CenterHorizontally, 165 horizontalAlignment = Alignment.CenterHorizontally,
144 modifier = Modifier 166 modifier = Modifier
145 - .padding(10.dp) 167 + .padding(10.dp),
146 ) { 168 ) {
147 Spacer(modifier = Modifier.height(50.dp)) 169 Spacer(modifier = Modifier.height(50.dp))
148 Image( 170 Image(
@@ -171,7 +193,18 @@ class MainActivity : ComponentActivity() { @@ -171,7 +193,18 @@ class MainActivity : ComponentActivity() {
171 onValueChange = { e2eeKey = it }, 193 onValueChange = { e2eeKey = it },
172 label = { Text("E2EE Key") }, 194 label = { Text("E2EE Key") },
173 modifier = Modifier.fillMaxWidth(), 195 modifier = Modifier.fillMaxWidth(),
174 - enabled = e2eeOn 196 + enabled = e2eeOn,
  197 + )
  198 + }
  199 +
  200 + if (stressTest) {
  201 + Spacer(modifier = Modifier.height(20.dp))
  202 + OutlinedTextField(
  203 + value = secondToken,
  204 + onValueChange = { secondToken = it },
  205 + label = { Text("Second token") },
  206 + modifier = Modifier.fillMaxWidth(),
  207 + enabled = stressTest,
175 ) 208 )
176 } 209 }
177 210
@@ -179,18 +212,38 @@ class MainActivity : ComponentActivity() { @@ -179,18 +212,38 @@ class MainActivity : ComponentActivity() {
179 Row( 212 Row(
180 horizontalArrangement = Arrangement.SpaceBetween, 213 horizontalArrangement = Arrangement.SpaceBetween,
181 verticalAlignment = Alignment.CenterVertically, 214 verticalAlignment = Alignment.CenterVertically,
182 - modifier = Modifier.fillMaxWidth() 215 + modifier = Modifier.fillMaxWidth(),
183 ) { 216 ) {
184 Text("Enable E2EE") 217 Text("Enable E2EE")
185 Switch( 218 Switch(
186 checked = e2eeOn, 219 checked = e2eeOn,
187 onCheckedChange = { e2eeOn = it }, 220 onCheckedChange = { e2eeOn = it },
188 - modifier = Modifier.defaultMinSize(minHeight = 100.dp)  
189 ) 221 )
190 } 222 }
191 223
192 - Spacer(modifier = Modifier.height(20.dp))  
193 - Button(onClick = { onConnect(url, token, e2eeKey, e2eeOn) }) { 224 + Row(
  225 + horizontalArrangement = Arrangement.SpaceBetween,
  226 + verticalAlignment = Alignment.CenterVertically,
  227 + modifier = Modifier.fillMaxWidth(),
  228 + ) {
  229 + Text("Stress test")
  230 + Switch(
  231 + checked = stressTest,
  232 + onCheckedChange = { stressTest = it },
  233 + )
  234 + }
  235 +
  236 + Spacer(modifier = Modifier.height(40.dp))
  237 + Button(
  238 + onClick = {
  239 + val stressTestCmd = if (stressTest) {
  240 + StressTest.SwitchRoom(token, secondToken)
  241 + } else {
  242 + StressTest.None
  243 + }
  244 + onConnect(url, token, e2eeKey, e2eeOn, stressTestCmd)
  245 + },
  246 + ) {
194 Text("Connect") 247 Text("Connect")
195 } 248 }
196 249
@@ -200,11 +253,13 @@ class MainActivity : ComponentActivity() { @@ -200,11 +253,13 @@ class MainActivity : ComponentActivity() {
200 } 253 }
201 254
202 Spacer(modifier = Modifier.height(20.dp)) 255 Spacer(modifier = Modifier.height(20.dp))
203 - Button(onClick = {  
204 - onReset()  
205 - url = MainViewModel.URL  
206 - token = MainViewModel.TOKEN  
207 - }) { 256 + Button(
  257 + onClick = {
  258 + onReset()
  259 + url = MainViewModel.URL
  260 + token = MainViewModel.TOKEN
  261 + },
  262 + ) {
208 Text("Reset Values") 263 Text("Reset Values")
209 } 264 }
210 } 265 }
1 /* 1 /*
2 - * Copyright 2023 LiveKit, Inc. 2 + * Copyright 2023-2024 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -25,6 +25,6 @@ class SampleApplication : Application() { @@ -25,6 +25,6 @@ class SampleApplication : Application() {
25 override fun onCreate() { 25 override fun onCreate() {
26 super.onCreate() 26 super.onCreate()
27 LiveKit.loggingLevel = LoggingLevel.VERBOSE 27 LiveKit.loggingLevel = LoggingLevel.VERBOSE
28 - LiveKit.enableWebRTCLogging = true 28 + // LiveKit.enableWebRTCLogging = true
29 } 29 }
30 } 30 }
@@ -28,18 +28,15 @@ import androidx.appcompat.app.AlertDialog @@ -28,18 +28,15 @@ import androidx.appcompat.app.AlertDialog
28 import androidx.appcompat.app.AppCompatActivity 28 import androidx.appcompat.app.AppCompatActivity
29 import androidx.lifecycle.lifecycleScope 29 import androidx.lifecycle.lifecycleScope
30 import androidx.recyclerview.widget.LinearLayoutManager 30 import androidx.recyclerview.widget.LinearLayoutManager
31 -import com.github.ajalt.timberkt.Timber  
32 import com.xwray.groupie.GroupieAdapter 31 import com.xwray.groupie.GroupieAdapter
33 -import io.livekit.android.audio.AudioProcessorInterface  
34 -import io.livekit.android.audio.AudioProcessorOptions  
35 import io.livekit.android.sample.common.R 32 import io.livekit.android.sample.common.R
36 import io.livekit.android.sample.databinding.CallActivityBinding 33 import io.livekit.android.sample.databinding.CallActivityBinding
37 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog 34 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog
38 import io.livekit.android.sample.dialog.showDebugMenuDialog 35 import io.livekit.android.sample.dialog.showDebugMenuDialog
39 import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog 36 import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
  37 +import io.livekit.android.sample.model.StressTest
40 import kotlinx.coroutines.flow.collectLatest 38 import kotlinx.coroutines.flow.collectLatest
41 import kotlinx.parcelize.Parcelize 39 import kotlinx.parcelize.Parcelize
42 -import java.nio.ByteBuffer  
43 40
44 class CallActivity : AppCompatActivity() { 41 class CallActivity : AppCompatActivity() {
45 42
@@ -47,34 +44,14 @@ class CallActivity : AppCompatActivity() { @@ -47,34 +44,14 @@ class CallActivity : AppCompatActivity() {
47 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) 44 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
48 ?: throw NullPointerException("args is null!") 45 ?: throw NullPointerException("args is null!")
49 46
50 - val audioProcessor = object : AudioProcessorInterface {  
51 - override fun isEnabled(): Boolean {  
52 - Timber.d { "${getName()} isEnabled" }  
53 - return true  
54 - }  
55 -  
56 - override fun getName(): String {  
57 - return "fake_noise_cancellation"  
58 - }  
59 -  
60 - override fun initializeAudioProcessing(sampleRateHz: Int, numChannels: Int) {  
61 - Timber.d { "${getName()} initialize" }  
62 - }  
63 -  
64 - override fun resetAudioProcessing(newRate: Int) {  
65 - Timber.d { "${getName()} reset" }  
66 - }  
67 -  
68 - override fun processAudio(numBands: Int, numFrames: Int, buffer: ByteBuffer) {  
69 - Timber.d { "${getName()} process" }  
70 - }  
71 - }  
72 -  
73 - val audioProcessorOptions = AudioProcessorOptions(  
74 - capturePostProcessor = audioProcessor, 47 + CallViewModel(
  48 + url = args.url,
  49 + token = args.token,
  50 + e2ee = args.e2eeOn,
  51 + e2eeKey = args.e2eeKey,
  52 + stressTest = args.stressTest,
  53 + application = application,
75 ) 54 )
76 -  
77 - CallViewModel(args.url, args.token, application, args.e2ee, args.e2eeKey, audioProcessorOptions)  
78 } 55 }
79 private lateinit var binding: CallActivityBinding 56 private lateinit var binding: CallActivityBinding
80 private val screenCaptureIntentLauncher = 57 private val screenCaptureIntentLauncher =
@@ -247,5 +224,11 @@ class CallActivity : AppCompatActivity() { @@ -247,5 +224,11 @@ class CallActivity : AppCompatActivity() {
247 } 224 }
248 225
249 @Parcelize 226 @Parcelize
250 - data class BundleArgs(val url: String, val token: String, val e2ee: Boolean, val e2eeKey: String) : Parcelable 227 + data class BundleArgs(
  228 + val url: String,
  229 + val token: String,
  230 + val e2eeKey: String,
  231 + val e2eeOn: Boolean,
  232 + val stressTest: StressTest,
  233 + ) : Parcelable
251 } 234 }
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
1 package io.livekit.android.sample 17 package io.livekit.android.sample
2 18
3 import android.content.Intent 19 import android.content.Intent
@@ -7,11 +23,12 @@ import android.widget.Toast @@ -7,11 +23,12 @@ import android.widget.Toast
7 import androidx.activity.viewModels 23 import androidx.activity.viewModels
8 import androidx.appcompat.app.AppCompatActivity 24 import androidx.appcompat.app.AppCompatActivity
9 import io.livekit.android.sample.databinding.MainActivityBinding 25 import io.livekit.android.sample.databinding.MainActivityBinding
  26 +import io.livekit.android.sample.model.StressTest
10 import io.livekit.android.sample.util.requestNeededPermissions 27 import io.livekit.android.sample.util.requestNeededPermissions
11 28
12 class MainActivity : AppCompatActivity() { 29 class MainActivity : AppCompatActivity() {
13 30
14 - val viewModel by viewModels<MainViewModel>() 31 + private val viewModel by viewModels<MainViewModel>()
15 override fun onCreate(savedInstanceState: Bundle?) { 32 override fun onCreate(savedInstanceState: Bundle?) {
16 super.onCreate(savedInstanceState) 33 super.onCreate(savedInstanceState)
17 34
@@ -21,6 +38,7 @@ class MainActivity : AppCompatActivity() { @@ -21,6 +38,7 @@ class MainActivity : AppCompatActivity() {
21 val tokenString = viewModel.getSavedToken() 38 val tokenString = viewModel.getSavedToken()
22 val e2EEOn = viewModel.getE2EEOptionsOn() 39 val e2EEOn = viewModel.getE2EEOptionsOn()
23 val e2EEKey = viewModel.getSavedE2EEKey() 40 val e2EEKey = viewModel.getSavedE2EEKey()
  41 +
24 binding.run { 42 binding.run {
25 url.editText?.text = SpannableStringBuilder(urlString) 43 url.editText?.text = SpannableStringBuilder(urlString)
26 token.editText?.text = SpannableStringBuilder(tokenString) 44 token.editText?.text = SpannableStringBuilder(tokenString)
@@ -31,11 +49,12 @@ class MainActivity : AppCompatActivity() { @@ -31,11 +49,12 @@ class MainActivity : AppCompatActivity() {
31 putExtra( 49 putExtra(
32 CallActivity.KEY_ARGS, 50 CallActivity.KEY_ARGS,
33 CallActivity.BundleArgs( 51 CallActivity.BundleArgs(
34 - url.editText?.text.toString(),  
35 - token.editText?.text.toString(),  
36 - e2eeEnabled.isChecked,  
37 - e2eeKey.editText?.text.toString()  
38 - ) 52 + url = url.editText?.text.toString(),
  53 + token = token.editText?.text.toString(),
  54 + e2eeOn = e2eeEnabled.isChecked,
  55 + e2eeKey = e2eeKey.editText?.text.toString(),
  56 + stressTest = StressTest.None,
  57 + ),
39 ) 58 )
40 } 59 }
41 60
@@ -51,7 +70,7 @@ class MainActivity : AppCompatActivity() { @@ -51,7 +70,7 @@ class MainActivity : AppCompatActivity() {
51 Toast.makeText( 70 Toast.makeText(
52 this@MainActivity, 71 this@MainActivity,
53 "Values saved.", 72 "Values saved.",
54 - Toast.LENGTH_SHORT 73 + Toast.LENGTH_SHORT,
55 ).show() 74 ).show()
56 } 75 }
57 76
@@ -65,7 +84,7 @@ class MainActivity : AppCompatActivity() { @@ -65,7 +84,7 @@ class MainActivity : AppCompatActivity() {
65 Toast.makeText( 84 Toast.makeText(
66 this@MainActivity, 85 this@MainActivity,
67 "Values reset.", 86 "Values reset.",
68 - Toast.LENGTH_SHORT 87 + Toast.LENGTH_SHORT,
69 ).show() 88 ).show()
70 } 89 }
71 } 90 }