davidliu
Committed by GitHub

Update documentation and add a basic quickstart sample app. (#124)

* update readme and docs

* clean up interface methods leaking into docs

* AudioSwitchHandler documentation

* Update package descriptions

* Include link to API reference in README

* update dependencies

* Add in basic one participant sample app

* clean up debug keys

* update readme sample

* Update README and add readmes for each project folder

* Assemble all modules for compile test safety

* Disconnect on destroy

* update compile/target version

* fix build errors

* fix tests
正在显示 53 个修改的文件 包含 792 行增加107 行删除
@@ -36,7 +36,7 @@ jobs: @@ -36,7 +36,7 @@ jobs:
36 run: chmod +x gradlew 36 run: chmod +x gradlew
37 37
38 - name: Build with Gradle 38 - name: Build with Gradle
39 - run: ./gradlew clean livekit-android-sdk:assembleRelease livekit-android-sdk:testRelease 39 + run: ./gradlew clean assembleRelease livekit-android-sdk:testRelease
40 40
41 - name: Import video test keys into gradle properties 41 - name: Import video test keys into gradle properties
42 if: github.event_name == 'push' 42 if: github.event_name == 'push'
1 # Android Kotlin SDK for LiveKit 1 # Android Kotlin SDK for LiveKit
2 2
3 -Official Android Client SDK for [LiveKit](https://github.com/livekit/livekit-server). Easily add video & audio capabilities to your Android apps. 3 +Official Android Client SDK for [LiveKit](https://github.com/livekit/livekit-server). Easily add
  4 +video & audio capabilities to your Android apps.
4 5
5 ## Docs 6 ## Docs
6 7
7 -Docs and guides at [https://docs.livekit.io](https://docs.livekit.io) 8 +Docs and guides at [https://docs.livekit.io](https://docs.livekit.io).
  9 +
  10 +API reference can be found
  11 +at [https://docs.livekit.io/client-sdk-android/index.html](https://docs.livekit.io/client-sdk-android/index.html)
  12 +.
8 13
9 ## Installation 14 ## Installation
10 15
@@ -35,13 +40,6 @@ subprojects { @@ -35,13 +40,6 @@ subprojects {
35 } 40 }
36 ``` 41 ```
37 42
38 -## Sample App  
39 -  
40 -There are two sample apps with similar functionality:  
41 -  
42 -* [Compose app](https://github.com/livekit/client-sdk-android/tree/master/sample-app-compose/src/main/java/io/livekit/android/composesample)  
43 -* [Standard app](https://github.com/livekit/client-sdk-android/tree/master/sample-app)  
44 -  
45 ## Usage 43 ## Usage
46 44
47 ### Permissions 45 ### Permissions
@@ -86,36 +84,47 @@ LiveKit uses `SurfaceViewRenderer` to render video tracks. A `TextureView` imple @@ -86,36 +84,47 @@ LiveKit uses `SurfaceViewRenderer` to render video tracks. A `TextureView` imple
86 provided through `TextureViewRenderer`. Subscribed audio tracks are automatically played. 84 provided through `TextureViewRenderer`. Subscribed audio tracks are automatically played.
87 85
88 ```kt 86 ```kt
89 -class MainActivity : AppCompatActivity(), RoomListener { 87 +class MainActivity : AppCompatActivity() {
  88 +
  89 + lateinit var room: Room
  90 +
90 override fun onCreate(savedInstanceState: Bundle?) { 91 override fun onCreate(savedInstanceState: Bundle?) {
91 super.onCreate(savedInstanceState) 92 super.onCreate(savedInstanceState)
92 - ...  
93 - val url = "wss://your_host"; 93 +
  94 + setContentView(R.layout.activity_main)
  95 +
  96 + // Create Room object.
  97 + room = LiveKit.create(applicationContext)
  98 +
  99 + // Setup the video renderer
  100 + room.initVideoRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer))
  101 +
  102 + connectToRoom()
  103 + }
  104 +
  105 + private fun connectToRoom() {
  106 +
  107 + val url = "wss://your_host"
94 val token = "your_token" 108 val token = "your_token"
95 109
96 lifecycleScope.launch { 110 lifecycleScope.launch {
97 - // Create Room object.  
98 - val room = LiveKit.create(  
99 - applicationContext,  
100 - RoomOptions(),  
101 - )  
102 - 111 +
103 // Setup event handling. 112 // Setup event handling.
104 launch { 113 launch {
105 room.events.collect { event -> 114 room.events.collect { event ->
106 - when(event){ 115 + when (event) {
107 is RoomEvent.TrackSubscribed -> onTrackSubscribed(event) 116 is RoomEvent.TrackSubscribed -> onTrackSubscribed(event)
  117 + else -> {}
108 } 118 }
109 } 119 }
110 } 120 }
111 - 121 +
112 // Connect to server. 122 // Connect to server.
113 room.connect( 123 room.connect(
114 url, 124 url,
115 token, 125 token,
116 - ConnectOptions()  
117 ) 126 )
118 - 127 +
119 // Turn on audio/video recording. 128 // Turn on audio/video recording.
120 val localParticipant = room.localParticipant 129 val localParticipant = room.localParticipant
121 localParticipant.setMicrophoneEnabled(true) 130 localParticipant.setMicrophoneEnabled(true)
@@ -124,22 +133,27 @@ class MainActivity : AppCompatActivity(), RoomListener { @@ -124,22 +133,27 @@ class MainActivity : AppCompatActivity(), RoomListener {
124 } 133 }
125 134
126 private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) { 135 private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) {
127 - if (event.track is VideoTrack) { 136 + val track = event.track
  137 + if (track is VideoTrack) {
128 attachVideo(track) 138 attachVideo(track)
129 } 139 }
130 } 140 }
131 141
132 private fun attachVideo(videoTrack: VideoTrack) { 142 private fun attachVideo(videoTrack: VideoTrack) {
133 - // viewBinding.renderer is a `io.livekit.android.renderer.SurfaceViewRenderer` in your  
134 - // layout  
135 - videoTrack.addRenderer(viewBinding.renderer) 143 + videoTrack.addRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer))
  144 + findViewById<View>(R.id.progress).visibility = View.GONE
136 } 145 }
137 } 146 }
138 ``` 147 ```
139 148
  149 +See
  150 +the [basic sample app](https://github.com/livekit/client-sdk-android/blob/main/sample-app-basic/src/main/java/io/livekit/android/sample/basic/MainActivity.kt)
  151 +for the full implementation.
  152 +
140 ### `@FlowObservable` 153 ### `@FlowObservable`
141 154
142 -Properties marked with `@FlowObservable` can be accessed as a Kotlin Flow to observe changes directly: 155 +Properties marked with `@FlowObservable` can be accessed as a Kotlin Flow to observe changes
  156 +directly:
143 157
144 ```kt 158 ```kt
145 coroutineScope.launch { 159 coroutineScope.launch {
@@ -149,12 +163,33 @@ coroutineScope.launch { @@ -149,12 +163,33 @@ coroutineScope.launch {
149 } 163 }
150 ``` 164 ```
151 165
  166 +## Sample App
  167 +
  168 +We have a basic quickstart sample
  169 +app [here](https://github.com/livekit/client-sdk-android/blob/main/sample-app-basic), showing how to
  170 +connect to a room, publish your device's audio/video, and display the video of one remote participant.
  171 +
  172 +There are two more full featured video conferencing sample apps:
  173 +
  174 +* [Compose app](https://github.com/livekit/client-sdk-android/tree/main/sample-app-compose/src/main/java/io/livekit/android/composesample)
  175 +* [Standard app](https://github.com/livekit/client-sdk-android/tree/main/sample-app/src/main/java/io/livekit/android/sample)
  176 +
  177 +They both use
  178 +the [`CallViewModel`](https://github.com/livekit/client-sdk-android/blob/main/sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt)
  179 +, which handles the `Room` connection and exposes the data needed for a basic video conferencing
  180 +app.
  181 +
  182 +The respective `ParticipantItem` class in each app is responsible for the displaying of each
  183 +participant's UI.
  184 +
  185 +* [Compose `ParticipantItem`](https://github.com/livekit/client-sdk-android/blob/main/sample-app-compose/src/main/java/io/livekit/android/composesample/ParticipantItem.kt)
  186 +* [Standard `ParticipantItem`](https://github.com/livekit/client-sdk-android/blob/main/sample-app/src/main/java/io/livekit/android/sample/ParticipantItem.kt)
  187 +
152 ## Dev Environment 188 ## Dev Environment
153 189
154 -To develop the Android SDK or running the sample app, you'll need: 190 +To develop the Android SDK or running the sample app directly from this repo, you'll need:
155 191
156 - Ensure the protocol submodule repo is initialized and updated with `git submodule update --init` 192 - Ensure the protocol submodule repo is initialized and updated with `git submodule update --init`
157 -- Install [Android Studio Arctic Fox 2020.3.1+](https://developer.android.com/studio)  
158 193
159 For those developing on Apple M1 Macs, please add below to $HOME/.gradle/gradle.properties 194 For those developing on Apple M1 Macs, please add below to $HOME/.gradle/gradle.properties
160 195
@@ -2,9 +2,9 @@ @@ -2,9 +2,9 @@
2 2
3 buildscript { 3 buildscript {
4 ext { 4 ext {
5 - compose_version = '1.1.1'  
6 - compose_compiler_version = '1.1.1'  
7 - kotlin_version = '1.6.10' 5 + compose_version = '1.2.0'
  6 + compose_compiler_version = '1.2.0'
  7 + kotlin_version = '1.7.0'
8 java_version = JavaVersion.VERSION_1_8 8 java_version = JavaVersion.VERSION_1_8
9 dokka_version = '1.5.0' 9 dokka_version = '1.5.0'
10 } 10 }
@@ -13,8 +13,8 @@ buildscript { @@ -13,8 +13,8 @@ buildscript {
13 mavenCentral() 13 mavenCentral()
14 } 14 }
15 dependencies { 15 dependencies {
16 - classpath 'com.android.tools.build:gradle:7.1.2'  
17 - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 + classpath 'com.android.tools.build:gradle:7.1.3'
  17 + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0"
18 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 18 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
19 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" 19 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
20 classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' 20 classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'
@@ -45,15 +45,15 @@ nexusStaging { @@ -45,15 +45,15 @@ nexusStaging {
45 45
46 ext { 46 ext {
47 androidSdk = [ 47 androidSdk = [
48 - compileVersion: 31,  
49 - targetVersion : 31, 48 + compileVersion: 32,
  49 + targetVersion : 32,
50 minVersion : 21, 50 minVersion : 21,
51 ] 51 ]
52 versions = [ 52 versions = [
53 - androidx_core : "1.7.0",  
54 - androidx_lifecycle: "2.4.0", 53 + androidx_core : "1.8.0",
  54 + androidx_lifecycle: "2.5.1",
55 autoService : '1.0.1', 55 autoService : '1.0.1',
56 - dagger : "2.27", 56 + dagger : "2.43",
57 groupie : "2.9.0", 57 groupie : "2.9.0",
58 junit : "4.13.2", 58 junit : "4.13.2",
59 junitJupiter : "5.5.0", 59 junitJupiter : "5.5.0",
@@ -49,7 +49,7 @@ android { @@ -49,7 +49,7 @@ android {
49 kotlinCompilerExtensionVersion compose_compiler_version 49 kotlinCompilerExtensionVersion compose_compiler_version
50 } 50 }
51 kotlinOptions { 51 kotlinOptions {
52 - freeCompilerArgs = ["-Xinline-classes", "-Xopt-in=kotlin.RequiresOptIn"] 52 + freeCompilerArgs = ["-Xinline-classes", "-opt-in=kotlin.RequiresOptIn"]
53 jvmTarget = java_version 53 jvmTarget = java_version
54 } 54 }
55 } 55 }
@@ -114,13 +114,13 @@ dependencies { @@ -114,13 +114,13 @@ dependencies {
114 api 'com.github.webrtc-sdk:android:104.5112.01' 114 api 'com.github.webrtc-sdk:android:104.5112.01'
115 api "com.squareup.okhttp3:okhttp:4.10.0" 115 api "com.squareup.okhttp3:okhttp:4.10.0"
116 api "com.twilio:audioswitch:1.1.5" 116 api "com.twilio:audioswitch:1.1.5"
117 - implementation "androidx.annotation:annotation:1.3.0" 117 + implementation "androidx.annotation:annotation:1.4.0"
118 implementation "androidx.core:core:${versions.androidx_core}" 118 implementation "androidx.core:core:${versions.androidx_core}"
119 implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}" 119 implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}"
120 implementation "androidx.compose.ui:ui:$compose_version" 120 implementation "androidx.compose.ui:ui:$compose_version"
121 121
122 - implementation 'com.google.dagger:dagger:2.38'  
123 - kapt 'com.google.dagger:dagger-compiler:2.38' 122 + implementation "com.google.dagger:dagger:${versions.dagger}"
  123 + kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
124 124
125 implementation deps.timber 125 implementation deps.timber
126 implementation 'com.vdurmont:semver4j:3.1.0' 126 implementation 'com.vdurmont:semver4j:3.1.0'
@@ -6,6 +6,15 @@ Android Client SDK to [LiveKit](https://github.com/livekit/livekit-server). @@ -6,6 +6,15 @@ Android Client SDK to [LiveKit](https://github.com/livekit/livekit-server).
6 6
7 This package contains the initial `connect` function. 7 This package contains the initial `connect` function.
8 8
  9 +# Package io.livekit.android.compose
  10 +
  11 +Utilities and composables for use with Jetpack Compose.
  12 +
9 # Package io.livekit.android.room 13 # Package io.livekit.android.room
10 14
11 -Room is the primary class that manages the connection to the LiveKit Room. It exposes listeners that lets you hook into room events 15 +Room is the primary class that manages the connection to the LiveKit Room. It exposes listeners that lets you hook into room events.
  16 +
  17 +# Package io.livekit.android.room.track
  18 +
  19 +`AudioTrack` and `VideoTrack` are the classes that represent the types of media streams that can be
  20 +subscribed and published.
@@ -10,13 +10,43 @@ import com.twilio.audioswitch.AudioSwitch @@ -10,13 +10,43 @@ import com.twilio.audioswitch.AudioSwitch
10 import javax.inject.Inject 10 import javax.inject.Inject
11 import javax.inject.Singleton 11 import javax.inject.Singleton
12 12
  13 +/**
  14 + * An [AudioHandler] built on top of [AudioSwitch].
  15 + *
  16 + * The various settings should be set before connecting to a [Room] and [start] is called.
  17 + */
13 @Singleton 18 @Singleton
14 class AudioSwitchHandler 19 class AudioSwitchHandler
15 @Inject 20 @Inject
16 constructor(private val context: Context) : AudioHandler { 21 constructor(private val context: Context) : AudioHandler {
  22 + /**
  23 + * Toggle whether logging is enabled for [AudioSwitch]. By default, this is set to false.
  24 + */
17 var loggingEnabled = false 25 var loggingEnabled = false
  26 +
  27 + /**
  28 + * Listen to changes in the available and active audio devices.
  29 + *
  30 + * @see AudioDeviceChangeListener
  31 + */
18 var audioDeviceChangeListener: AudioDeviceChangeListener? = null 32 var audioDeviceChangeListener: AudioDeviceChangeListener? = null
  33 +
  34 + /**
  35 + * Listen to changes in audio focus.
  36 + *
  37 + * @see AudioManager.OnAudioFocusChangeListener
  38 + */
19 var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null 39 var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
  40 +
  41 + /**
  42 + * The preferred priority of audio devices to use. The first available audio device will be used.
  43 + *
  44 + * By default, the preferred order is set to:
  45 + * 1. BluetoothHeadset
  46 + * 2. WiredHeadset
  47 + * 3. Earpiece
  48 + * 4. Speakerphone
  49 + */
20 var preferredDeviceList: List<Class<out AudioDevice>>? = null 50 var preferredDeviceList: List<Class<out AudioDevice>>? = null
21 51
22 private var audioSwitch: AudioSwitch? = null 52 private var audioSwitch: AudioSwitch? = null
@@ -602,6 +602,7 @@ internal constructor( @@ -602,6 +602,7 @@ internal constructor(
602 LKLog.e { "error setting remote description for answer: ${outcome.value} " } 602 LKLog.e { "error setting remote description for answer: ${outcome.value} " }
603 return@launch 603 return@launch
604 } 604 }
  605 + else -> {}
605 } 606 }
606 } 607 }
607 608
@@ -621,6 +622,7 @@ internal constructor( @@ -621,6 +622,7 @@ internal constructor(
621 LKLog.e { "error setting local description for answer: ${outcome.value}" } 622 LKLog.e { "error setting local description for answer: ${outcome.value}" }
622 return@launch 623 return@launch
623 } 624 }
  625 + else -> {}
624 } 626 }
625 } 627 }
626 628
@@ -45,7 +45,7 @@ constructor( @@ -45,7 +45,7 @@ constructor(
45 @Named(InjectionNames.DISPATCHER_IO) 45 @Named(InjectionNames.DISPATCHER_IO)
46 private val ioDispatcher: CoroutineDispatcher, 46 private val ioDispatcher: CoroutineDispatcher,
47 val audioHandler: AudioHandler, 47 val audioHandler: AudioHandler,
48 -) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { 48 +) : RTCEngine.Listener, ParticipantListener {
49 49
50 private lateinit var coroutineScope: CoroutineScope 50 private lateinit var coroutineScope: CoroutineScope
51 private val eventBus = BroadcastEventBus<RoomEvent>() 51 private val eventBus = BroadcastEventBus<RoomEvent>()
@@ -217,7 +217,7 @@ constructor( @@ -217,7 +217,7 @@ constructor(
217 val networkRequest = NetworkRequest.Builder() 217 val networkRequest = NetworkRequest.Builder()
218 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 218 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
219 .build() 219 .build()
220 - cm.registerNetworkCallback(networkRequest, this) 220 + cm.registerNetworkCallback(networkRequest, networkCallback)
221 221
222 if (options.audio) { 222 if (options.audio) {
223 val audioTrack = localParticipant.createAudioTrack() 223 val audioTrack = localParticipant.createAudioTrack()
@@ -237,6 +237,9 @@ constructor( @@ -237,6 +237,9 @@ constructor(
237 handleDisconnect() 237 handleDisconnect()
238 } 238 }
239 239
  240 + /**
  241 + * @suppress
  242 + */
240 override fun onJoinResponse(response: LivekitRtc.JoinResponse) { 243 override fun onJoinResponse(response: LivekitRtc.JoinResponse) {
241 244
242 LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" } 245 LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" }
@@ -441,7 +444,7 @@ constructor( @@ -441,7 +444,7 @@ constructor(
441 444
442 try { 445 try {
443 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 446 val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
444 - cm.unregisterNetworkCallback(this) 447 + cm.unregisterNetworkCallback(networkCallback)
445 } catch (e: IllegalArgumentException) { 448 } catch (e: IllegalArgumentException) {
446 // do nothing, may happen on older versions if attempting to unregister twice. 449 // do nothing, may happen on older versions if attempting to unregister twice.
447 } 450 }
@@ -512,29 +515,29 @@ constructor( @@ -512,29 +515,29 @@ constructor(
512 } 515 }
513 516
514 //------------------------------------- NetworkCallback -------------------------------------// 517 //------------------------------------- NetworkCallback -------------------------------------//
515 -  
516 - /**  
517 - * @suppress  
518 - */  
519 - override fun onLost(network: Network) {  
520 - // lost connection, flip to reconnecting  
521 - hasLostConnectivity = true  
522 - }  
523 -  
524 - /**  
525 - * @suppress  
526 - */  
527 - override fun onAvailable(network: Network) {  
528 - // only actually reconnect after connection is re-established  
529 - if (!hasLostConnectivity) {  
530 - return 518 + private val networkCallback = object : ConnectivityManager.NetworkCallback() {
  519 + /**
  520 + * @suppress
  521 + */
  522 + override fun onLost(network: Network) {
  523 + // lost connection, flip to reconnecting
  524 + hasLostConnectivity = true
  525 + }
  526 +
  527 + /**
  528 + * @suppress
  529 + */
  530 + override fun onAvailable(network: Network) {
  531 + // only actually reconnect after connection is re-established
  532 + if (!hasLostConnectivity) {
  533 + return
  534 + }
  535 + LKLog.i { "network connection available, reconnecting" }
  536 + reconnect()
  537 + hasLostConnectivity = false
531 } 538 }
532 - LKLog.i { "network connection available, reconnecting" }  
533 - reconnect()  
534 - hasLostConnectivity = false  
535 } 539 }
536 540
537 -  
538 //----------------------------------- RTCEngine.Listener ------------------------------------// 541 //----------------------------------- RTCEngine.Listener ------------------------------------//
539 542
540 /** 543 /**
@@ -820,7 +823,6 @@ constructor( @@ -820,7 +823,6 @@ constructor(
820 } 823 }
821 824
822 /** 825 /**
823 - * @suppress  
824 * // TODO(@dl): can this be moved out of Room/SDK? 826 * // TODO(@dl): can this be moved out of Room/SDK?
825 */ 827 */
826 fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) { 828 fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) {
@@ -830,7 +832,6 @@ constructor( @@ -830,7 +832,6 @@ constructor(
830 } 832 }
831 833
832 /** 834 /**
833 - * @suppress  
834 * // TODO(@dl): can this be moved out of Room/SDK? 835 * // TODO(@dl): can this be moved out of Room/SDK?
835 */ 836 */
836 fun initVideoRenderer(viewRenderer: TextureViewRenderer) { 837 fun initVideoRenderer(viewRenderer: TextureViewRenderer) {
@@ -180,6 +180,9 @@ internal constructor( @@ -180,6 +180,9 @@ internal constructor(
180 createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) 180 createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
181 publishVideoTrack(track) 181 publishVideoTrack(track)
182 } 182 }
  183 + else -> {
  184 + LKLog.w { "Attempting to enable an unknown source, ignoring." }
  185 + }
183 } 186 }
184 } 187 }
185 } else { 188 } else {
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
  3 +import android.content.Context
  4 +import android.net.ConnectivityManager
3 import android.net.Network 5 import android.net.Network
  6 +import androidx.test.platform.app.InstrumentationRegistry
4 import io.livekit.android.MockE2ETest 7 import io.livekit.android.MockE2ETest
5 import io.livekit.android.events.EventCollector 8 import io.livekit.android.events.EventCollector
6 import io.livekit.android.events.FlowCollector 9 import io.livekit.android.events.FlowCollector
@@ -23,6 +26,9 @@ import org.junit.Test @@ -23,6 +26,9 @@ import org.junit.Test
23 import org.junit.runner.RunWith 26 import org.junit.runner.RunWith
24 import org.mockito.Mockito 27 import org.mockito.Mockito
25 import org.robolectric.RobolectricTestRunner 28 import org.robolectric.RobolectricTestRunner
  29 +import org.robolectric.Shadows.shadowOf
  30 +import org.robolectric.shadows.ShadowConnectivityManager
  31 +import org.robolectric.shadows.ShadowNetworkInfo
26 32
27 @ExperimentalCoroutinesApi 33 @ExperimentalCoroutinesApi
28 @RunWith(RobolectricTestRunner::class) 34 @RunWith(RobolectricTestRunner::class)
@@ -259,8 +265,19 @@ class RoomMockE2ETest : MockE2ETest() { @@ -259,8 +265,19 @@ class RoomMockE2ETest : MockE2ETest() {
259 connect() 265 connect()
260 val eventCollector = EventCollector(room.events, coroutineRule.scope) 266 val eventCollector = EventCollector(room.events, coroutineRule.scope)
261 val network = Mockito.mock(Network::class.java) 267 val network = Mockito.mock(Network::class.java)
262 - room.onLost(network)  
263 - room.onAvailable(network) 268 +
  269 + val connectivityManager = InstrumentationRegistry.getInstrumentation()
  270 + .context
  271 + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  272 + val shadowConnectivityManager: ShadowConnectivityManager = shadowOf(connectivityManager)
  273 +
  274 + shadowConnectivityManager.networkCallbacks.forEach { callback ->
  275 + callback.onLost(network)
  276 + }
  277 + shadowConnectivityManager.networkCallbacks.forEach { callback ->
  278 + callback.onAvailable(network)
  279 + }
  280 +
264 val events = eventCollector.stopCollecting() 281 val events = eventCollector.stopCollecting()
265 282
266 Assert.assertEquals(1, events.size) 283 Assert.assertEquals(1, events.size)
1 package io.livekit.android.room 1 package io.livekit.android.room
2 2
3 import android.content.Context 3 import android.content.Context
  4 +import android.net.ConnectivityManager
4 import android.net.Network 5 import android.net.Network
5 import androidx.test.core.app.ApplicationProvider 6 import androidx.test.core.app.ApplicationProvider
  7 +import androidx.test.platform.app.InstrumentationRegistry
6 import io.livekit.android.audio.NoAudioHandler 8 import io.livekit.android.audio.NoAudioHandler
7 import io.livekit.android.coroutines.TestCoroutineRule 9 import io.livekit.android.coroutines.TestCoroutineRule
8 import io.livekit.android.events.EventCollector 10 import io.livekit.android.events.EventCollector
@@ -26,6 +28,8 @@ import org.mockito.Mockito @@ -26,6 +28,8 @@ import org.mockito.Mockito
26 import org.mockito.junit.MockitoJUnit 28 import org.mockito.junit.MockitoJUnit
27 import org.mockito.kotlin.* 29 import org.mockito.kotlin.*
28 import org.robolectric.RobolectricTestRunner 30 import org.robolectric.RobolectricTestRunner
  31 +import org.robolectric.Shadows
  32 +import org.robolectric.shadows.ShadowConnectivityManager
29 import org.webrtc.EglBase 33 import org.webrtc.EglBase
30 34
31 @ExperimentalCoroutinesApi 35 @ExperimentalCoroutinesApi
@@ -108,8 +112,19 @@ class RoomTest { @@ -108,8 +112,19 @@ class RoomTest {
108 connect() 112 connect()
109 113
110 val network = Mockito.mock(Network::class.java) 114 val network = Mockito.mock(Network::class.java)
111 - room.onLost(network)  
112 - room.onAvailable(network) 115 +
  116 + val connectivityManager = InstrumentationRegistry.getInstrumentation()
  117 + .context
  118 + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  119 + val shadowConnectivityManager: ShadowConnectivityManager = Shadows.shadowOf(connectivityManager)
  120 +
  121 + shadowConnectivityManager.networkCallbacks.forEach { callback ->
  122 + callback.onLost(network)
  123 + }
  124 + shadowConnectivityManager.networkCallbacks.forEach { callback ->
  125 + callback.onAvailable(network)
  126 + }
  127 +
113 Mockito.verify(rtcEngine).reconnect() 128 Mockito.verify(rtcEngine).reconnect()
114 } 129 }
115 130
  1 +# Sample-app-basic
  2 +
  3 +A quickstart app showing how to:
  4 +
  5 +1. Connect to a room.
  6 +2. Publish your device's audio/video.
  7 +3. Display a remote participant's video.
  8 +
  9 +This app only handles the video of one remote participant. For a more fully featured video
  10 +conferencing app, check out the other sample apps in this repo.
  1 +plugins {
  2 + id 'com.android.application'
  3 + id 'org.jetbrains.kotlin.android'
  4 +}
  5 +
  6 +android {
  7 + compileSdk 32
  8 +
  9 + defaultConfig {
  10 + applicationId "io.livekit.android.sample.basic"
  11 + minSdk 21
  12 + targetSdk 32
  13 + versionCode 1
  14 + versionName "1.0"
  15 +
  16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  17 + }
  18 +
  19 + buildTypes {
  20 + release {
  21 + minifyEnabled false
  22 + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  23 + }
  24 + }
  25 + compileOptions {
  26 + sourceCompatibility JavaVersion.VERSION_1_8
  27 + targetCompatibility JavaVersion.VERSION_1_8
  28 + }
  29 + kotlinOptions {
  30 + jvmTarget = '1.8'
  31 + }
  32 +}
  33 +
  34 +dependencies {
  35 +
  36 + implementation "androidx.core:core-ktx:${versions.androidx_core}"
  37 + implementation 'androidx.appcompat:appcompat:1.4.2'
  38 + implementation "androidx.activity:activity-ktx:1.5.1"
  39 + implementation 'androidx.fragment:fragment-ktx:1.5.1'
  40 + implementation "androidx.viewpager2:viewpager2:1.0.0"
  41 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
  42 + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
  43 + implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
  44 + implementation 'com.google.android.material:material:1.6.1'
  45 + implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
  46 + implementation project(":livekit-android-sdk")
  47 + testImplementation 'junit:junit:4.13.2'
  48 + androidTestImplementation 'androidx.test.ext:junit:1.1.3'
  49 + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
  50 +}
  1 +# Add project specific ProGuard rules here.
  2 +# You can control the set of applied configuration files using the
  3 +# proguardFiles setting in build.gradle.
  4 +#
  5 +# For more details, see
  6 +# http://developer.android.com/guide/developing/tools/proguard.html
  7 +
  8 +# If your project uses WebView with JS, uncomment the following
  9 +# and specify the fully qualified class name to the JavaScript interface
  10 +# class:
  11 +#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
  12 +# public *;
  13 +#}
  14 +
  15 +# Uncomment this to preserve the line number information for
  16 +# debugging stack traces.
  17 +#-keepattributes SourceFile,LineNumberTable
  18 +
  19 +# If you keep the line number information, uncomment this to
  20 +# hide the original source file name.
  21 +#-renamesourcefileattribute SourceFile
  1 +package io.livekit.android.sample.basic
  2 +
  3 +import androidx.test.platform.app.InstrumentationRegistry
  4 +import androidx.test.ext.junit.runners.AndroidJUnit4
  5 +
  6 +import org.junit.Test
  7 +import org.junit.runner.RunWith
  8 +
  9 +import org.junit.Assert.*
  10 +
  11 +/**
  12 + * Instrumented test, which will execute on an Android device.
  13 + *
  14 + * See [testing documentation](http://d.android.com/tools/testing).
  15 + */
  16 +@RunWith(AndroidJUnit4::class)
  17 +class ExampleInstrumentedTest {
  18 + @Test
  19 + fun useAppContext() {
  20 + // Context of the app under test.
  21 + val appContext = InstrumentationRegistry.getInstrumentation().targetContext
  22 + assertEquals("io.livekit.android.sample.basic", appContext.packageName)
  23 + }
  24 +}
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3 + package="io.livekit.android.sample.basic">
  4 +
  5 + <application
  6 + android:allowBackup="true"
  7 + android:icon="@mipmap/ic_launcher"
  8 + android:label="@string/app_name"
  9 + android:networkSecurityConfig="@xml/network_security_config"
  10 + android:roundIcon="@mipmap/ic_launcher_round"
  11 + android:supportsRtl="true"
  12 + android:theme="@style/Theme.Livekitandroid">
  13 + <activity
  14 + android:name=".MainActivity"
  15 + android:exported="true">
  16 + <intent-filter>
  17 + <action android:name="android.intent.action.MAIN" />
  18 +
  19 + <category android:name="android.intent.category.LAUNCHER" />
  20 + </intent-filter>
  21 + </activity>
  22 + </application>
  23 +
  24 +</manifest>
  1 +package io.livekit.android.sample.basic
  2 +
  3 +import android.Manifest
  4 +import android.content.pm.PackageManager
  5 +import android.os.Build
  6 +import android.os.Bundle
  7 +import android.view.View
  8 +import android.widget.Toast
  9 +import androidx.activity.result.contract.ActivityResultContracts
  10 +import androidx.appcompat.app.AppCompatActivity
  11 +import androidx.core.content.ContextCompat
  12 +import androidx.lifecycle.lifecycleScope
  13 +import io.livekit.android.LiveKit
  14 +import io.livekit.android.events.RoomEvent
  15 +import io.livekit.android.events.collect
  16 +import io.livekit.android.renderer.SurfaceViewRenderer
  17 +import io.livekit.android.room.Room
  18 +import io.livekit.android.room.track.VideoTrack
  19 +import kotlinx.coroutines.launch
  20 +
  21 +class MainActivity : AppCompatActivity() {
  22 +
  23 + lateinit var room: Room
  24 +
  25 + override fun onCreate(savedInstanceState: Bundle?) {
  26 + super.onCreate(savedInstanceState)
  27 +
  28 + setContentView(R.layout.activity_main)
  29 +
  30 + // Create Room object.
  31 + room = LiveKit.create(applicationContext)
  32 +
  33 + // Setup the video renderer
  34 + room.initVideoRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer))
  35 +
  36 + requestNeededPermissions { connectToRoom() }
  37 + }
  38 +
  39 + private fun connectToRoom() {
  40 +
  41 + val url = "wss://your_host"
  42 + val token = "your_token"
  43 +
  44 + lifecycleScope.launch {
  45 +
  46 + // Setup event handling.
  47 + launch {
  48 + room.events.collect { event ->
  49 + when (event) {
  50 + is RoomEvent.TrackSubscribed -> onTrackSubscribed(event)
  51 + else -> {}
  52 + }
  53 + }
  54 + }
  55 +
  56 + // Connect to server.
  57 + room.connect(
  58 + url,
  59 + token,
  60 + )
  61 +
  62 + // Turn on audio/video recording.
  63 + val localParticipant = room.localParticipant
  64 + localParticipant.setMicrophoneEnabled(true)
  65 + localParticipant.setCameraEnabled(true)
  66 + }
  67 + }
  68 +
  69 + private fun onTrackSubscribed(event: RoomEvent.TrackSubscribed) {
  70 + val track = event.track
  71 + if (track is VideoTrack) {
  72 + attachVideo(track)
  73 + }
  74 + }
  75 +
  76 + private fun attachVideo(videoTrack: VideoTrack) {
  77 + videoTrack.addRenderer(findViewById<SurfaceViewRenderer>(R.id.renderer))
  78 + findViewById<View>(R.id.progress).visibility = View.GONE
  79 + }
  80 +
  81 + private fun requestNeededPermissions(onHasPermissions: () -> Unit) {
  82 + val requestPermissionLauncher =
  83 + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants ->
  84 + var hasDenied = false
  85 + // Check if any permissions weren't granted.
  86 + for (grant in grants.entries) {
  87 + if (!grant.value) {
  88 + Toast.makeText(this, "Missing permission: ${grant.key}", Toast.LENGTH_SHORT).show()
  89 +
  90 + hasDenied = true
  91 + }
  92 + }
  93 +
  94 + if (!hasDenied) {
  95 + onHasPermissions()
  96 + }
  97 + }
  98 +
  99 + // Assemble the needed permissions to request
  100 + val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
  101 + .let { perms ->
  102 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
  103 + // Need BLUETOOTH_CONNECT permission on API S+ to output to bluetooth devices.
  104 + perms + listOf(Manifest.permission.BLUETOOTH_CONNECT)
  105 + } else {
  106 + perms
  107 + }
  108 + }
  109 + .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED }
  110 + .toTypedArray()
  111 +
  112 + if (neededPermissions.isNotEmpty()) {
  113 + requestPermissionLauncher.launch(neededPermissions)
  114 + } else {
  115 + onHasPermissions()
  116 + }
  117 + }
  118 +
  119 + override fun onDestroy() {
  120 + super.onDestroy()
  121 + room.disconnect()
  122 + }
  123 +}
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + xmlns:aapt="http://schemas.android.com/aapt"
  3 + android:width="108dp"
  4 + android:height="108dp"
  5 + android:viewportWidth="108"
  6 + android:viewportHeight="108">
  7 + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
  8 + <aapt:attr name="android:fillColor">
  9 + <gradient
  10 + android:endX="85.84757"
  11 + android:endY="92.4963"
  12 + android:startX="42.9492"
  13 + android:startY="49.59793"
  14 + android:type="linear">
  15 + <item
  16 + android:color="#44000000"
  17 + android:offset="0.0" />
  18 + <item
  19 + android:color="#00000000"
  20 + android:offset="1.0" />
  21 + </gradient>
  22 + </aapt:attr>
  23 + </path>
  24 + <path
  25 + android:fillColor="#FFFFFF"
  26 + android:fillType="nonZero"
  27 + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
  28 + android:strokeWidth="1"
  29 + android:strokeColor="#00000000" />
  30 +</vector>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  3 + android:width="108dp"
  4 + android:height="108dp"
  5 + android:viewportWidth="108"
  6 + android:viewportHeight="108">
  7 + <path
  8 + android:fillColor="#3DDC84"
  9 + android:pathData="M0,0h108v108h-108z" />
  10 + <path
  11 + android:fillColor="#00000000"
  12 + android:pathData="M9,0L9,108"
  13 + android:strokeWidth="0.8"
  14 + android:strokeColor="#33FFFFFF" />
  15 + <path
  16 + android:fillColor="#00000000"
  17 + android:pathData="M19,0L19,108"
  18 + android:strokeWidth="0.8"
  19 + android:strokeColor="#33FFFFFF" />
  20 + <path
  21 + android:fillColor="#00000000"
  22 + android:pathData="M29,0L29,108"
  23 + android:strokeWidth="0.8"
  24 + android:strokeColor="#33FFFFFF" />
  25 + <path
  26 + android:fillColor="#00000000"
  27 + android:pathData="M39,0L39,108"
  28 + android:strokeWidth="0.8"
  29 + android:strokeColor="#33FFFFFF" />
  30 + <path
  31 + android:fillColor="#00000000"
  32 + android:pathData="M49,0L49,108"
  33 + android:strokeWidth="0.8"
  34 + android:strokeColor="#33FFFFFF" />
  35 + <path
  36 + android:fillColor="#00000000"
  37 + android:pathData="M59,0L59,108"
  38 + android:strokeWidth="0.8"
  39 + android:strokeColor="#33FFFFFF" />
  40 + <path
  41 + android:fillColor="#00000000"
  42 + android:pathData="M69,0L69,108"
  43 + android:strokeWidth="0.8"
  44 + android:strokeColor="#33FFFFFF" />
  45 + <path
  46 + android:fillColor="#00000000"
  47 + android:pathData="M79,0L79,108"
  48 + android:strokeWidth="0.8"
  49 + android:strokeColor="#33FFFFFF" />
  50 + <path
  51 + android:fillColor="#00000000"
  52 + android:pathData="M89,0L89,108"
  53 + android:strokeWidth="0.8"
  54 + android:strokeColor="#33FFFFFF" />
  55 + <path
  56 + android:fillColor="#00000000"
  57 + android:pathData="M99,0L99,108"
  58 + android:strokeWidth="0.8"
  59 + android:strokeColor="#33FFFFFF" />
  60 + <path
  61 + android:fillColor="#00000000"
  62 + android:pathData="M0,9L108,9"
  63 + android:strokeWidth="0.8"
  64 + android:strokeColor="#33FFFFFF" />
  65 + <path
  66 + android:fillColor="#00000000"
  67 + android:pathData="M0,19L108,19"
  68 + android:strokeWidth="0.8"
  69 + android:strokeColor="#33FFFFFF" />
  70 + <path
  71 + android:fillColor="#00000000"
  72 + android:pathData="M0,29L108,29"
  73 + android:strokeWidth="0.8"
  74 + android:strokeColor="#33FFFFFF" />
  75 + <path
  76 + android:fillColor="#00000000"
  77 + android:pathData="M0,39L108,39"
  78 + android:strokeWidth="0.8"
  79 + android:strokeColor="#33FFFFFF" />
  80 + <path
  81 + android:fillColor="#00000000"
  82 + android:pathData="M0,49L108,49"
  83 + android:strokeWidth="0.8"
  84 + android:strokeColor="#33FFFFFF" />
  85 + <path
  86 + android:fillColor="#00000000"
  87 + android:pathData="M0,59L108,59"
  88 + android:strokeWidth="0.8"
  89 + android:strokeColor="#33FFFFFF" />
  90 + <path
  91 + android:fillColor="#00000000"
  92 + android:pathData="M0,69L108,69"
  93 + android:strokeWidth="0.8"
  94 + android:strokeColor="#33FFFFFF" />
  95 + <path
  96 + android:fillColor="#00000000"
  97 + android:pathData="M0,79L108,79"
  98 + android:strokeWidth="0.8"
  99 + android:strokeColor="#33FFFFFF" />
  100 + <path
  101 + android:fillColor="#00000000"
  102 + android:pathData="M0,89L108,89"
  103 + android:strokeWidth="0.8"
  104 + android:strokeColor="#33FFFFFF" />
  105 + <path
  106 + android:fillColor="#00000000"
  107 + android:pathData="M0,99L108,99"
  108 + android:strokeWidth="0.8"
  109 + android:strokeColor="#33FFFFFF" />
  110 + <path
  111 + android:fillColor="#00000000"
  112 + android:pathData="M19,29L89,29"
  113 + android:strokeWidth="0.8"
  114 + android:strokeColor="#33FFFFFF" />
  115 + <path
  116 + android:fillColor="#00000000"
  117 + android:pathData="M19,39L89,39"
  118 + android:strokeWidth="0.8"
  119 + android:strokeColor="#33FFFFFF" />
  120 + <path
  121 + android:fillColor="#00000000"
  122 + android:pathData="M19,49L89,49"
  123 + android:strokeWidth="0.8"
  124 + android:strokeColor="#33FFFFFF" />
  125 + <path
  126 + android:fillColor="#00000000"
  127 + android:pathData="M19,59L89,59"
  128 + android:strokeWidth="0.8"
  129 + android:strokeColor="#33FFFFFF" />
  130 + <path
  131 + android:fillColor="#00000000"
  132 + android:pathData="M19,69L89,69"
  133 + android:strokeWidth="0.8"
  134 + android:strokeColor="#33FFFFFF" />
  135 + <path
  136 + android:fillColor="#00000000"
  137 + android:pathData="M19,79L89,79"
  138 + android:strokeWidth="0.8"
  139 + android:strokeColor="#33FFFFFF" />
  140 + <path
  141 + android:fillColor="#00000000"
  142 + android:pathData="M29,19L29,89"
  143 + android:strokeWidth="0.8"
  144 + android:strokeColor="#33FFFFFF" />
  145 + <path
  146 + android:fillColor="#00000000"
  147 + android:pathData="M39,19L39,89"
  148 + android:strokeWidth="0.8"
  149 + android:strokeColor="#33FFFFFF" />
  150 + <path
  151 + android:fillColor="#00000000"
  152 + android:pathData="M49,19L49,89"
  153 + android:strokeWidth="0.8"
  154 + android:strokeColor="#33FFFFFF" />
  155 + <path
  156 + android:fillColor="#00000000"
  157 + android:pathData="M59,19L59,89"
  158 + android:strokeWidth="0.8"
  159 + android:strokeColor="#33FFFFFF" />
  160 + <path
  161 + android:fillColor="#00000000"
  162 + android:pathData="M69,19L69,89"
  163 + android:strokeWidth="0.8"
  164 + android:strokeColor="#33FFFFFF" />
  165 + <path
  166 + android:fillColor="#00000000"
  167 + android:pathData="M79,19L79,89"
  168 + android:strokeWidth="0.8"
  169 + android:strokeColor="#33FFFFFF" />
  170 +</vector>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:tools="http://schemas.android.com/tools"
  4 + android:layout_width="match_parent"
  5 + android:layout_height="match_parent"
  6 + tools:context=".MainActivity">
  7 +
  8 + <io.livekit.android.renderer.SurfaceViewRenderer
  9 + android:id="@+id/renderer"
  10 + android:layout_width="match_parent"
  11 + android:layout_height="match_parent" />
  12 +
  13 + <ProgressBar
  14 + android:id="@+id/progress"
  15 + android:layout_width="wrap_content"
  16 + android:layout_height="wrap_content"
  17 + android:layout_gravity="center" />
  18 +
  19 +</FrameLayout>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  3 + <background android:drawable="@drawable/ic_launcher_background" />
  4 + <foreground android:drawable="@drawable/ic_launcher_foreground" />
  5 +</adaptive-icon>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  3 + <background android:drawable="@drawable/ic_launcher_background" />
  4 + <foreground android:drawable="@drawable/ic_launcher_foreground" />
  5 +</adaptive-icon>
  1 +<resources xmlns:tools="http://schemas.android.com/tools">
  2 + <!-- Base application theme. -->
  3 + <style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
  4 + <!-- Primary brand color. -->
  5 + <item name="colorPrimary">@color/purple_200</item>
  6 + <item name="colorPrimaryVariant">@color/purple_700</item>
  7 + <item name="colorOnPrimary">@color/black</item>
  8 + <!-- Secondary brand color. -->
  9 + <item name="colorSecondary">@color/teal_200</item>
  10 + <item name="colorSecondaryVariant">@color/teal_200</item>
  11 + <item name="colorOnSecondary">@color/black</item>
  12 + <!-- Status bar color. -->
  13 + <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
  14 + <!-- Customize your theme here. -->
  15 + </style>
  16 +</resources>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<resources>
  3 + <color name="purple_200">#FFBB86FC</color>
  4 + <color name="purple_500">#FF6200EE</color>
  5 + <color name="purple_700">#FF3700B3</color>
  6 + <color name="teal_200">#FF03DAC5</color>
  7 + <color name="teal_700">#FF018786</color>
  8 + <color name="black">#FF000000</color>
  9 + <color name="white">#FFFFFFFF</color>
  10 +</resources>
  1 +<resources>
  2 + <string name="app_name">sample-app-basic</string>
  3 +</resources>
  1 +<resources xmlns:tools="http://schemas.android.com/tools">
  2 + <!-- Base application theme. -->
  3 + <style name="Theme.Livekitandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
  4 + <!-- Primary brand color. -->
  5 + <item name="colorPrimary">@color/purple_500</item>
  6 + <item name="colorPrimaryVariant">@color/purple_700</item>
  7 + <item name="colorOnPrimary">@color/white</item>
  8 + <!-- Secondary brand color. -->
  9 + <item name="colorSecondary">@color/teal_200</item>
  10 + <item name="colorSecondaryVariant">@color/teal_700</item>
  11 + <item name="colorOnSecondary">@color/black</item>
  12 + <!-- Status bar color. -->
  13 + <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
  14 + <!-- Customize your theme here. -->
  15 + </style>
  16 +</resources>
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<network-security-config>
  3 + <domain-config cleartextTrafficPermitted="true">
  4 + <domain includeSubdomains="true">example.com</domain>
  5 + </domain-config>
  6 +
  7 + <base-config cleartextTrafficPermitted="true">
  8 + <trust-anchors>
  9 + <certificates src="system" />
  10 + </trust-anchors>
  11 + </base-config>
  12 +</network-security-config>
  1 +package io.livekit.android.sample.basic
  2 +
  3 +import org.junit.Test
  4 +
  5 +import org.junit.Assert.*
  6 +
  7 +/**
  8 + * Example local unit test, which will execute on the development machine (host).
  9 + *
  10 + * See [testing documentation](http://d.android.com/tools/testing).
  11 + */
  12 +class ExampleUnitTest {
  13 + @Test
  14 + fun addition_isCorrect() {
  15 + assertEquals(4, 2 + 2)
  16 + }
  17 +}
  1 +# sample-app-common
  2 +
  3 +Contains code common to `sample-app` and `sample-app-compose`.
1 <?xml version="1.0" encoding="utf-8"?> 1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 - package="io.livekit.android.sample"> 3 + package="io.livekit.android.sample.common">
4 4
5 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 5 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6 <uses-permission android:name="android.permission.INTERNET" /> 6 <uses-permission android:name="android.permission.INTERNET" />
@@ -2,6 +2,7 @@ package io.livekit.android.sample @@ -2,6 +2,7 @@ package io.livekit.android.sample
2 2
3 import android.app.Application 3 import android.app.Application
4 import android.content.Intent 4 import android.content.Intent
  5 +import android.media.projection.MediaProjectionManager
5 import androidx.lifecycle.AndroidViewModel 6 import androidx.lifecycle.AndroidViewModel
6 import androidx.lifecycle.LiveData 7 import androidx.lifecycle.LiveData
7 import androidx.lifecycle.MutableLiveData 8 import androidx.lifecycle.MutableLiveData
@@ -57,6 +58,7 @@ class CallViewModel( @@ -57,6 +58,7 @@ class CallViewModel(
57 58
58 private var localScreencastTrack: LocalScreencastVideoTrack? = null 59 private var localScreencastTrack: LocalScreencastVideoTrack? = null
59 60
  61 + // Controls
60 private val mutableMicEnabled = MutableLiveData(true) 62 private val mutableMicEnabled = MutableLiveData(true)
61 val micEnabled = mutableMicEnabled.hide() 63 val micEnabled = mutableMicEnabled.hide()
62 64
@@ -69,18 +71,22 @@ class CallViewModel( @@ -69,18 +71,22 @@ class CallViewModel(
69 private val mutableScreencastEnabled = MutableLiveData(false) 71 private val mutableScreencastEnabled = MutableLiveData(false)
70 val screenshareEnabled = mutableScreencastEnabled.hide() 72 val screenshareEnabled = mutableScreencastEnabled.hide()
71 73
  74 + // Emits a string whenever a data message is received.
72 private val mutableDataReceived = MutableSharedFlow<String>() 75 private val mutableDataReceived = MutableSharedFlow<String>()
73 val dataReceived = mutableDataReceived 76 val dataReceived = mutableDataReceived
74 77
  78 + // Whether other participants are allowed to subscribe to this participant's tracks.
75 private val mutablePermissionAllowed = MutableStateFlow(true) 79 private val mutablePermissionAllowed = MutableStateFlow(true)
76 val permissionAllowed = mutablePermissionAllowed.hide() 80 val permissionAllowed = mutablePermissionAllowed.hide()
77 81
78 init { 82 init {
79 viewModelScope.launch { 83 viewModelScope.launch {
  84 + // Collect any errors.
80 launch { 85 launch {
81 error.collect { Timber.e(it) } 86 error.collect { Timber.e(it) }
82 } 87 }
83 88
  89 + // Handle any changes in speakers.
84 launch { 90 launch {
85 combine(participants, activeSpeakers) { participants, speakers -> participants to speakers } 91 combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }
86 .collect { (participantsList, speakers) -> 92 .collect { (participantsList, speakers) ->
@@ -93,6 +99,7 @@ class CallViewModel( @@ -93,6 +99,7 @@ class CallViewModel(
93 } 99 }
94 100
95 launch { 101 launch {
  102 + // Handle room events.
96 room.events.collect { 103 room.events.collect {
97 when (it) { 104 when (it) {
98 is RoomEvent.FailedToConnect -> mutableError.value = it.error 105 is RoomEvent.FailedToConnect -> mutableError.value = it.error
@@ -124,6 +131,7 @@ class CallViewModel( @@ -124,6 +131,7 @@ class CallViewModel(
124 localParticipant.setCameraEnabled(true) 131 localParticipant.setCameraEnabled(true)
125 mutableCameraEnabled.postValue(localParticipant.isCameraEnabled()) 132 mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())
126 133
  134 + // Update the speaker
127 handlePrimarySpeaker(emptyList(), emptyList(), room) 135 handlePrimarySpeaker(emptyList(), emptyList(), room)
128 } catch (e: Throwable) { 136 } catch (e: Throwable) {
129 mutableError.value = e 137 mutableError.value = e
@@ -167,6 +175,10 @@ class CallViewModel( @@ -167,6 +175,10 @@ class CallViewModel(
167 mutablePrimarySpeaker.value = speaker 175 mutablePrimarySpeaker.value = speaker
168 } 176 }
169 177
  178 + /**
  179 + * Start a screen capture with the result intent from
  180 + * [MediaProjectionManager.createScreenCaptureIntent]
  181 + */
170 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) { 182 fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
171 val localParticipant = room.localParticipant 183 val localParticipant = room.localParticipant
172 viewModelScope.launch { 184 viewModelScope.launch {
@@ -243,6 +255,7 @@ class CallViewModel( @@ -243,6 +255,7 @@ class CallViewModel(
243 room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value) 255 room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value)
244 } 256 }
245 257
  258 + // Debug functions
246 fun simulateMigration() { 259 fun simulateMigration() {
247 room.sendSimulateScenario( 260 room.sendSimulateScenario(
248 LivekitRtc.SimulateScenario.newBuilder() 261 LivekitRtc.SimulateScenario.newBuilder()
@@ -4,6 +4,7 @@ import android.app.Application @@ -4,6 +4,7 @@ import android.app.Application
4 import androidx.core.content.edit 4 import androidx.core.content.edit
5 import androidx.lifecycle.AndroidViewModel 5 import androidx.lifecycle.AndroidViewModel
6 import androidx.preference.PreferenceManager 6 import androidx.preference.PreferenceManager
  7 +import io.livekit.android.sample.common.BuildConfig
7 8
8 class MainViewModel(application: Application) : AndroidViewModel(application) { 9 class MainViewModel(application: Application) : AndroidViewModel(application) {
9 10
@@ -13,6 +13,7 @@ fun ComponentActivity.requestNeededPermissions() { @@ -13,6 +13,7 @@ fun ComponentActivity.requestNeededPermissions() {
13 registerForActivityResult( 13 registerForActivityResult(
14 ActivityResultContracts.RequestMultiplePermissions() 14 ActivityResultContracts.RequestMultiplePermissions()
15 ) { grants -> 15 ) { grants ->
  16 + // Check if any permissions weren't granted.
16 for (grant in grants.entries) { 17 for (grant in grants.entries) {
17 if (!grant.value) { 18 if (!grant.value) {
18 Toast.makeText( 19 Toast.makeText(
@@ -24,6 +25,7 @@ fun ComponentActivity.requestNeededPermissions() { @@ -24,6 +25,7 @@ fun ComponentActivity.requestNeededPermissions() {
24 } 25 }
25 } 26 }
26 } 27 }
  28 +
27 val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) 29 val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
28 .let { perms -> 30 .let { perms ->
29 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 31 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -32,12 +34,7 @@ fun ComponentActivity.requestNeededPermissions() { @@ -32,12 +34,7 @@ fun ComponentActivity.requestNeededPermissions() {
32 perms 34 perms
33 } 35 }
34 } 36 }
35 - .filter {  
36 - ContextCompat.checkSelfPermission(  
37 - this,  
38 - it  
39 - ) == PackageManager.PERMISSION_DENIED  
40 - } 37 + .filter { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_DENIED }
41 .toTypedArray() 38 .toTypedArray()
42 39
43 if (neededPermissions.isNotEmpty()) { 40 if (neededPermissions.isNotEmpty()) {
  1 +# sample-app-compose
  2 +
  3 +A sample video conferencing app for LiveKit made using Jetpack Compose.
1 plugins { 1 plugins {
2 id 'com.android.application' 2 id 'com.android.application'
3 - id 'kotlin-android' 3 + id 'org.jetbrains.kotlin.android'
4 id 'kotlin-parcelize' 4 id 'kotlin-parcelize'
5 } 5 }
6 6
@@ -32,7 +32,7 @@ android { @@ -32,7 +32,7 @@ android {
32 } 32 }
33 kotlinOptions { 33 kotlinOptions {
34 jvmTarget = java_version 34 jvmTarget = java_version
35 - freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' 35 + freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
36 } 36 }
37 buildFeatures { 37 buildFeatures {
38 compose true 38 compose true
@@ -13,7 +13,6 @@ import androidx.compose.foundation.background @@ -13,7 +13,6 @@ import androidx.compose.foundation.background
13 import androidx.compose.foundation.layout.* 13 import androidx.compose.foundation.layout.*
14 import androidx.compose.foundation.lazy.LazyRow 14 import androidx.compose.foundation.lazy.LazyRow
15 import androidx.compose.material.* 15 import androidx.compose.material.*
16 -import androidx.compose.material.ripple.rememberRipple  
17 import androidx.compose.runtime.* 16 import androidx.compose.runtime.*
18 import androidx.compose.runtime.livedata.observeAsState 17 import androidx.compose.runtime.livedata.observeAsState
19 import androidx.compose.ui.Alignment 18 import androidx.compose.ui.Alignment
@@ -217,7 +216,6 @@ class CallActivity : AppCompatActivity() { @@ -217,7 +216,6 @@ class CallActivity : AppCompatActivity() {
217 ) { 216 ) {
218 Surface( 217 Surface(
219 onClick = { viewModel.setMicEnabled(!micEnabled) }, 218 onClick = { viewModel.setMicEnabled(!micEnabled) },
220 - indication = rememberRipple(false),  
221 modifier = Modifier 219 modifier = Modifier
222 .size(controlSize) 220 .size(controlSize)
223 .padding(controlPadding) 221 .padding(controlPadding)
@@ -232,7 +230,6 @@ class CallActivity : AppCompatActivity() { @@ -232,7 +230,6 @@ class CallActivity : AppCompatActivity() {
232 } 230 }
233 Surface( 231 Surface(
234 onClick = { viewModel.setCameraEnabled(!videoEnabled) }, 232 onClick = { viewModel.setCameraEnabled(!videoEnabled) },
235 - indication = rememberRipple(false),  
236 modifier = Modifier 233 modifier = Modifier
237 .size(controlSize) 234 .size(controlSize)
238 .padding(controlPadding) 235 .padding(controlPadding)
@@ -247,7 +244,6 @@ class CallActivity : AppCompatActivity() { @@ -247,7 +244,6 @@ class CallActivity : AppCompatActivity() {
247 } 244 }
248 Surface( 245 Surface(
249 onClick = { viewModel.flipCamera() }, 246 onClick = { viewModel.flipCamera() },
250 - indication = rememberRipple(false),  
251 modifier = Modifier 247 modifier = Modifier
252 .size(controlSize) 248 .size(controlSize)
253 .padding(controlPadding) 249 .padding(controlPadding)
@@ -266,7 +262,6 @@ class CallActivity : AppCompatActivity() { @@ -266,7 +262,6 @@ class CallActivity : AppCompatActivity() {
266 viewModel.stopScreenCapture() 262 viewModel.stopScreenCapture()
267 } 263 }
268 }, 264 },
269 - indication = rememberRipple(false),  
270 modifier = Modifier 265 modifier = Modifier
271 .size(controlSize) 266 .size(controlSize)
272 .padding(controlPadding) 267 .padding(controlPadding)
@@ -284,7 +279,6 @@ class CallActivity : AppCompatActivity() { @@ -284,7 +279,6 @@ class CallActivity : AppCompatActivity() {
284 var messageToSend by remember { mutableStateOf("") } 279 var messageToSend by remember { mutableStateOf("") }
285 Surface( 280 Surface(
286 onClick = { showMessageDialog = true }, 281 onClick = { showMessageDialog = true },
287 - indication = rememberRipple(false),  
288 modifier = Modifier 282 modifier = Modifier
289 .size(controlSize) 283 .size(controlSize)
290 .padding(controlPadding) 284 .padding(controlPadding)
@@ -335,7 +329,6 @@ class CallActivity : AppCompatActivity() { @@ -335,7 +329,6 @@ class CallActivity : AppCompatActivity() {
335 } 329 }
336 Surface( 330 Surface(
337 onClick = { onExitClick() }, 331 onClick = { onExitClick() },
338 - indication = rememberRipple(false),  
339 modifier = Modifier 332 modifier = Modifier
340 .size(controlSize) 333 .size(controlSize)
341 .padding(controlPadding) 334 .padding(controlPadding)
@@ -358,7 +351,6 @@ class CallActivity : AppCompatActivity() { @@ -358,7 +351,6 @@ class CallActivity : AppCompatActivity() {
358 var showAudioDeviceDialog by remember { mutableStateOf(false) } 351 var showAudioDeviceDialog by remember { mutableStateOf(false) }
359 Surface( 352 Surface(
360 onClick = { showAudioDeviceDialog = true }, 353 onClick = { showAudioDeviceDialog = true },
361 - indication = rememberRipple(false),  
362 modifier = Modifier 354 modifier = Modifier
363 .size(controlSize) 355 .size(controlSize)
364 .padding(controlPadding) 356 .padding(controlPadding)
@@ -380,7 +372,6 @@ class CallActivity : AppCompatActivity() { @@ -380,7 +372,6 @@ class CallActivity : AppCompatActivity() {
380 } 372 }
381 Surface( 373 Surface(
382 onClick = { viewModel.toggleSubscriptionPermissions() }, 374 onClick = { viewModel.toggleSubscriptionPermissions() },
383 - indication = rememberRipple(false),  
384 modifier = Modifier 375 modifier = Modifier
385 .size(controlSize) 376 .size(controlSize)
386 .padding(controlPadding) 377 .padding(controlPadding)
@@ -397,7 +388,6 @@ class CallActivity : AppCompatActivity() { @@ -397,7 +388,6 @@ class CallActivity : AppCompatActivity() {
397 var showDebugDialog by remember { mutableStateOf(false) } 388 var showDebugDialog by remember { mutableStateOf(false) }
398 Surface( 389 Surface(
399 onClick = { showDebugDialog = true }, 390 onClick = { showDebugDialog = true },
400 - indication = rememberRipple(false),  
401 modifier = Modifier 391 modifier = Modifier
402 .size(controlSize) 392 .size(controlSize)
403 .padding(controlPadding) 393 .padding(controlPadding)
  1 +# sample-app
  2 +
  3 +A sample video conferencing app for LiveKit.
1 -apply plugin: 'com.android.application'  
2 -  
3 -apply plugin: 'kotlin-android'  
4 -apply plugin: 'kotlin-parcelize' 1 +plugins {
  2 + id 'com.android.application'
  3 + id 'org.jetbrains.kotlin.android'
  4 + id 'kotlin-parcelize'
  5 +}
5 6
6 android { 7 android {
7 compileSdkVersion androidSdk.compileVersion 8 compileSdkVersion androidSdk.compileVersion
@@ -36,16 +37,16 @@ dependencies { @@ -36,16 +37,16 @@ dependencies {
36 implementation fileTree(dir: 'libs', include: ['*.jar']) 37 implementation fileTree(dir: 'libs', include: ['*.jar'])
37 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 38 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
38 implementation deps.coroutines.lib 39 implementation deps.coroutines.lib
39 - implementation 'com.google.android.material:material:1.4.0'  
40 - implementation 'androidx.appcompat:appcompat:1.4.0' 40 + implementation 'com.google.android.material:material:1.6.1'
  41 + implementation 'androidx.appcompat:appcompat:1.4.2'
41 implementation "androidx.core:core-ktx:${versions.androidx_core}" 42 implementation "androidx.core:core-ktx:${versions.androidx_core}"
42 - implementation "androidx.activity:activity-ktx:1.4.0"  
43 - implementation 'androidx.fragment:fragment-ktx:1.3.6' 43 + implementation "androidx.activity:activity-ktx:1.5.1"
  44 + implementation 'androidx.fragment:fragment-ktx:1.5.1'
44 implementation "androidx.viewpager2:viewpager2:1.0.0" 45 implementation "androidx.viewpager2:viewpager2:1.0.0"
45 implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}" 46 implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
46 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" 47 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
47 implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" 48 implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
48 - implementation 'com.google.android.material:material:1.3.0' 49 + implementation 'com.google.android.material:material:1.6.1'
49 implementation "com.github.lisawray.groupie:groupie:${versions.groupie}" 50 implementation "com.github.lisawray.groupie:groupie:${versions.groupie}"
50 implementation "com.github.lisawray.groupie:groupie-viewbinding:${versions.groupie}" 51 implementation "com.github.lisawray.groupie:groupie-viewbinding:${versions.groupie}"
51 implementation deps.timber 52 implementation deps.timber
@@ -8,3 +8,4 @@ rootProject.name='livekit-android' @@ -8,3 +8,4 @@ rootProject.name='livekit-android'
8 include ':sample-app-common' 8 include ':sample-app-common'
9 include ':livekit-lint' 9 include ':livekit-lint'
10 include ':video-encode-decode-test' 10 include ':video-encode-decode-test'
  11 +include ':sample-app-basic'
  1 +# video-encode-decode-test
  2 +
  3 +Tests for checking various video codec capabilities in simulcast.
1 plugins { 1 plugins {
2 id 'com.android.application' 2 id 'com.android.application'
3 - id 'kotlin-android' 3 + id 'org.jetbrains.kotlin.android'
4 id 'kotlin-parcelize' 4 id 'kotlin-parcelize'
5 } 5 }
6 6
@@ -57,7 +57,7 @@ android { @@ -57,7 +57,7 @@ android {
57 } 57 }
58 kotlinOptions { 58 kotlinOptions {
59 jvmTarget = java_version 59 jvmTarget = java_version
60 - freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' 60 + freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
61 } 61 }
62 buildFeatures { 62 buildFeatures {
63 compose true 63 compose true
@@ -17,11 +17,11 @@ import androidx.constraintlayout.compose.ConstraintLayout @@ -17,11 +17,11 @@ import androidx.constraintlayout.compose.ConstraintLayout
17 import androidx.constraintlayout.compose.Dimension 17 import androidx.constraintlayout.compose.Dimension
18 import androidx.lifecycle.ViewModel 18 import androidx.lifecycle.ViewModel
19 import androidx.lifecycle.ViewModelProvider 19 import androidx.lifecycle.ViewModelProvider
  20 +import androidx.lifecycle.viewmodel.CreationExtras
20 import com.google.accompanist.pager.ExperimentalPagerApi 21 import com.google.accompanist.pager.ExperimentalPagerApi
21 import io.livekit.android.composesample.ui.theme.AppTheme 22 import io.livekit.android.composesample.ui.theme.AppTheme
22 import kotlinx.parcelize.Parcelize 23 import kotlinx.parcelize.Parcelize
23 24
24 -@OptIn(ExperimentalPagerApi::class)  
25 class CallActivity : AppCompatActivity() { 25 class CallActivity : AppCompatActivity() {
26 26
27 private lateinit var viewModel1: CallViewModel 27 private lateinit var viewModel1: CallViewModel
@@ -31,11 +31,13 @@ class CallActivity : AppCompatActivity() { @@ -31,11 +31,13 @@ class CallActivity : AppCompatActivity() {
31 override fun onCreate(savedInstanceState: Bundle?) { 31 override fun onCreate(savedInstanceState: Bundle?) {
32 super.onCreate(savedInstanceState) 32 super.onCreate(savedInstanceState)
33 33
34 - val viewModelProvider = ViewModelProvider(this, object : ViewModelProvider.KeyedFactory() {  
35 - override fun <T : ViewModel> create(key: String, modelClass: Class<T>): T { 34 + val viewModelProvider = ViewModelProvider(this, object : ViewModelProvider.Factory {
  35 + override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
  36 +
36 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) 37 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
37 ?: throw NullPointerException("args is null!") 38 ?: throw NullPointerException("args is null!")
38 39
  40 + val key = extras[ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
39 val token = if (key == VIEWMODEL_KEY1) args.token1 else args.token2 41 val token = if (key == VIEWMODEL_KEY1) args.token1 else args.token2
40 val showVideo = key == VIEWMODEL_KEY1 42 val showVideo = key == VIEWMODEL_KEY1
41 @Suppress("UNCHECKED_CAST") 43 @Suppress("UNCHECKED_CAST")