Committed by
GitHub
Compose sample (#8)
* intermediate commit * compose sample * inspection profiles
正在显示
46 个修改的文件
包含
1295 行增加
和
9 行删除
.idea/inspectionProfiles/Project_Default.xml
0 → 100644
| 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' |
sample-app-compose/.gitignore
0 → 100644
| 1 | +/build |
sample-app-compose/build.gradle
0 → 100644
| 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 | +} |
sample-app-compose/proguard-rules.pro
0 → 100644
| 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 |
sample-app-compose/src/androidTest/java/io/livekit/android/composesample/ExampleInstrumentedTest.kt
0 → 100644
| 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 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.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 | +<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 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' |
-
请 注册 或 登录 后发表评论