davidliu
Committed by GitHub

Compose sample (#8)

* intermediate commit

* compose sample

* inspection profiles
正在显示 46 个修改的文件 包含 1295 行增加9 行删除
  1 +<component name="InspectionProjectProfileManager">
  2 + <profile version="1.0">
  3 + <option name="myName" value="Project Default" />
  4 + <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
  5 + <option name="previewFile" value="true" />
  6 + </inspection_tool>
  7 + <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
  8 + <option name="previewFile" value="true" />
  9 + </inspection_tool>
  10 + <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
  11 + <option name="previewFile" value="true" />
  12 + </inspection_tool>
  13 + <inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
  14 + <option name="previewFile" value="true" />
  15 + </inspection_tool>
  16 + <inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
  17 + <option name="previewFile" value="true" />
  18 + </inspection_tool>
  19 + </profile>
  20 +</component>
@@ -2,5 +2,6 @@ @@ -2,5 +2,6 @@
2 <project version="4"> 2 <project version="4">
3 <component name="VcsDirectoryMappings"> 3 <component name="VcsDirectoryMappings">
4 <mapping directory="$PROJECT_DIR$" vcs="Git" /> 4 <mapping directory="$PROJECT_DIR$" vcs="Git" />
  5 + <mapping directory="$PROJECT_DIR$/protocol" vcs="Git" />
5 </component> 6 </component>
6 </project> 7 </project>
1 // Top-level build file where you can add configuration options common to all sub-projects/modules. 1 // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 2
3 buildscript { 3 buildscript {
4 - ext.kotlin_version = '1.5.21'  
5 - ext.java_version = JavaVersion.VERSION_1_8  
6 - ext.dokka_version = '1.5.0' 4 + ext {
  5 + compose_version = '1.0.3'
  6 + kotlin_version = '1.5.30'
  7 + java_version = JavaVersion.VERSION_1_8
  8 + dokka_version = '1.5.0'
  9 + }
7 repositories { 10 repositories {
8 google() 11 google()
9 mavenCentral() 12 mavenCentral()
@@ -58,6 +61,8 @@ ext { @@ -58,6 +61,8 @@ ext {
58 protoSrc: "$projectDir/protocol", 61 protoSrc: "$projectDir/protocol",
59 ] 62 ]
60 deps = [ 63 deps = [
  64 + kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2",
  65 + timber : "com.github.ajalt:timberkt:1.5.1",
61 ] 66 ]
62 annotations = [ 67 annotations = [
63 ] 68 ]
@@ -97,7 +97,7 @@ dependencies { @@ -97,7 +97,7 @@ dependencies {
97 protobuf files(generated.protoSrc) 97 protobuf files(generated.protoSrc)
98 implementation fileTree(dir: 'libs', include: ['*.jar']) 98 implementation fileTree(dir: 'libs', include: ['*.jar'])
99 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 99 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
100 - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' 100 + implementation deps.kotlinx_coroutines
101 implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0' 101 implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0'
102 api 'org.webrtc:google-webrtc:1.0.32006' 102 api 'org.webrtc:google-webrtc:1.0.32006'
103 api "com.squareup.okhttp3:okhttp:4.9.0" 103 api "com.squareup.okhttp3:okhttp:4.9.0"
@@ -107,7 +107,7 @@ dependencies { @@ -107,7 +107,7 @@ dependencies {
107 implementation 'com.google.dagger:dagger:2.38' 107 implementation 'com.google.dagger:dagger:2.38'
108 kapt 'com.google.dagger:dagger-compiler:2.38' 108 kapt 'com.google.dagger:dagger-compiler:2.38'
109 109
110 - implementation 'com.github.ajalt:timberkt:1.5.1' 110 + implementation deps.timber
111 implementation 'com.vdurmont:semver4j:3.1.0' 111 implementation 'com.vdurmont:semver4j:3.1.0'
112 112
113 testImplementation 'junit:junit:4.13.2' 113 testImplementation 'junit:junit:4.13.2'
  1 +plugins {
  2 + id 'com.android.application'
  3 + id 'kotlin-android'
  4 + id 'kotlin-parcelize'
  5 +}
  6 +
  7 +android {
  8 + compileSdkVersion 30
  9 +
  10 + defaultConfig {
  11 + applicationId "io.livekit.android.composesample"
  12 + minSdkVersion 21
  13 + targetSdkVersion 30
  14 + versionCode 1
  15 + versionName "1.0"
  16 +
  17 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  18 + vectorDrawables {
  19 + useSupportLibrary true
  20 + }
  21 + }
  22 +
  23 + buildTypes {
  24 + release {
  25 + minifyEnabled false
  26 + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  27 + }
  28 + }
  29 + compileOptions {
  30 + sourceCompatibility java_version
  31 + targetCompatibility java_version
  32 + }
  33 + kotlinOptions {
  34 + jvmTarget = java_version
  35 + freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
  36 + }
  37 + buildFeatures {
  38 + compose true
  39 + }
  40 + composeOptions {
  41 + kotlinCompilerExtensionVersion compose_version
  42 + kotlinCompilerVersion kotlin_version
  43 + }
  44 + packagingOptions {
  45 + resources {
  46 + excludes += '/META-INF/{AL2.0,LGPL2.1}'
  47 + }
  48 + }
  49 +}
  50 +
  51 +dependencies {
  52 + implementation fileTree(dir: 'libs', include: ['*.jar'])
  53 + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
  54 + implementation deps.kotlinx_coroutines
  55 + implementation 'androidx.core:core-ktx:1.6.0'
  56 + implementation 'androidx.appcompat:appcompat:1.3.1'
  57 + implementation 'com.google.android.material:material:1.4.0'
  58 + implementation "androidx.compose.ui:ui:$compose_version"
  59 + implementation "androidx.compose.material:material:$compose_version"
  60 + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
  61 + implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
  62 + implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
  63 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
  64 + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
  65 + implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
  66 + implementation 'androidx.activity:activity-compose:1.3.1'
  67 + implementation 'androidx.preference:preference-ktx:1.1.1'
  68 + implementation 'com.google.accompanist:accompanist-pager:0.19.0'
  69 + implementation 'com.google.accompanist:accompanist-pager-indicators:0.19.0'
  70 + implementation deps.timber
  71 + implementation project(":livekit-android-sdk")
  72 + testImplementation 'junit:junit:4.+'
  73 + androidTestImplementation 'androidx.test.ext:junit:1.1.3'
  74 + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
  75 + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
  76 + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
  77 +}
  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.composesample
  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.composesample", 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.composesample">
  4 +
  5 + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  6 + <uses-permission android:name="android.permission.INTERNET" />
  7 + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  8 + <uses-permission android:name="android.permission.RECORD_AUDIO" />
  9 + <uses-permission android:name="android.permission.CAMERA" />
  10 +
  11 + <application
  12 + android:allowBackup="true"
  13 + android:name=".SampleApplication"
  14 + android:fullBackupContent="true"
  15 + android:icon="@mipmap/ic_launcher"
  16 + android:label="@string/app_name"
  17 + android:roundIcon="@mipmap/ic_launcher_round"
  18 + android:networkSecurityConfig="@xml/network_security_config"
  19 + android:supportsRtl="true"
  20 + android:theme="@style/Theme.Livekitandroid">
  21 + <activity
  22 + android:name=".MainActivity"
  23 + android:exported="true"
  24 + android:label="@string/app_name"
  25 + android:theme="@style/Theme.Livekitandroid.NoActionBar">
  26 + <intent-filter>
  27 + <action android:name="android.intent.action.MAIN" />
  28 +
  29 + <category android:name="android.intent.category.LAUNCHER" />
  30 + </intent-filter>
  31 + </activity>
  32 + <activity
  33 + android:name=".CallActivity"
  34 + android:theme="@style/Theme.Livekitandroid.NoActionBar" />
  35 + </application>
  36 +
  37 +</manifest>
  1 +package io.livekit.android.composesample
  2 +
  3 +import android.media.AudioManager
  4 +import android.os.Bundle
  5 +import android.os.Parcelable
  6 +import androidx.activity.compose.setContent
  7 +import androidx.appcompat.app.AppCompatActivity
  8 +import androidx.compose.foundation.background
  9 +import androidx.compose.foundation.layout.*
  10 +import androidx.compose.material.*
  11 +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
  12 +import androidx.compose.runtime.*
  13 +import androidx.compose.runtime.livedata.observeAsState
  14 +import androidx.compose.ui.Alignment
  15 +import androidx.compose.ui.Modifier
  16 +import androidx.compose.ui.graphics.Color
  17 +import androidx.compose.ui.res.painterResource
  18 +import androidx.compose.ui.tooling.preview.Preview
  19 +import androidx.compose.ui.unit.dp
  20 +import androidx.compose.ui.viewinterop.AndroidView
  21 +import androidx.constraintlayout.compose.ConstraintLayout
  22 +import androidx.constraintlayout.compose.Dimension
  23 +import com.github.ajalt.timberkt.Timber
  24 +import com.google.accompanist.pager.ExperimentalPagerApi
  25 +import com.google.accompanist.pager.HorizontalPager
  26 +import com.google.accompanist.pager.rememberPagerState
  27 +import io.livekit.android.composesample.ui.theme.AppTheme
  28 +import io.livekit.android.renderer.TextureViewRenderer
  29 +import io.livekit.android.room.Room
  30 +import io.livekit.android.room.participant.RemoteParticipant
  31 +import io.livekit.android.room.track.LocalVideoTrack
  32 +import kotlinx.parcelize.Parcelize
  33 +
  34 +@OptIn(ExperimentalPagerApi::class)
  35 +class CallActivity : AppCompatActivity() {
  36 +
  37 + private val viewModel: CallViewModel by viewModelByFactory {
  38 + val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
  39 + ?: throw NullPointerException("args is null!")
  40 + CallViewModel(args.url, args.token, application)
  41 + }
  42 + private val focusChangeListener = AudioManager.OnAudioFocusChangeListener {}
  43 +
  44 + override fun onCreate(savedInstanceState: Bundle?) {
  45 + super.onCreate(savedInstanceState)
  46 +
  47 + val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
  48 + with(audioManager) {
  49 + isSpeakerphoneOn = true
  50 + isMicrophoneMute = false
  51 + mode = AudioManager.MODE_IN_COMMUNICATION
  52 + }
  53 + val result = audioManager.requestAudioFocus(
  54 + focusChangeListener,
  55 + AudioManager.STREAM_VOICE_CALL,
  56 + AudioManager.AUDIOFOCUS_GAIN,
  57 + )
  58 + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
  59 + Timber.v { "Audio focus request granted for VOICE_CALL streams" }
  60 + } else {
  61 + Timber.v { "Audio focus request failed" }
  62 + }
  63 +
  64 + setContent {
  65 + AppTheme(darkTheme = true) {
  66 + val room by viewModel.room.observeAsState()
  67 + val participants by viewModel.remoteParticipants.observeAsState(emptyList())
  68 + val micEnabled by viewModel.micEnabled.observeAsState(true)
  69 + val videoEnabled by viewModel.videoEnabled.observeAsState(true)
  70 + val flipButtonEnabled by viewModel.flipButtonVideoEnabled.observeAsState(true)
  71 + Content(
  72 + room,
  73 + participants,
  74 + micEnabled,
  75 + videoEnabled,
  76 + flipButtonEnabled
  77 + )
  78 + }
  79 + }
  80 + }
  81 +
  82 + @Preview(showBackground = true, showSystemUi = true)
  83 + @Composable
  84 + fun Content(
  85 + room: Room? = null,
  86 + participants: List<RemoteParticipant> = emptyList(),
  87 + micEnabled: Boolean = true,
  88 + videoEnabled: Boolean = true,
  89 + flipButtonEnabled: Boolean = true,
  90 + ) {
  91 + ConstraintLayout(
  92 + modifier = Modifier
  93 + .fillMaxSize()
  94 + .background(MaterialTheme.colors.background)
  95 + ) {
  96 + val (tabRow, pager, buttonBar, cameraView) = createRefs()
  97 +
  98 + if (participants.isNotEmpty()) {
  99 + val pagerState = rememberPagerState()
  100 + ScrollableTabRow(
  101 + // Our selected tab is our current page
  102 + selectedTabIndex = pagerState.currentPage,
  103 + // Override the indicator, using the provided pagerTabIndicatorOffset modifier
  104 + indicator = { tabPositions ->
  105 + TabRowDefaults.Indicator(
  106 + modifier = Modifier
  107 + .height(1.dp)
  108 + .tabIndicatorOffset(tabPositions[pagerState.currentPage]),
  109 + height = 1.dp,
  110 + color = Color.Gray
  111 + )
  112 + },
  113 + modifier = Modifier
  114 + .background(Color.DarkGray)
  115 + .constrainAs(tabRow) {
  116 + top.linkTo(parent.top)
  117 + width = Dimension.fillToConstraints
  118 + }
  119 + ) {
  120 + // Add tabs for all of our pages
  121 + participants.forEachIndexed { index, participant ->
  122 + Tab(
  123 + text = { Text(participant.identity ?: "Unnamed $index") },
  124 + selected = pagerState.currentPage == index,
  125 + onClick = { /* TODO*/ },
  126 + )
  127 + }
  128 + }
  129 + HorizontalPager(
  130 + count = participants.size,
  131 + state = pagerState,
  132 + modifier = Modifier
  133 + .constrainAs(pager) {
  134 + top.linkTo(tabRow.bottom)
  135 + bottom.linkTo(buttonBar.top)
  136 + start.linkTo(parent.start)
  137 + end.linkTo(parent.end)
  138 + width = Dimension.fillToConstraints
  139 + height = Dimension.fillToConstraints
  140 + }
  141 + ) { index ->
  142 + if (room != null) {
  143 + ParticipantItem(room = room, participant = participants[index])
  144 + }
  145 + }
  146 + }
  147 +
  148 + if (room != null) {
  149 + var videoNeedsSetup by remember { mutableStateOf(true) }
  150 + AndroidView(
  151 + factory = { context ->
  152 + TextureViewRenderer(context).apply {
  153 + room.initVideoRenderer(this)
  154 + }
  155 + },
  156 + modifier = Modifier
  157 + .width(200.dp)
  158 + .height(200.dp)
  159 + .padding(bottom = 10.dp, end = 10.dp)
  160 + .background(Color.Black)
  161 + .constrainAs(cameraView) {
  162 + bottom.linkTo(buttonBar.top)
  163 + end.linkTo(parent.end)
  164 + },
  165 + update = { view ->
  166 + val videoTrack = room.localParticipant.videoTracks.values
  167 + .firstOrNull()
  168 + ?.track as? LocalVideoTrack
  169 +
  170 + if (videoNeedsSetup) {
  171 + videoTrack?.addRenderer(view)
  172 + videoNeedsSetup = false
  173 + }
  174 + }
  175 + )
  176 + }
  177 + Row(
  178 + modifier = Modifier
  179 + .padding(top = 10.dp, bottom = 20.dp)
  180 + .fillMaxWidth()
  181 + .constrainAs(buttonBar) {
  182 + bottom.linkTo(parent.bottom)
  183 + width = Dimension.fillToConstraints
  184 + },
  185 + horizontalArrangement = Arrangement.SpaceEvenly,
  186 + verticalAlignment = Alignment.Bottom,
  187 + ) {
  188 + FloatingActionButton(
  189 + onClick = { viewModel.setMicEnabled(!micEnabled) },
  190 + backgroundColor = Color.DarkGray,
  191 + ) {
  192 + val resource =
  193 + if (micEnabled) R.drawable.outline_mic_24 else R.drawable.outline_mic_off_24
  194 + Icon(
  195 + painterResource(id = resource),
  196 + contentDescription = "Mic",
  197 + tint = Color.White,
  198 + )
  199 + }
  200 + FloatingActionButton(
  201 + onClick = { viewModel.setVideoEnabled(!videoEnabled) },
  202 + backgroundColor = Color.DarkGray,
  203 + ) {
  204 + val resource =
  205 + if (videoEnabled) R.drawable.outline_videocam_24 else R.drawable.outline_videocam_off_24
  206 + Icon(
  207 + painterResource(id = resource),
  208 + contentDescription = "Video",
  209 + tint = Color.White,
  210 + )
  211 + }
  212 + FloatingActionButton(
  213 + onClick = { viewModel.flipVideo() },
  214 + backgroundColor = Color.DarkGray,
  215 + ) {
  216 + Icon(
  217 + painterResource(id = R.drawable.outline_flip_camera_android_24),
  218 + contentDescription = "Flip Camera",
  219 + tint = Color.White,
  220 + )
  221 + }
  222 + }
  223 + }
  224 + }
  225 +
  226 + override fun onDestroy() {
  227 + super.onDestroy()
  228 +
  229 + val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
  230 + with(audioManager) {
  231 + isSpeakerphoneOn = false
  232 + isMicrophoneMute = true
  233 + abandonAudioFocus(focusChangeListener)
  234 + mode = AudioManager.MODE_NORMAL
  235 + }
  236 + }
  237 +
  238 + companion object {
  239 + const val KEY_ARGS = "args"
  240 + }
  241 +
  242 + @Parcelize
  243 + data class BundleArgs(val url: String, val token: String) : Parcelable
  244 +}
  1 +package io.livekit.android.composesample
  2 +
  3 +import android.app.Application
  4 +import androidx.lifecycle.AndroidViewModel
  5 +import androidx.lifecycle.LiveData
  6 +import androidx.lifecycle.MutableLiveData
  7 +import androidx.lifecycle.viewModelScope
  8 +import com.github.ajalt.timberkt.Timber
  9 +import io.livekit.android.ConnectOptions
  10 +import io.livekit.android.LiveKit
  11 +import io.livekit.android.room.Room
  12 +import io.livekit.android.room.RoomListener
  13 +import io.livekit.android.room.participant.Participant
  14 +import io.livekit.android.room.participant.RemoteParticipant
  15 +import io.livekit.android.room.track.LocalAudioTrack
  16 +import io.livekit.android.room.track.LocalVideoTrack
  17 +import kotlinx.coroutines.launch
  18 +
  19 +class CallViewModel(
  20 + val url: String,
  21 + val token: String,
  22 + application: Application
  23 +) : AndroidViewModel(application), RoomListener {
  24 + private val mutableRoom = MutableLiveData<Room>()
  25 + val room: LiveData<Room> = mutableRoom
  26 + private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
  27 + val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
  28 +
  29 + private var localAudioTrack: LocalAudioTrack? = null
  30 + private var localVideoTrack: LocalVideoTrack? = null
  31 +
  32 + private val mutableMicEnabled = MutableLiveData(true)
  33 + val micEnabled = mutableMicEnabled.hide()
  34 +
  35 + private val mutableVideoEnabled = MutableLiveData(true)
  36 + val videoEnabled = mutableVideoEnabled.hide()
  37 +
  38 + private val mutableFlipVideoButtonEnabled = MutableLiveData(true)
  39 + val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide()
  40 +
  41 + init {
  42 + viewModelScope.launch {
  43 + val room = LiveKit.connect(
  44 + application,
  45 + url,
  46 + token,
  47 + ConnectOptions(),
  48 + this@CallViewModel
  49 + )
  50 +
  51 + val localParticipant = room.localParticipant
  52 + val audioTrack = localParticipant.createAudioTrack()
  53 + localParticipant.publishAudioTrack(audioTrack)
  54 + this@CallViewModel.localAudioTrack = audioTrack
  55 + mutableMicEnabled.postValue(audioTrack.enabled)
  56 +
  57 + val videoTrack = localParticipant.createVideoTrack()
  58 + localParticipant.publishVideoTrack(videoTrack)
  59 + videoTrack.startCapture()
  60 + this@CallViewModel.localVideoTrack = videoTrack
  61 + mutableVideoEnabled.postValue(videoTrack.enabled)
  62 +
  63 + updateParticipants(room)
  64 + mutableRoom.value = room
  65 + }
  66 + }
  67 +
  68 + private fun updateParticipants(room: Room) {
  69 + mutableRemoteParticipants.postValue(
  70 + room.remoteParticipants
  71 + .keys
  72 + .sortedBy { it }
  73 + .mapNotNull { room.remoteParticipants[it] }
  74 + )
  75 + }
  76 +
  77 + override fun onCleared() {
  78 + super.onCleared()
  79 + mutableRoom.value?.disconnect()
  80 + }
  81 +
  82 + override fun onDisconnect(room: Room, error: Exception?) {
  83 + }
  84 +
  85 + override fun onParticipantConnected(
  86 + room: Room,
  87 + participant: RemoteParticipant
  88 + ) {
  89 + updateParticipants(room)
  90 + }
  91 +
  92 + override fun onParticipantDisconnected(
  93 + room: Room,
  94 + participant: RemoteParticipant
  95 + ) {
  96 + updateParticipants(room)
  97 + }
  98 +
  99 + override fun onFailedToConnect(room: Room, error: Exception) {
  100 + }
  101 +
  102 + override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
  103 + Timber.i { "active speakers changed ${speakers.count()}" }
  104 + }
  105 +
  106 + override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {
  107 + Timber.i { "Participant metadata changed: ${participant.identity}" }
  108 + }
  109 +
  110 + fun setMicEnabled(enabled: Boolean) {
  111 + localAudioTrack?.enabled = enabled
  112 + mutableMicEnabled.postValue(enabled)
  113 + }
  114 +
  115 + fun setVideoEnabled(enabled: Boolean) {
  116 + localVideoTrack?.enabled = enabled
  117 + mutableVideoEnabled.postValue(enabled)
  118 + }
  119 +
  120 + fun flipVideo() {
  121 + // TODO
  122 + }
  123 +}
  124 +
  125 +private fun <T> LiveData<T>.hide(): LiveData<T> = this
  1 +package io.livekit.android.composesample
  2 +
  3 +import android.Manifest
  4 +import android.content.Intent
  5 +import android.content.pm.PackageManager
  6 +import android.os.Bundle
  7 +import android.text.SpannableStringBuilder
  8 +import android.widget.Toast
  9 +import androidx.activity.ComponentActivity
  10 +import androidx.activity.compose.setContent
  11 +import androidx.activity.result.contract.ActivityResultContracts
  12 +import androidx.compose.foundation.layout.*
  13 +import androidx.compose.material.*
  14 +import androidx.compose.runtime.*
  15 +import androidx.compose.ui.Alignment
  16 +import androidx.compose.ui.Modifier
  17 +import androidx.compose.ui.tooling.preview.Preview
  18 +import androidx.compose.ui.unit.dp
  19 +import androidx.core.content.ContextCompat
  20 +import androidx.core.content.edit
  21 +import androidx.preference.PreferenceManager
  22 +import com.google.accompanist.pager.ExperimentalPagerApi
  23 +import io.livekit.android.composesample.ui.theme.AppTheme
  24 +
  25 +@ExperimentalPagerApi
  26 +class MainActivity : ComponentActivity() {
  27 + override fun onCreate(savedInstanceState: Bundle?) {
  28 + super.onCreate(savedInstanceState)
  29 +
  30 + requestPermissions()
  31 + val preferences = PreferenceManager.getDefaultSharedPreferences(this)
  32 + val defaultUrl = preferences.getString(PREFERENCES_KEY_URL, URL) as String
  33 + val defaultToken = preferences.getString(PREFERENCES_KEY_TOKEN, TOKEN) as String
  34 + setContent {
  35 + MainContent(
  36 + defaultUrl = defaultUrl,
  37 + defaultToken = defaultToken,
  38 + onConnect = { url, token ->
  39 + val intent = Intent(this@MainActivity, CallActivity::class.java).apply {
  40 + putExtra(
  41 + CallActivity.KEY_ARGS,
  42 + CallActivity.BundleArgs(
  43 + url,
  44 + token
  45 + )
  46 + )
  47 + }
  48 + startActivity(intent)
  49 + },
  50 + onSave = { url, token ->
  51 + preferences.edit {
  52 + putString(PREFERENCES_KEY_URL, url)
  53 + putString(PREFERENCES_KEY_TOKEN, token)
  54 + }
  55 +
  56 + Toast.makeText(
  57 + this@MainActivity,
  58 + "Values saved.",
  59 + Toast.LENGTH_SHORT
  60 + ).show()
  61 + },
  62 + onReset = {
  63 + preferences.edit {
  64 + clear()
  65 + }
  66 + Toast.makeText(
  67 + this@MainActivity,
  68 + "Values reset.",
  69 + Toast.LENGTH_SHORT
  70 + ).show()
  71 + }
  72 + )
  73 + }
  74 + }
  75 +
  76 + @Preview(
  77 + showBackground = true,
  78 + showSystemUi = true,
  79 + )
  80 + @Composable
  81 + fun MainContent(
  82 + defaultUrl: String = URL,
  83 + defaultToken: String = TOKEN,
  84 + onConnect: (url: String, token: String) -> Unit = { _, _ -> },
  85 + onSave: (url: String, token: String) -> Unit = { _, _ -> },
  86 + onReset: () -> Unit = {},
  87 + ) {
  88 + AppTheme {
  89 + var url by remember { mutableStateOf(defaultUrl) }
  90 + var token by remember { mutableStateOf(defaultToken) }
  91 + // A surface container using the 'background' color from the theme
  92 + Surface(color = MaterialTheme.colors.background) {
  93 + Column(
  94 + horizontalAlignment = Alignment.CenterHorizontally,
  95 + modifier = Modifier.padding(10.dp)
  96 + ) {
  97 + OutlinedTextField(
  98 + value = url,
  99 + onValueChange = { url = it },
  100 + label = { Text("URL") },
  101 + modifier = Modifier.fillMaxWidth(),
  102 + )
  103 + Spacer(modifier = Modifier.height(20.dp))
  104 + OutlinedTextField(
  105 + value = token,
  106 + onValueChange = { token = it },
  107 + label = { Text("Token") },
  108 + modifier = Modifier.fillMaxWidth(),
  109 + )
  110 +
  111 + Spacer(modifier = Modifier.height(20.dp))
  112 + Button(onClick = { onConnect(url, token) }) {
  113 + Text("Connect")
  114 + }
  115 +
  116 + Spacer(modifier = Modifier.height(20.dp))
  117 + Button(onClick = { onSave(url, token) }) {
  118 + Text("Save Values")
  119 + }
  120 +
  121 + Spacer(modifier = Modifier.height(20.dp))
  122 + Button(onClick = {
  123 + url = URL
  124 + token = TOKEN
  125 + onReset()
  126 + }) {
  127 + Text("Reset Values")
  128 + }
  129 + }
  130 + }
  131 + }
  132 + }
  133 +
  134 + private fun requestPermissions() {
  135 + val requestPermissionLauncher =
  136 + registerForActivityResult(
  137 + ActivityResultContracts.RequestMultiplePermissions()
  138 + ) { grants ->
  139 + for (grant in grants.entries) {
  140 + if (!grant.value) {
  141 + Toast.makeText(
  142 + this,
  143 + "Missing permission: ${grant.key}",
  144 + Toast.LENGTH_SHORT
  145 + )
  146 + .show()
  147 + }
  148 + }
  149 + }
  150 + val neededPermissions = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
  151 + .filter {
  152 + ContextCompat.checkSelfPermission(
  153 + this,
  154 + it
  155 + ) == PackageManager.PERMISSION_DENIED
  156 + }
  157 + .toTypedArray()
  158 + if (neededPermissions.isNotEmpty()) {
  159 + requestPermissionLauncher.launch(neededPermissions)
  160 + }
  161 + }
  162 +
  163 + companion object {
  164 + const val PREFERENCES_KEY_URL = "url"
  165 + const val PREFERENCES_KEY_TOKEN = "token"
  166 +
  167 + const val URL = "wss://livekit.watercooler.fm"
  168 + const val TOKEN =
  169 + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5ODQyMzE0OTgsImlzcyI6IkFQSU1teGlMOHJxdUt6dFpFb1pKVjlGYiIsImp0aSI6ImZvcnRoIiwibmJmIjoxNjI0MjMxNDk4LCJ2aWRlbyI6eyJyb29tIjoibXlyb29tIiwicm9vbUpvaW4iOnRydWV9fQ.PVx_lXAIGxcD2VRslosrbkigc777GXbu-DQME8hjJKI"
  170 + }
  171 +}
  1 +package io.livekit.android.composesample
  2 +
  3 +import androidx.compose.foundation.layout.fillMaxSize
  4 +import androidx.compose.runtime.*
  5 +import androidx.compose.ui.Modifier
  6 +import androidx.compose.ui.viewinterop.AndroidView
  7 +import com.github.ajalt.timberkt.Timber
  8 +import io.livekit.android.renderer.TextureViewRenderer
  9 +import io.livekit.android.room.Room
  10 +import io.livekit.android.room.participant.ParticipantListener
  11 +import io.livekit.android.room.participant.RemoteParticipant
  12 +import io.livekit.android.room.track.RemoteTrackPublication
  13 +import io.livekit.android.room.track.Track
  14 +import io.livekit.android.room.track.VideoTrack
  15 +
  16 +@Composable
  17 +fun ParticipantItem(
  18 + room: Room,
  19 + participant: RemoteParticipant,
  20 +) {
  21 + var videoBound by remember(room, participant) { mutableStateOf(false) }
  22 + fun getVideoTrack(): VideoTrack? {
  23 + return participant
  24 + .videoTracks.values
  25 + .firstOrNull()?.track as? VideoTrack
  26 + }
  27 +
  28 + fun setupVideoIfNeeded(videoTrack: VideoTrack, view: TextureViewRenderer) {
  29 + if (videoBound) {
  30 + return
  31 + }
  32 +
  33 + videoBound = true
  34 + Timber.v { "adding renderer to $videoTrack" }
  35 + videoTrack.addRenderer(view)
  36 + }
  37 +
  38 + AndroidView(
  39 + factory = { context ->
  40 + TextureViewRenderer(context).apply {
  41 + room.initVideoRenderer(this)
  42 +
  43 + }
  44 + },
  45 + modifier = Modifier.fillMaxSize(),
  46 + update = { view ->
  47 + participant.listener = object : ParticipantListener {
  48 + override fun onTrackSubscribed(
  49 + track: Track,
  50 + publication: RemoteTrackPublication,
  51 + participant: RemoteParticipant
  52 + ) {
  53 + if (track is VideoTrack) {
  54 + setupVideoIfNeeded(track, view)
  55 + }
  56 + }
  57 +
  58 + override fun onTrackUnpublished(
  59 + publication: RemoteTrackPublication,
  60 + participant: RemoteParticipant
  61 + ) {
  62 + super.onTrackUnpublished(publication, participant)
  63 + Timber.e { "Track unpublished" }
  64 + }
  65 + }
  66 + val existingTrack = getVideoTrack()
  67 + if (existingTrack != null) {
  68 + setupVideoIfNeeded(existingTrack, view)
  69 + }
  70 + }
  71 + )
  72 +}
  1 +package io.livekit.android.composesample
  2 +
  3 +import android.app.Application
  4 +import timber.log.Timber
  5 +
  6 +class SampleApplication : Application() {
  7 +
  8 + override fun onCreate() {
  9 + super.onCreate()
  10 + Timber.plant(Timber.DebugTree())
  11 + }
  12 +}
  1 +package io.livekit.android.composesample
  2 +
  3 +import androidx.activity.viewModels
  4 +import androidx.fragment.app.FragmentActivity
  5 +import androidx.lifecycle.ViewModel
  6 +import androidx.lifecycle.ViewModelProvider
  7 +
  8 +typealias CreateViewModel<VM> = () -> VM
  9 +
  10 +inline fun <reified VM : ViewModel> FragmentActivity.viewModelByFactory(
  11 + noinline create: CreateViewModel<VM>
  12 +): Lazy<VM> {
  13 + return viewModels {
  14 + createViewModelFactoryFactory(create)
  15 + }
  16 +}
  17 +
  18 +fun <VM> createViewModelFactoryFactory(
  19 + create: CreateViewModel<VM>
  20 +): ViewModelProvider.Factory {
  21 + return object : ViewModelProvider.Factory {
  22 + override fun <T : ViewModel?> create(modelClass: Class<T>): T {
  23 + @Suppress("UNCHECKED_CAST")
  24 + return create() as? T
  25 + ?: throw IllegalArgumentException("Unknown viewmodel class!")
  26 + }
  27 + }
  28 +}
  1 +package io.livekit.android.composesample.ui.theme
  2 +
  3 +import androidx.compose.ui.graphics.Color
  4 +
  5 +val Purple200 = Color(0xFFBB86FC)
  6 +val Purple500 = Color(0xFF6200EE)
  7 +val Purple700 = Color(0xFF3700B3)
  8 +val Teal200 = Color(0xFF03DAC5)
  1 +package io.livekit.android.composesample.ui.theme
  2 +
  3 +import androidx.compose.foundation.shape.RoundedCornerShape
  4 +import androidx.compose.material.Shapes
  5 +import androidx.compose.ui.unit.dp
  6 +
  7 +val Shapes = Shapes(
  8 + small = RoundedCornerShape(4.dp),
  9 + medium = RoundedCornerShape(4.dp),
  10 + large = RoundedCornerShape(0.dp)
  11 +)
  1 +package io.livekit.android.composesample.ui.theme
  2 +
  3 +import androidx.compose.foundation.isSystemInDarkTheme
  4 +import androidx.compose.material.MaterialTheme
  5 +import androidx.compose.material.darkColors
  6 +import androidx.compose.material.lightColors
  7 +import androidx.compose.runtime.Composable
  8 +import androidx.compose.ui.graphics.Color
  9 +
  10 +private val DarkColorPalette = darkColors(
  11 + primary = Purple200,
  12 + primaryVariant = Purple700,
  13 + secondary = Teal200,
  14 + background = Color.Black
  15 +)
  16 +
  17 +private val LightColorPalette = lightColors(
  18 + primary = Purple500,
  19 + primaryVariant = Purple700,
  20 + secondary = Teal200
  21 +
  22 + /* Other default colors to override
  23 + background = Color.White,
  24 + surface = Color.White,
  25 + onPrimary = Color.White,
  26 + onSecondary = Color.Black,
  27 + onBackground = Color.Black,
  28 + onSurface = Color.Black,
  29 + */
  30 +)
  31 +
  32 +@Composable
  33 +fun AppTheme(
  34 + darkTheme: Boolean = isSystemInDarkTheme(),
  35 + content: @Composable() () -> Unit
  36 +) {
  37 + val colors = if (darkTheme) {
  38 + DarkColorPalette
  39 + } else {
  40 + LightColorPalette
  41 + }
  42 +
  43 + MaterialTheme(
  44 + colors = colors,
  45 + typography = Typography,
  46 + shapes = Shapes,
  47 + content = content
  48 + )
  49 +}
  1 +package io.livekit.android.composesample.ui.theme
  2 +
  3 +import androidx.compose.material.Typography
  4 +import androidx.compose.ui.text.TextStyle
  5 +import androidx.compose.ui.text.font.FontFamily
  6 +import androidx.compose.ui.text.font.FontWeight
  7 +import androidx.compose.ui.unit.sp
  8 +
  9 +// Set of Material typography styles to start with
  10 +val Typography = Typography(
  11 + body1 = TextStyle(
  12 + fontFamily = FontFamily.Default,
  13 + fontWeight = FontWeight.Normal,
  14 + fontSize = 16.sp
  15 + )
  16 + /* Other default text styles to override
  17 + button = TextStyle(
  18 + fontFamily = FontFamily.Default,
  19 + fontWeight = FontWeight.W500,
  20 + fontSize = 14.sp
  21 + ),
  22 + caption = TextStyle(
  23 + fontFamily = FontFamily.Default,
  24 + fontWeight = FontWeight.Normal,
  25 + fontSize = 12.sp
  26 + )
  27 + */
  28 +)
  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 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M9,12c0,1.66 1.34,3 3,3s3,-1.34 3,-3s-1.34,-3 -3,-3S9,10.34 9,12zM13,12c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1s0.45,-1 1,-1S13,11.45 13,12z"/>
  10 + <path
  11 + android:fillColor="@android:color/white"
  12 + android:pathData="M8,10V8H5.09C6.47,5.61 9.05,4 12,4c3.72,0 6.85,2.56 7.74,6h2.06c-0.93,-4.56 -4.96,-8 -9.8,-8C8.73,2 5.82,3.58 4,6.01V4H2v6H8z"/>
  13 + <path
  14 + android:fillColor="@android:color/white"
  15 + android:pathData="M16,14v2h2.91c-1.38,2.39 -3.96,4 -6.91,4c-3.72,0 -6.85,-2.56 -7.74,-6H2.2c0.93,4.56 4.96,8 9.8,8c3.27,0 6.18,-1.58 8,-4.01V20h2v-6H16z"/>
  16 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6C9,12.66 10.34,14 12,14z"/>
  10 + <path
  11 + android:fillColor="@android:color/white"
  12 + android:pathData="M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5H5c0,3.53 2.61,6.43 6,6.92V21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92H17z"/>
  13 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M10.8,4.9c0,-0.66 0.54,-1.2 1.2,-1.2s1.2,0.54 1.2,1.2l-0.01,3.91L15,10.6V5c0,-1.66 -1.34,-3 -3,-3 -1.54,0 -2.79,1.16 -2.96,2.65l1.76,1.76V4.9zM19,11h-1.7c0,0.58 -0.1,1.13 -0.27,1.64l1.27,1.27c0.44,-0.88 0.7,-1.87 0.7,-2.91zM4.41,2.86L3,4.27l6,6V11c0,1.66 1.34,3 3,3 0.23,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c0.91,-0.13 1.77,-0.45 2.55,-0.9l4.2,4.2 1.41,-1.41L4.41,2.86z"/>
  10 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M15,8v8H5V8h10m1,-2H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4V7c0,-0.55 -0.45,-1 -1,-1z"/>
  10 +</vector>
  1 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
  2 + android:width="24dp"
  3 + android:height="24dp"
  4 + android:viewportWidth="24"
  5 + android:viewportHeight="24"
  6 + android:tint="?attr/colorControlNormal">
  7 + <path
  8 + android:fillColor="@android:color/white"
  9 + android:pathData="M9.56,8l-2,-2 -4.15,-4.14L2,3.27 4.73,6L4,6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.55,-0.18L19.73,21l1.41,-1.41 -8.86,-8.86L9.56,8zM5,16L5,8h1.73l8,8L5,16zM15,8v2.61l6,6L21,6.5l-4,4L17,7c0,-0.55 -0.45,-1 -1,-1h-5.61l2,2L15,8z"/>
  10 +</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 +<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 Compose</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 +
  17 + <style name="Theme.Livekitandroid.NoActionBar">
  18 + <item name="windowActionBar">false</item>
  19 + <item name="windowNoTitle">true</item>
  20 + </style>
  21 +
  22 + <style name="Theme.Livekitandroid.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
  23 +
  24 + <style name="Theme.Livekitandroid.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
  25 +</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.composesample
  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 +}
@@ -35,22 +35,22 @@ android { @@ -35,22 +35,22 @@ android {
35 dependencies { 35 dependencies {
36 implementation fileTree(dir: 'libs', include: ['*.jar']) 36 implementation fileTree(dir: 'libs', include: ['*.jar'])
37 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 37 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
38 - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' 38 + implementation deps.kotlinx_coroutines
39 implementation 'com.google.android.material:material:1.3.0' 39 implementation 'com.google.android.material:material:1.3.0'
40 implementation 'androidx.appcompat:appcompat:1.2.0' 40 implementation 'androidx.appcompat:appcompat:1.2.0'
41 implementation 'androidx.core:core-ktx:1.3.2' 41 implementation 'androidx.core:core-ktx:1.3.2'
42 implementation "androidx.activity:activity-ktx:1.2.2" 42 implementation "androidx.activity:activity-ktx:1.2.2"
43 implementation 'androidx.fragment:fragment-ktx:1.3.2' 43 implementation 'androidx.fragment:fragment-ktx:1.3.2'
44 - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'  
45 implementation 'androidx.preference:preference:1.1.1' 44 implementation 'androidx.preference:preference:1.1.1'
46 implementation "androidx.viewpager2:viewpager2:1.0.0" 45 implementation "androidx.viewpager2:viewpager2:1.0.0"
  46 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidx_lifecycle}"
47 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" 47 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}"
48 implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" 48 implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}"
49 implementation 'com.google.android.material:material:1.3.0' 49 implementation 'com.google.android.material:material:1.3.0'
50 implementation "com.xwray:groupie:${versions.groupie}" 50 implementation "com.xwray:groupie:${versions.groupie}"
51 implementation "com.xwray:groupie-viewbinding:${versions.groupie}" 51 implementation "com.xwray:groupie-viewbinding:${versions.groupie}"
52 implementation 'com.snakydesign.livedataextensions:lives:1.3.0' 52 implementation 'com.snakydesign.livedataextensions:lives:1.3.0'
53 - implementation 'com.github.ajalt:timberkt:1.5.1' 53 + implementation deps.timber
54 implementation project(":livekit-android-sdk") 54 implementation project(":livekit-android-sdk")
55 testImplementation 'junit:junit:4.12' 55 testImplementation 'junit:junit:4.12'
56 androidTestImplementation 'androidx.test.ext:junit:1.1.2' 56 androidTestImplementation 'androidx.test.ext:junit:1.1.2'
@@ -4,5 +4,5 @@ pluginManagement { @@ -4,5 +4,5 @@ pluginManagement {
4 jcenter() 4 jcenter()
5 } 5 }
6 } 6 }
7 -include ':sample-app', ':livekit-android-sdk' 7 +include ':sample-app', ':sample-app-compose', ':livekit-android-sdk'
8 rootProject.name='livekit-android' 8 rootProject.name='livekit-android'