Committed by
GitHub
Fix network request leak on pre-8.1 devices (#389)
* Fix network request leak * spotless
正在显示
10 个修改的文件
包含
314 行增加
和
97 行删除
| @@ -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 |
livekit-android-sdk/src/main/java/io/livekit/android/room/network/NetworkCallbackManager.kt
0 → 100644
| 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 | +} |
| @@ -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 | } |
-
请 注册 或 登录 后发表评论