├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── daily-bots-android-demo ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── co │ │ └── daily │ │ └── bots │ │ └── demo │ │ ├── ConfigConstants.kt │ │ ├── MainActivity.kt │ │ ├── Preferences.kt │ │ ├── RTVIApplication.kt │ │ ├── VoiceClientManager.kt │ │ ├── ui │ │ ├── AudioIndicator.kt │ │ ├── BotIndicator.kt │ │ ├── InCallFooter.kt │ │ ├── InCallHeader.kt │ │ ├── InCallLayout.kt │ │ ├── Logo.kt │ │ ├── PermissionScreen.kt │ │ ├── Timer.kt │ │ ├── UserCamButton.kt │ │ ├── UserMicButton.kt │ │ ├── VoiceClientSettingsPanel.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── utils │ │ ├── RealTimeClock.kt │ │ └── TimeUtils.kt │ └── res │ ├── drawable │ ├── chevron_down.xml │ ├── chevron_right.xml │ ├── cog.xml │ ├── console.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── logo.png │ ├── microphone.xml │ ├── microphone_off.xml │ ├── phone_hangup.xml │ ├── timer_outline.xml │ ├── video.xml │ └── video_off.xml │ ├── font │ └── inter.ttf │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ ├── strings.xml │ └── themes.xml ├── files └── screenshot.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "**" 10 | workflow_dispatch: 11 | inputs: 12 | sdk_git_ref: 13 | type: string 14 | description: "Which git ref of the app to build" 15 | 16 | concurrency: 17 | group: build-android-${{ github.event.pull_request.number || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | sdk: 22 | name: "Demo" 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout repo 26 | uses: actions/checkout@v4 27 | with: 28 | ref: ${{ github.event.inputs.sdk_git_ref || github.ref }} 29 | 30 | - name: "Install Java" 31 | uses: actions/setup-java@v4 32 | with: 33 | distribution: 'temurin' 34 | java-version: '17' 35 | 36 | - name: Build demo app 37 | run: ./gradlew :daily-bots-android-demo:assembleDebug 38 | 39 | - name: Upload demo APK 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: Demo App (Debug) 43 | path: daily-bots-android-demo/build/outputs/apk/debug/daily-bots-android-demo-debug.apk 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Daily 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daily Bots Android Demo 2 | 3 | Demo app for connecting to a Daily Bots RTVI backend. 4 | 5 | screenshot 6 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.jetbrains.kotlin.android) apply false 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.compose.compiler) apply false 5 | } 6 | -------------------------------------------------------------------------------- /daily-bots-android-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /daily-bots-android-demo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.jetbrains.kotlin.serialization) 5 | alias(libs.plugins.compose.compiler) 6 | } 7 | 8 | android { 9 | namespace = "co.daily.bots.demo" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "co.daily.bots.demo" 14 | minSdk = 26 15 | targetSdk = 35 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | vectorDrawables { 20 | useSupportLibrary = true 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | } 42 | 43 | buildFeatures { 44 | compose = true 45 | buildConfig = true 46 | } 47 | 48 | composeOptions { 49 | kotlinCompilerExtensionVersion = "1.5.1" 50 | } 51 | 52 | packaging { 53 | resources { 54 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 55 | } 56 | } 57 | } 58 | 59 | dependencies { 60 | implementation(libs.rtvi.client.daily) 61 | implementation(libs.androidx.core.ktx) 62 | implementation(libs.androidx.lifecycle.runtime.ktx) 63 | implementation(libs.androidx.activity.compose) 64 | implementation(platform(libs.androidx.compose.bom)) 65 | implementation(libs.androidx.ui) 66 | implementation(libs.androidx.ui.graphics) 67 | implementation(libs.androidx.ui.tooling.preview) 68 | implementation(libs.androidx.material3) 69 | implementation(libs.accompanist.permissions) 70 | implementation(libs.androidx.constraintlayout.compose) 71 | implementation(libs.kotlinx.serialization.json) 72 | androidTestImplementation(platform(libs.androidx.compose.bom)) 73 | debugImplementation(libs.androidx.ui.tooling) 74 | debugImplementation(libs.androidx.ui.test.manifest) 75 | } 76 | -------------------------------------------------------------------------------- /daily-bots-android-demo/proguard-rules.pro: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ConfigConstants.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | object ConfigConstants { 6 | 7 | object Together : LLMProvider { 8 | 9 | val Llama8B = 10 | "Llama 3.1 8B Instruct Turbo" isLLMModel "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo" 11 | val Llama70B = 12 | "Llama 3.1 70B Instruct Turbo" isLLMModel "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo" 13 | val Llama405B = 14 | "Llama 3.1 405B Instruct Turbo" isLLMModel "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo" 15 | 16 | override val name = "Together AI" 17 | override val id = "together" 18 | 19 | override val models = 20 | NamedOptionList(listOf(Llama8B, Llama70B, Llama405B), default = Llama70B) 21 | } 22 | 23 | object Anthropic : LLMProvider { 24 | 25 | override val name = "Anthropic" 26 | override val id = "anthropic" 27 | 28 | override val models = 29 | NamedOptionList(listOf("Claude Sonnet 3.5" isLLMModel "claude-3-5-sonnet-20240620")) 30 | } 31 | 32 | object Cartesia : TTSProvider { 33 | 34 | override val name = "Cartesia" 35 | override val id = "cartesia" 36 | 37 | override val voices = NamedOptionList( 38 | listOf( 39 | "British lady" isVoice "79a125e8-cd45-4c13-8a67-188112f4dd22", 40 | "California girl" isVoice "b7d50908-b17c-442d-ad8d-810c63997ed9", 41 | "Doctor mischief" isVoice "fb26447f-308b-471e-8b00-8e9f04284eb5", 42 | "Child" isVoice "2ee87190-8f84-4925-97da-e52547f9462c", 43 | "Merchant" isVoice "50d6beb4-80ea-4802-8387-6c948fe84208", 44 | "Kentucky man" isVoice "726d5ae5-055f-4c3d-8355-d9677de68937" 45 | ) 46 | ) 47 | } 48 | 49 | object Deepgram : STTProvider { 50 | override val name = "Deepgram" 51 | override val id = "deepgram" 52 | 53 | val English = "English" isSTTLang "en" 54 | 55 | override val models = NamedOptionList( 56 | listOf( 57 | STTOptionModel( 58 | name = "Nova 2 Conversational AI (English)", 59 | id = "nova-2-conversationalai", 60 | languages = NamedOptionList( 61 | listOf("English" isSTTLang "en") 62 | ) 63 | ), 64 | STTOptionModel( 65 | name = "Nova 2 General (Multilingual)", 66 | id = "nova-2-general", 67 | languages = NamedOptionList( 68 | listOf( 69 | "Bulgarian" isSTTLang "bg", 70 | "Catalan" isSTTLang "ca", 71 | "Chinese (Mandarin, Simplified)" isSTTLang "zh", 72 | "Chinese (Mandarin, Traditional)" isSTTLang "zh-TW", 73 | "Czech" isSTTLang "cs", 74 | "Danish" isSTTLang "da", 75 | "Danish" isSTTLang "da-DK", 76 | "Dutch" isSTTLang "nl", 77 | English, 78 | "English (US)" isSTTLang "en-US", 79 | "English (AU)" isSTTLang "en-AU", 80 | "English (GB)" isSTTLang "en-GB", 81 | "English (NZ)" isSTTLang "en-NZ", 82 | "English (IN)" isSTTLang "en-IN", 83 | "Estonian" isSTTLang "et", 84 | "Finnish" isSTTLang "fi", 85 | "Flemish" isSTTLang "nl-BE", 86 | "French" isSTTLang "fr", 87 | "French (CA)" isSTTLang "fr-CA", 88 | "German" isSTTLang "de", 89 | "German (Switzerland)" isSTTLang "de-CH", 90 | "Greek" isSTTLang "el", 91 | "Hindi" isSTTLang "hi", 92 | "Hungarian" isSTTLang "hu", 93 | "Indonesian" isSTTLang "id", 94 | "Italian" isSTTLang "it", 95 | "Japanese" isSTTLang "ja", 96 | "Korean" isSTTLang "ko", 97 | "Korean" isSTTLang "ko-KR", 98 | "Latvian" isSTTLang "lv", 99 | "Lithuanian" isSTTLang "lt", 100 | "Malay" isSTTLang "ms", 101 | "Multilingual (Spanish + English)" isSTTLang "multi", 102 | "Norwegian" isSTTLang "no", 103 | "Polish" isSTTLang "pl", 104 | "Portuguese" isSTTLang "pt", 105 | "Portuguese (BR)" isSTTLang "pt-BR", 106 | "Romanian" isSTTLang "ro", 107 | "Russian" isSTTLang "ru", 108 | "Slovak" isSTTLang "sk", 109 | "Spanish" isSTTLang "es", 110 | "Spanish (Latin America)" isSTTLang "es-419", 111 | "Swedish" isSTTLang "sv", 112 | "Swedish" isSTTLang "sv-SE", 113 | "Thai" isSTTLang "th", 114 | "Thai" isSTTLang "th-TH", 115 | "Turkish" isSTTLang "tr", 116 | "Ukrainian" isSTTLang "uk", 117 | "Vietnamese" isSTTLang "vi", 118 | ), 119 | default = English 120 | ) 121 | ), 122 | ) 123 | ) 124 | } 125 | 126 | val botProfiles = NamedOptionList( 127 | listOf( 128 | BotProfile( 129 | name = "Voice only", 130 | id = "voice_2024_10", 131 | llmProviders = NamedOptionList(listOf(Anthropic, Together), default = Together), 132 | ttsProviders = NamedOptionList(listOf(Cartesia)), 133 | sttProviders = NamedOptionList(listOf(Deepgram)) 134 | ), 135 | BotProfile( 136 | name = "Voice and vision", 137 | id = "vision_2024_10", 138 | llmProviders = NamedOptionList(listOf(Anthropic)), 139 | ttsProviders = NamedOptionList(listOf(Cartesia)), 140 | sttProviders = NamedOptionList(listOf(Deepgram)) 141 | ), 142 | ) 143 | ) 144 | } 145 | 146 | @Immutable 147 | data class NamedOptionList( 148 | val options: List, 149 | val default: E = options.first() 150 | ) { 151 | fun byIdOrDefault(id: String?) = 152 | id?.let { idNonNull -> options.firstOrNull { it.id == idNonNull } } ?: default 153 | } 154 | 155 | interface NamedOption { 156 | val name: String 157 | val id: String 158 | } 159 | 160 | interface STTProvider : NamedOption { 161 | override val name: String 162 | override val id: String 163 | val models: NamedOptionList 164 | } 165 | 166 | interface TTSProvider : NamedOption { 167 | override val name: String 168 | override val id: String 169 | val voices: NamedOptionList 170 | } 171 | 172 | interface LLMProvider : NamedOption { 173 | override val name: String 174 | override val id: String 175 | val models: NamedOptionList 176 | } 177 | 178 | data class BotProfile( 179 | override val name: String, 180 | override val id: String, 181 | val llmProviders: NamedOptionList, 182 | val ttsProviders: NamedOptionList, 183 | val sttProviders: NamedOptionList, 184 | ) : NamedOption 185 | 186 | data class STTOptionModel( 187 | override val name: String, 188 | override val id: String, 189 | val languages: NamedOptionList 190 | ) : NamedOption 191 | 192 | data class STTOptionLanguage( 193 | override val name: String, 194 | override val id: String, 195 | ) : NamedOption 196 | 197 | data class LLMOptionModel( 198 | override val name: String, 199 | override val id: String, 200 | ) : NamedOption 201 | 202 | data class TTSOptionVoice( 203 | override val name: String, 204 | override val id: String, 205 | ) : NamedOption 206 | 207 | private infix fun String.isLLMModel(id: String) = LLMOptionModel(name = this, id = id) 208 | private infix fun String.isSTTLang(id: String) = STTOptionLanguage(name = this, id = id) 209 | private infix fun String.isVoice(id: String) = TTSOptionVoice(name = this, id = id) -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.annotation.DrawableRes 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.border 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.layout.Arrangement 12 | import androidx.compose.foundation.layout.Box 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.Row 15 | import androidx.compose.foundation.layout.Spacer 16 | import androidx.compose.foundation.layout.fillMaxSize 17 | import androidx.compose.foundation.layout.fillMaxWidth 18 | import androidx.compose.foundation.layout.height 19 | import androidx.compose.foundation.layout.imePadding 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.layout.size 22 | import androidx.compose.foundation.layout.width 23 | import androidx.compose.foundation.rememberScrollState 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.foundation.text.KeyboardActions 26 | import androidx.compose.foundation.text.KeyboardOptions 27 | import androidx.compose.foundation.verticalScroll 28 | import androidx.compose.material3.AlertDialog 29 | import androidx.compose.material3.Button 30 | import androidx.compose.material3.ExperimentalMaterial3Api 31 | import androidx.compose.material3.Icon 32 | import androidx.compose.material3.ModalBottomSheet 33 | import androidx.compose.material3.Scaffold 34 | import androidx.compose.material3.Text 35 | import androidx.compose.material3.TextField 36 | import androidx.compose.material3.rememberModalBottomSheetState 37 | import androidx.compose.runtime.Composable 38 | import androidx.compose.runtime.getValue 39 | import androidx.compose.runtime.mutableStateOf 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.setValue 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.draw.clip 45 | import androidx.compose.ui.draw.shadow 46 | import androidx.compose.ui.graphics.Color 47 | import androidx.compose.ui.res.painterResource 48 | import androidx.compose.ui.text.font.FontWeight 49 | import androidx.compose.ui.text.input.ImeAction 50 | import androidx.compose.ui.text.input.KeyboardType 51 | import androidx.compose.ui.unit.dp 52 | import androidx.compose.ui.unit.sp 53 | import co.daily.bots.demo.ui.InCallLayout 54 | import co.daily.bots.demo.ui.Logo 55 | import co.daily.bots.demo.ui.PermissionScreen 56 | import co.daily.bots.demo.ui.VoiceClientSettingsPanel 57 | import co.daily.bots.demo.ui.theme.Colors 58 | import co.daily.bots.demo.ui.theme.RTVIClientTheme 59 | import co.daily.bots.demo.ui.theme.TextStyles 60 | import co.daily.bots.demo.ui.theme.textFieldColors 61 | 62 | 63 | private const val DEFAULT_BACKEND = "https://api.daily.co/v1/bots" 64 | 65 | class MainActivity : ComponentActivity() { 66 | 67 | override fun onCreate(savedInstanceState: Bundle?) { 68 | super.onCreate(savedInstanceState) 69 | enableEdgeToEdge() 70 | 71 | val voiceClientManager = VoiceClientManager(this) 72 | 73 | setContent { 74 | RTVIClientTheme { 75 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> 76 | Box( 77 | Modifier 78 | .fillMaxSize() 79 | .padding(innerPadding) 80 | ) { 81 | PermissionScreen() 82 | 83 | val vcState = voiceClientManager.state.value 84 | 85 | if (vcState != null) { 86 | InCallLayout(voiceClientManager) 87 | 88 | } else { 89 | ConnectSettings(voiceClientManager) 90 | } 91 | 92 | voiceClientManager.errors.firstOrNull()?.let { errorText -> 93 | 94 | val dismiss: () -> Unit = { voiceClientManager.errors.removeAt(0) } 95 | 96 | AlertDialog( 97 | onDismissRequest = dismiss, 98 | confirmButton = { 99 | Button(onClick = dismiss) { 100 | Text( 101 | text = "OK", 102 | fontSize = 14.sp, 103 | fontWeight = FontWeight.W700, 104 | color = Color.White, 105 | style = TextStyles.base 106 | ) 107 | } 108 | }, 109 | containerColor = Color.White, 110 | title = { 111 | Text( 112 | text = "Error", 113 | fontSize = 22.sp, 114 | fontWeight = FontWeight.W600, 115 | color = Color.Black, 116 | style = TextStyles.base 117 | ) 118 | }, 119 | text = { 120 | Text( 121 | text = errorText.message, 122 | fontSize = 16.sp, 123 | fontWeight = FontWeight.W400, 124 | color = Color.Black, 125 | style = TextStyles.base 126 | ) 127 | } 128 | ) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | @OptIn(ExperimentalMaterial3Api::class) 138 | @Composable 139 | fun ConnectSettings( 140 | voiceClientManager: VoiceClientManager, 141 | ) { 142 | val scrollState = rememberScrollState() 143 | 144 | var settingsExpanded by remember { mutableStateOf(false) } 145 | 146 | val lastInitOptions = Preferences.lastInitOptions 147 | 148 | val initOptions = lastInitOptions.value?.inflateInit() ?: VoiceClientManager.InitOptions.default() 149 | 150 | val runtimeOptions = lastInitOptions.value?.inflateRuntime(initOptions) ?: VoiceClientManager.RuntimeOptions.default() 151 | 152 | val start = { 153 | val backendUrl = Preferences.backendUrl.value 154 | val apiKey = Preferences.apiKey.value 155 | 156 | voiceClientManager.start( 157 | baseUrl = backendUrl ?: DEFAULT_BACKEND, 158 | apiKey = apiKey, 159 | initOptions = initOptions, 160 | runtimeOptions = runtimeOptions 161 | ) 162 | } 163 | 164 | Box( 165 | modifier = Modifier 166 | .fillMaxSize() 167 | .verticalScroll(scrollState) 168 | .imePadding() 169 | .padding(20.dp), 170 | contentAlignment = Alignment.Center 171 | ) { 172 | Box( 173 | Modifier 174 | .fillMaxWidth() 175 | .shadow(2.dp, RoundedCornerShape(16.dp)) 176 | .clip(RoundedCornerShape(16.dp)) 177 | .background(Colors.mainSurfaceBackground) 178 | ) { 179 | Column( 180 | Modifier 181 | .fillMaxWidth() 182 | .padding( 183 | vertical = 24.dp, 184 | horizontal = 28.dp 185 | ) 186 | ) { 187 | Box( 188 | Modifier 189 | .fillMaxWidth() 190 | .padding(top = 12.dp, bottom = 24.dp), 191 | contentAlignment = Alignment.Center 192 | ) { 193 | Logo(Modifier) 194 | } 195 | 196 | Text( 197 | modifier = Modifier.align(Alignment.CenterHorizontally), 198 | text = "Connect to an RTVI server", 199 | fontSize = 22.sp, 200 | fontWeight = FontWeight.W700, 201 | style = TextStyles.base 202 | ) 203 | 204 | Spacer(modifier = Modifier.height(36.dp)) 205 | 206 | Text( 207 | text = "Backend URL", 208 | fontSize = 16.sp, 209 | fontWeight = FontWeight.W400, 210 | style = TextStyles.base 211 | ) 212 | 213 | Spacer(modifier = Modifier.height(12.dp)) 214 | 215 | TextField( 216 | modifier = Modifier 217 | .fillMaxWidth() 218 | .border(1.dp, Colors.textFieldBorder, RoundedCornerShape(12.dp)), 219 | value = Preferences.backendUrl.value ?: DEFAULT_BACKEND, 220 | onValueChange = { Preferences.backendUrl.value = it }, 221 | keyboardOptions = KeyboardOptions( 222 | keyboardType = KeyboardType.Uri, 223 | imeAction = ImeAction.Next 224 | ), 225 | colors = textFieldColors(), 226 | shape = RoundedCornerShape(12.dp) 227 | ) 228 | 229 | Spacer(modifier = Modifier.height(18.dp)) 230 | 231 | Text( 232 | text = "Daily API key", 233 | fontSize = 16.sp, 234 | fontWeight = FontWeight.W400, 235 | style = TextStyles.base 236 | ) 237 | 238 | Spacer(modifier = Modifier.height(12.dp)) 239 | 240 | TextField( 241 | modifier = Modifier 242 | .fillMaxWidth() 243 | .border(1.dp, Colors.textFieldBorder, RoundedCornerShape(12.dp)), 244 | value = Preferences.apiKey.value ?: "", 245 | onValueChange = { Preferences.apiKey.value = it }, 246 | keyboardOptions = KeyboardOptions( 247 | keyboardType = KeyboardType.Password, 248 | imeAction = ImeAction.Go 249 | ), 250 | colors = textFieldColors(), 251 | shape = RoundedCornerShape(12.dp), 252 | keyboardActions = KeyboardActions( 253 | onDone = { start() } 254 | ) 255 | ) 256 | 257 | Spacer(modifier = Modifier.height(36.dp)) 258 | 259 | Row( 260 | modifier = Modifier.fillMaxWidth(), 261 | horizontalArrangement = Arrangement.spacedBy(16.dp) 262 | ) { 263 | 264 | ConnectDialogButton( 265 | modifier = Modifier.weight(1f), 266 | onClick = { settingsExpanded = true }, 267 | text = "Settings", 268 | foreground = Color.Black, 269 | background = Color.White, 270 | border = Colors.textFieldBorder, 271 | icon = R.drawable.cog 272 | ) 273 | 274 | ConnectDialogButton( 275 | modifier = Modifier.weight(1f), 276 | onClick = start, 277 | text = "Connect", 278 | foreground = Color.White, 279 | background = Colors.buttonNormal, 280 | border = Colors.buttonNormal 281 | ) 282 | } 283 | } 284 | } 285 | } 286 | 287 | if (settingsExpanded) { 288 | ModalBottomSheet( 289 | onDismissRequest = { settingsExpanded = false }, 290 | containerColor = Colors.activityBackground, 291 | sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) 292 | ) { 293 | VoiceClientSettingsPanel(initOptions = initOptions, runtimeOptions = runtimeOptions) 294 | } 295 | } 296 | } 297 | 298 | @Composable 299 | private fun ConnectDialogButton( 300 | onClick: () -> Unit, 301 | text: String, 302 | foreground: Color, 303 | background: Color, 304 | border: Color, 305 | modifier: Modifier = Modifier, 306 | @DrawableRes icon: Int? = null, 307 | ) { 308 | val shape = RoundedCornerShape(8.dp) 309 | 310 | Row( 311 | modifier 312 | .border(1.dp, border, shape) 313 | .clip(shape) 314 | .background(background) 315 | .clickable(onClick = onClick) 316 | .padding(vertical = 10.dp, horizontal = 24.dp), 317 | verticalAlignment = Alignment.CenterVertically, 318 | horizontalArrangement = Arrangement.Center 319 | ) { 320 | if (icon != null) { 321 | Icon( 322 | modifier = Modifier.size(24.dp), 323 | painter = painterResource(icon), 324 | tint = foreground, 325 | contentDescription = null 326 | ) 327 | 328 | Spacer(modifier = Modifier.width(8.dp)) 329 | } 330 | 331 | Text( 332 | text = text, 333 | fontSize = 16.sp, 334 | fontWeight = FontWeight.W500, 335 | color = foreground 336 | ) 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/Preferences.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.compose.runtime.mutableStateOf 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.json.Json 9 | 10 | private val JSON_INSTANCE = Json { ignoreUnknownKeys = true } 11 | 12 | object Preferences { 13 | 14 | private const val PREF_BACKEND_URL = "backend_url" 15 | private const val PREF_API_KEY = "api_key" 16 | private const val PREF_INIT_OPTIONS = "init_options" 17 | 18 | private lateinit var prefs: SharedPreferences 19 | 20 | fun initAppStart(context: Context) { 21 | prefs = context.applicationContext.getSharedPreferences("prefs", Context.MODE_PRIVATE) 22 | 23 | listOf(backendUrl, apiKey, lastInitOptions).forEach { it.init() } 24 | } 25 | 26 | private fun getString(key: String): String? = prefs.getString(key, null) 27 | 28 | interface BasePref { 29 | fun init() 30 | } 31 | 32 | class StringPref(private val key: String): BasePref { 33 | private val cachedValue = mutableStateOf(null) 34 | 35 | override fun init() { 36 | cachedValue.value = getString(key) 37 | prefs.registerOnSharedPreferenceChangeListener { _, changedKey -> 38 | if (key == changedKey) { 39 | cachedValue.value = getString(key) 40 | } 41 | } 42 | } 43 | 44 | var value: String? 45 | get() = cachedValue.value 46 | set(newValue) { 47 | cachedValue.value = newValue 48 | prefs.edit().putString(key, newValue).apply() 49 | } 50 | } 51 | 52 | class JsonPref(private val key: String, private var serializer: KSerializer): BasePref { 53 | private val cachedValue = mutableStateOf(null) 54 | 55 | private fun lookupValue(): E? = 56 | getString(key)?.let { JSON_INSTANCE.decodeFromString(serializer, it) } 57 | 58 | override fun init() { 59 | cachedValue.value = lookupValue() 60 | prefs.registerOnSharedPreferenceChangeListener { _, changedKey -> 61 | if (key == changedKey) { 62 | cachedValue.value = lookupValue() 63 | } 64 | } 65 | } 66 | 67 | var value: E? 68 | get() = cachedValue.value 69 | set(newValue) { 70 | cachedValue.value = newValue 71 | prefs.edit() 72 | .putString(key, newValue?.let { JSON_INSTANCE.encodeToString(serializer, it) }) 73 | .apply() 74 | } 75 | } 76 | 77 | val backendUrl = StringPref(PREF_BACKEND_URL) 78 | val apiKey = StringPref(PREF_API_KEY) 79 | val lastInitOptions = JsonPref(PREF_INIT_OPTIONS, LastInitOptions.serializer()) 80 | } 81 | 82 | @Serializable 83 | data class LastInitOptions( 84 | val botProfile: String? = null, 85 | val ttsProvider: String? = null, 86 | val ttsVoice: String? = null, 87 | val llmProvider: String? = null, 88 | val llmModel: String? = null, 89 | val sttProvider: String? = null, 90 | val sttModel: String? = null, 91 | val sttLanguage: String? = null, 92 | ) { 93 | companion object { 94 | fun from(initOptions: VoiceClientManager.InitOptions, runtimeOptions: VoiceClientManager.RuntimeOptions) = LastInitOptions( 95 | botProfile = initOptions.botProfile.id, 96 | ttsProvider = initOptions.ttsProvider.id, 97 | ttsVoice = runtimeOptions.ttsVoice.id, 98 | llmProvider = initOptions.llmProvider.id, 99 | llmModel = runtimeOptions.llmModel.id 100 | ) 101 | } 102 | 103 | fun inflateInit(): VoiceClientManager.InitOptions { 104 | 105 | val botProfile = ConfigConstants.botProfiles.byIdOrDefault(botProfile) 106 | 107 | return VoiceClientManager.InitOptions( 108 | botProfile = botProfile, 109 | ttsProvider = botProfile.ttsProviders.byIdOrDefault(ttsProvider), 110 | llmProvider = botProfile.llmProviders.byIdOrDefault(llmProvider), 111 | sttProvider = botProfile.sttProviders.byIdOrDefault(sttProvider) 112 | ) 113 | } 114 | 115 | fun inflateRuntime(initOptions: VoiceClientManager.InitOptions): VoiceClientManager.RuntimeOptions { 116 | 117 | val sttModelInstance = initOptions.sttProvider.models.byIdOrDefault(sttModel) 118 | 119 | return VoiceClientManager.RuntimeOptions( 120 | ttsVoice = initOptions.ttsProvider.voices.byIdOrDefault(ttsVoice), 121 | llmModel = initOptions.llmProvider.models.byIdOrDefault(llmModel), 122 | sttModel = sttModelInstance, 123 | sttLanguage = sttModelInstance.languages.byIdOrDefault(sttLanguage) 124 | ) 125 | } 126 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/RTVIApplication.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo 2 | 3 | import android.app.Application 4 | 5 | class RTVIApplication : Application() { 6 | override fun onCreate() { 7 | super.onCreate() 8 | Preferences.initAppStart(this) 9 | } 10 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/VoiceClientManager.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo 2 | 3 | import ai.rtvi.client.RTVIClient 4 | import ai.rtvi.client.RTVIClientOptions 5 | import ai.rtvi.client.RTVIClientParams 6 | import ai.rtvi.client.RTVIEventCallbacks 7 | import ai.rtvi.client.daily.DailyVoiceClient 8 | import ai.rtvi.client.result.Future 9 | import ai.rtvi.client.result.RTVIError 10 | import ai.rtvi.client.result.Result 11 | import ai.rtvi.client.types.ActionDescription 12 | import ai.rtvi.client.types.Option 13 | import ai.rtvi.client.types.Participant 14 | import ai.rtvi.client.types.PipecatMetrics 15 | import ai.rtvi.client.types.RTVIURLEndpoints 16 | import ai.rtvi.client.types.ServiceConfig 17 | import ai.rtvi.client.types.ServiceRegistration 18 | import ai.rtvi.client.types.Tracks 19 | import ai.rtvi.client.types.Transcript 20 | import ai.rtvi.client.types.TransportState 21 | import ai.rtvi.client.types.Value 22 | import android.content.Context 23 | import android.util.Log 24 | import androidx.compose.runtime.Immutable 25 | import androidx.compose.runtime.Stable 26 | import androidx.compose.runtime.mutableFloatStateOf 27 | import androidx.compose.runtime.mutableStateListOf 28 | import androidx.compose.runtime.mutableStateOf 29 | import co.daily.bots.demo.utils.Timestamp 30 | 31 | @Immutable 32 | data class Error(val message: String) 33 | 34 | @Stable 35 | class VoiceClientManager(private val context: Context) { 36 | 37 | companion object { 38 | private const val TAG = "VoiceClientManager" 39 | } 40 | 41 | @Immutable 42 | data class InitOptions( 43 | val botProfile: BotProfile, 44 | val ttsProvider: TTSProvider, 45 | val llmProvider: LLMProvider, 46 | val sttProvider: STTProvider, 47 | ) { 48 | companion object { 49 | fun default() = ConfigConstants.botProfiles.default.let { botProfile -> 50 | InitOptions( 51 | botProfile = botProfile, 52 | ttsProvider = botProfile.ttsProviders.default, 53 | llmProvider = botProfile.llmProviders.default, 54 | sttProvider = botProfile.sttProviders.default 55 | ) 56 | } 57 | } 58 | } 59 | 60 | @Immutable 61 | data class RuntimeOptions( 62 | val ttsVoice: TTSOptionVoice, 63 | val llmModel: LLMOptionModel, 64 | val sttModel: STTOptionModel, 65 | val sttLanguage: STTOptionLanguage, 66 | ) { 67 | companion object { 68 | fun default() = ConfigConstants.botProfiles.default.let { botProfile -> 69 | RuntimeOptions( 70 | ttsVoice = botProfile.ttsProviders.default.voices.default, 71 | llmModel = botProfile.llmProviders.default.models.default, 72 | sttModel = botProfile.sttProviders.default.models.default, 73 | sttLanguage = botProfile.sttProviders.default.models.default.languages.default 74 | ) 75 | } 76 | } 77 | } 78 | 79 | private val client = mutableStateOf(null) 80 | 81 | val state = mutableStateOf(null) 82 | 83 | val errors = mutableStateListOf() 84 | 85 | val actionDescriptions = 86 | mutableStateOf, RTVIError>?>(null) 87 | 88 | val expiryTime = mutableStateOf(null) 89 | 90 | val botReady = mutableStateOf(false) 91 | val botIsTalking = mutableStateOf(false) 92 | val userIsTalking = mutableStateOf(false) 93 | val botAudioLevel = mutableFloatStateOf(0f) 94 | val userAudioLevel = mutableFloatStateOf(0f) 95 | 96 | val mic = mutableStateOf(false) 97 | val camera = mutableStateOf(false) 98 | val tracks = mutableStateOf(null) 99 | 100 | private fun Future.displayErrors() = withErrorCallback { 101 | Log.e(TAG, "Future resolved with error: ${it.description}", it.exception) 102 | errors.add(Error(it.description)) 103 | } 104 | 105 | fun start( 106 | baseUrl: String, 107 | apiKey: String?, 108 | initOptions: InitOptions, 109 | runtimeOptions: RuntimeOptions, 110 | ) { 111 | 112 | if (client.value != null) { 113 | return 114 | } 115 | 116 | val options = RTVIClientOptions( 117 | params = RTVIClientParams( 118 | baseUrl = baseUrl, 119 | endpoints = RTVIURLEndpoints(connect = "/start"), 120 | // Note: For security reasons, don't include your API key in a production 121 | // client app. See: https://docs.dailybots.ai/architecture 122 | headers = apiKey 123 | ?.takeUnless { it.isEmpty() } 124 | ?.let { listOf("Authorization" to "Bearer $it") } 125 | ?: emptyList(), 126 | requestData = listOf( 127 | "bot_profile" to Value.Str(initOptions.botProfile.id), 128 | "max_duration" to Value.Number(600.0) 129 | ), 130 | config = listOf( 131 | ServiceConfig( 132 | "tts", listOf( 133 | Option("voice", runtimeOptions.ttsVoice.id) 134 | ) 135 | ), 136 | ServiceConfig( 137 | "llm", listOf( 138 | Option("model", runtimeOptions.llmModel.id), 139 | Option( 140 | "initial_messages", Value.Array( 141 | Value.Object( 142 | "role" to Value.Str("system"), 143 | "content" to Value.Str("You are a helpful voice assistant. Keep answers brief, and do not include markdown or other formatting in your responses, as they will be read out using TTS. Please greet the user and offer to assist them.") 144 | ) 145 | ) 146 | ), 147 | Option("run_on_config", true), 148 | ) 149 | ), 150 | ServiceConfig( 151 | "stt", listOf( 152 | Option("model", runtimeOptions.sttModel.id), 153 | Option("language", runtimeOptions.sttLanguage.id), 154 | ) 155 | ) 156 | ) 157 | ), 158 | services = listOf( 159 | ServiceRegistration("tts", initOptions.ttsProvider.id), 160 | ServiceRegistration("llm", initOptions.llmProvider.id), 161 | ServiceRegistration("stt", initOptions.sttProvider.id), 162 | ) 163 | ) 164 | 165 | state.value = TransportState.Disconnected 166 | 167 | val callbacks = object : RTVIEventCallbacks() { 168 | override fun onTransportStateChanged(state: TransportState) { 169 | this@VoiceClientManager.state.value = state 170 | } 171 | 172 | override fun onBackendError(message: String) { 173 | "Error from backend: $message".let { 174 | Log.e(TAG, it) 175 | errors.add(Error(it)) 176 | } 177 | } 178 | 179 | override fun onBotReady(version: String, config: List) { 180 | 181 | Log.i(TAG, "Bot ready. Version $version, config: $config") 182 | 183 | botReady.value = true 184 | 185 | client.value?.describeActions()?.withCallback { 186 | actionDescriptions.value = it 187 | } 188 | } 189 | 190 | override fun onPipecatMetrics(data: PipecatMetrics) { 191 | Log.i(TAG, "Pipecat metrics: $data") 192 | } 193 | 194 | override fun onUserTranscript(data: Transcript) { 195 | Log.i(TAG, "User transcript: $data") 196 | } 197 | 198 | override fun onBotTranscript(text: String) { 199 | Log.i(TAG, "Bot transcript: $text") 200 | } 201 | 202 | override fun onBotStartedSpeaking() { 203 | Log.i(TAG, "Bot started speaking") 204 | botIsTalking.value = true 205 | } 206 | 207 | override fun onBotStoppedSpeaking() { 208 | Log.i(TAG, "Bot stopped speaking") 209 | botIsTalking.value = false 210 | } 211 | 212 | override fun onUserStartedSpeaking() { 213 | Log.i(TAG, "User started speaking") 214 | userIsTalking.value = true 215 | } 216 | 217 | override fun onUserStoppedSpeaking() { 218 | Log.i(TAG, "User stopped speaking") 219 | userIsTalking.value = false 220 | } 221 | 222 | override fun onTracksUpdated(tracks: Tracks) { 223 | this@VoiceClientManager.tracks.value = tracks 224 | } 225 | 226 | override fun onInputsUpdated(camera: Boolean, mic: Boolean) { 227 | this@VoiceClientManager.camera.value = camera 228 | this@VoiceClientManager.mic.value = mic 229 | } 230 | 231 | override fun onConnected() { 232 | expiryTime.value = client.value?.expiry?.let(Timestamp::ofEpochSecs) 233 | } 234 | 235 | override fun onDisconnected() { 236 | expiryTime.value = null 237 | actionDescriptions.value = null 238 | botIsTalking.value = false 239 | userIsTalking.value = false 240 | state.value = null 241 | actionDescriptions.value = null 242 | botReady.value = false 243 | tracks.value = null 244 | 245 | client.value?.release() 246 | client.value = null 247 | } 248 | 249 | override fun onUserAudioLevel(level: Float) { 250 | userAudioLevel.floatValue = level 251 | } 252 | 253 | override fun onRemoteAudioLevel(level: Float, participant: Participant) { 254 | botAudioLevel.floatValue = level 255 | } 256 | } 257 | 258 | val client = DailyVoiceClient(context, callbacks, options) 259 | 260 | client.connect().displayErrors().withErrorCallback { 261 | callbacks.onDisconnected() 262 | } 263 | 264 | this.client.value = client 265 | } 266 | 267 | fun enableCamera(enabled: Boolean) { 268 | client.value?.enableCam(enabled)?.displayErrors() 269 | } 270 | 271 | fun enableMic(enabled: Boolean) { 272 | client.value?.enableMic(enabled)?.displayErrors() 273 | } 274 | 275 | fun toggleCamera() = enableCamera(!camera.value) 276 | fun toggleMic() = enableMic(!mic.value) 277 | 278 | fun stop() { 279 | client.value?.disconnect()?.displayErrors() 280 | } 281 | 282 | fun action(service: String, action: String, args: Map) = 283 | client.value?.action( 284 | service = service, 285 | action = action, 286 | arguments = args.map { Option(it.key, it.value) })?.displayErrors() 287 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/AudioIndicator.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.animateFloat 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.Canvas 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.StrokeCap 16 | import androidx.compose.ui.semantics.clearAndSetSemantics 17 | 18 | @Composable 19 | fun ListeningAnimation( 20 | modifier: Modifier, 21 | active: Boolean, 22 | level: Float, 23 | color: Color, 24 | ) { 25 | val infiniteTransition = rememberInfiniteTransition("listeningAnimation") 26 | 27 | val loopState by infiniteTransition.animateFloat( 28 | initialValue = 0f, 29 | targetValue = Math.PI.toFloat() * 2f, 30 | animationSpec = infiniteRepeatable(tween(durationMillis = 1000, easing = LinearEasing)), 31 | label = "listeningAnimationLoopState" 32 | ) 33 | 34 | val activeFraction by animateFloatAsState( 35 | if (active) { 36 | Math.pow(level.toDouble(), 0.3).toFloat() 37 | } else { 38 | 0f 39 | } 40 | ) 41 | 42 | Canvas(modifier.clearAndSetSemantics { }) { 43 | 44 | val strokeWidthPx = size.width / 12 45 | 46 | val lineCount = 5 47 | 48 | for (i in 1..lineCount) { 49 | 50 | val sine = Math.sin(loopState + 0.9 * i) 51 | val fraction = activeFraction * ((sine + 1) / 2).toFloat() 52 | 53 | val x = (size.width / (lineCount + 1)) * i 54 | 55 | val yMax = size.height * 0.25f 56 | val yMin = size.height * 0.5f 57 | 58 | val y = yMin + (yMax - yMin) * fraction 59 | val yEnd = size.height - y 60 | 61 | this.drawLine( 62 | start = Offset(x, y), 63 | end = Offset(x, yEnd), 64 | color = color, 65 | strokeWidth = strokeWidthPx, 66 | cap = StrokeCap.Round 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/BotIndicator.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.ui.theme.Colors 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.animation.animateColorAsState 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.aspectRatio 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material3.CircularProgressIndicator 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.FloatState 17 | import androidx.compose.runtime.State 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableFloatStateOf 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.draw.shadow 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.graphics.StrokeCap 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import androidx.compose.ui.unit.dp 30 | 31 | @Composable 32 | fun BotIndicator( 33 | modifier: Modifier, 34 | isReady: Boolean, 35 | isTalking: State, 36 | audioLevel: FloatState, 37 | ) { 38 | Box( 39 | modifier = modifier.padding(15.dp), 40 | contentAlignment = Alignment.Center 41 | ) { 42 | val color by animateColorAsState(if (isTalking.value || !isReady) { 43 | Color.Black 44 | } else { 45 | Colors.botIndicatorBackground 46 | }) 47 | 48 | Box( 49 | Modifier 50 | .aspectRatio(1f) 51 | .fillMaxSize() 52 | .shadow(20.dp, CircleShape) 53 | .border(12.dp, Color.White, CircleShape) 54 | .border(1.dp, Colors.lightGrey, CircleShape) 55 | .clip(CircleShape) 56 | .background(color) 57 | .padding(50.dp), 58 | contentAlignment = Alignment.Center, 59 | ) { 60 | AnimatedContent( 61 | targetState = isReady 62 | ) { isReadyVal -> 63 | if (isReadyVal) { 64 | ListeningAnimation( 65 | modifier = Modifier.fillMaxSize(), 66 | active = isTalking.value, 67 | level = audioLevel.floatValue, 68 | color = Color.White 69 | ) 70 | } else { 71 | CircularProgressIndicator( 72 | modifier = Modifier.size(180.dp), 73 | color = Color.White, 74 | strokeWidth = 12.dp, 75 | strokeCap = StrokeCap.Round, 76 | trackColor = color 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | @Composable 85 | @Preview 86 | fun PreviewBotIndicator() { 87 | BotIndicator( 88 | modifier = Modifier, 89 | isReady = false, 90 | isTalking = remember { mutableStateOf(true) }, 91 | audioLevel = remember { mutableFloatStateOf(1.0f) } 92 | ) 93 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/InCallFooter.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.R 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import androidx.annotation.DrawableRes 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.res.painterResource 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.sp 29 | 30 | @Composable 31 | private fun FooterButton( 32 | modifier: Modifier, 33 | onClick: () -> Unit, 34 | @DrawableRes icon: Int, 35 | text: String, 36 | foreground: Color, 37 | background: Color, 38 | border: Color, 39 | ) { 40 | val shape = RoundedCornerShape(12.dp) 41 | 42 | Row( 43 | modifier 44 | .border(1.dp, border, shape) 45 | .clip(shape) 46 | .background(background) 47 | .clickable(onClick = onClick) 48 | .padding(vertical = 10.dp, horizontal = 18.dp), 49 | verticalAlignment = Alignment.CenterVertically, 50 | horizontalArrangement = Arrangement.Center 51 | ) { 52 | Icon( 53 | modifier = Modifier.size(24.dp), 54 | painter = painterResource(icon), 55 | tint = foreground, 56 | contentDescription = null 57 | ) 58 | 59 | Spacer(modifier = Modifier.width(8.dp)) 60 | 61 | Text( 62 | text = text, 63 | fontSize = 14.sp, 64 | fontWeight = FontWeight.W600, 65 | color = foreground 66 | ) 67 | } 68 | } 69 | 70 | @Composable 71 | fun InCallFooter( 72 | onClickCommands: () -> Unit, 73 | onClickEnd: () -> Unit, 74 | ) { 75 | Row(Modifier 76 | .fillMaxWidth() 77 | .padding(15.dp) 78 | ) { 79 | 80 | FooterButton( 81 | modifier = Modifier.weight(1f), 82 | onClick = onClickCommands, 83 | icon = R.drawable.console, 84 | text = "Commands", 85 | foreground = Color.Black, 86 | background = Color.White, 87 | border = Colors.lightGrey 88 | ) 89 | 90 | Spacer(Modifier.width(15.dp)) 91 | 92 | FooterButton( 93 | modifier = Modifier.weight(1f), 94 | onClick = onClickEnd, 95 | icon = R.drawable.phone_hangup, 96 | text = "End", 97 | foreground = Color.White, 98 | background = Colors.endButton, 99 | border = Colors.endButton 100 | ) 101 | } 102 | } 103 | 104 | @Composable 105 | @Preview 106 | fun PreviewInCallFooter() { 107 | InCallFooter({}, {}) 108 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/InCallHeader.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.utils.Timestamp 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.togetherWith 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | import androidx.constraintlayout.compose.ConstraintLayout 15 | 16 | @Composable 17 | fun InCallHeader( 18 | expiryTime: Timestamp? 19 | ) { 20 | ConstraintLayout( 21 | Modifier 22 | .fillMaxWidth() 23 | .padding(vertical = 15.dp) 24 | ) { 25 | val (refLogo, refTimer) = createRefs() 26 | 27 | Logo(Modifier.constrainAs(refLogo) { 28 | top.linkTo(parent.top) 29 | bottom.linkTo(parent.bottom) 30 | start.linkTo(parent.start, 15.dp) 31 | }) 32 | 33 | AnimatedContent( 34 | modifier = Modifier.constrainAs(refTimer) { 35 | top.linkTo(parent.top) 36 | bottom.linkTo(parent.bottom) 37 | end.linkTo(parent.end) 38 | }, 39 | targetState = expiryTime, 40 | transitionSpec = { fadeIn() togetherWith fadeOut() } 41 | ) { expiryTimeVal -> 42 | if (expiryTimeVal != null) { 43 | Timer(expiryTime = expiryTimeVal, modifier = Modifier) 44 | } 45 | } 46 | } 47 | } 48 | 49 | @Composable 50 | @Preview 51 | fun PreviewInCallHeader() { 52 | InCallHeader( 53 | Timestamp.now() + java.time.Duration.ofMinutes(3) 54 | ) 55 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/InCallLayout.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.VoiceClientManager 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import co.daily.bots.demo.ui.theme.TextStyles 6 | import co.daily.bots.demo.ui.theme.textFieldColors 7 | import ai.rtvi.client.result.Result 8 | import ai.rtvi.client.result.VoiceError 9 | import ai.rtvi.client.types.Type 10 | import ai.rtvi.client.types.Value 11 | import androidx.compose.animation.animateColorAsState 12 | import androidx.compose.foundation.background 13 | import androidx.compose.foundation.border 14 | import androidx.compose.foundation.clickable 15 | import androidx.compose.foundation.layout.Arrangement 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.foundation.layout.Column 18 | import androidx.compose.foundation.layout.Row 19 | import androidx.compose.foundation.layout.Spacer 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.foundation.layout.fillMaxWidth 22 | import androidx.compose.foundation.layout.height 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.rememberScrollState 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.foundation.verticalScroll 27 | import androidx.compose.material3.AlertDialog 28 | import androidx.compose.material3.Button 29 | import androidx.compose.material3.Checkbox 30 | import androidx.compose.material3.ExperimentalMaterial3Api 31 | import androidx.compose.material3.ModalBottomSheet 32 | import androidx.compose.material3.Text 33 | import androidx.compose.material3.TextField 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.derivedStateOf 36 | import androidx.compose.runtime.getValue 37 | import androidx.compose.runtime.mutableStateListOf 38 | import androidx.compose.runtime.mutableStateMapOf 39 | import androidx.compose.runtime.mutableStateOf 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.setValue 42 | import androidx.compose.runtime.snapshots.SnapshotStateList 43 | import androidx.compose.ui.Alignment 44 | import androidx.compose.ui.Modifier 45 | import androidx.compose.ui.draw.clip 46 | import androidx.compose.ui.graphics.Color 47 | import androidx.compose.ui.text.font.FontWeight 48 | import androidx.compose.ui.unit.dp 49 | import androidx.compose.ui.unit.sp 50 | import kotlinx.serialization.ExperimentalSerializationApi 51 | import kotlinx.serialization.json.Json 52 | 53 | @OptIn(ExperimentalSerializationApi::class) 54 | private val JSON_PRETTY = Json { 55 | prettyPrint = true 56 | prettyPrintIndent = " " 57 | } 58 | 59 | @OptIn(ExperimentalMaterial3Api::class) 60 | @Composable 61 | fun InCallLayout(voiceClientManager: VoiceClientManager) { 62 | 63 | var commandsExpanded by remember { mutableStateOf(false) } 64 | 65 | val localCam by remember { derivedStateOf { voiceClientManager.tracks.value?.local?.video } } 66 | 67 | Column(Modifier.fillMaxSize()) { 68 | 69 | InCallHeader(expiryTime = voiceClientManager.expiryTime.value) 70 | 71 | Box( 72 | modifier = Modifier 73 | .weight(1f) 74 | .fillMaxWidth(), 75 | contentAlignment = Alignment.Center 76 | ) { 77 | Column( 78 | modifier = Modifier.fillMaxWidth(), 79 | horizontalAlignment = Alignment.CenterHorizontally, 80 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 81 | ) { 82 | BotIndicator( 83 | modifier = Modifier, 84 | isReady = voiceClientManager.botReady.value, 85 | isTalking = voiceClientManager.botIsTalking, 86 | audioLevel = voiceClientManager.botAudioLevel 87 | ) 88 | 89 | Row( 90 | verticalAlignment = Alignment.CenterVertically 91 | ) { 92 | UserMicButton( 93 | onClick = voiceClientManager::toggleMic, 94 | micEnabled = voiceClientManager.mic.value, 95 | modifier = Modifier, 96 | isTalking = voiceClientManager.userIsTalking, 97 | audioLevel = voiceClientManager.userAudioLevel 98 | ) 99 | 100 | UserCamButton( 101 | onClick = voiceClientManager::toggleCamera, 102 | camEnabled = voiceClientManager.camera.value, 103 | camTrackId = localCam, 104 | modifier = Modifier 105 | ) 106 | } 107 | } 108 | } 109 | 110 | InCallFooter( 111 | onClickCommands = { commandsExpanded = true }, 112 | onClickEnd = voiceClientManager::stop 113 | ) 114 | 115 | if (commandsExpanded) { 116 | ModalBottomSheet( 117 | onDismissRequest = { commandsExpanded = false }, 118 | containerColor = Color.White, 119 | ) { 120 | ActionList(voiceClientManager) 121 | } 122 | } 123 | } 124 | } 125 | 126 | @Composable 127 | fun ActionList(voiceClientManager: VoiceClientManager) { 128 | 129 | val resultDialogText: SnapshotStateList> = 130 | remember { mutableStateListOf() } 131 | 132 | val actions = voiceClientManager.actionDescriptions.value?.valueOrNull ?: emptyList() 133 | 134 | val scrollState = rememberScrollState() 135 | 136 | Column( 137 | Modifier 138 | .fillMaxWidth() 139 | .verticalScroll(scrollState) 140 | .padding(24.dp) 141 | ) { 142 | actions.forEach { action -> 143 | 144 | Spacer(Modifier.height(12.dp)) 145 | 146 | Box( 147 | modifier = Modifier 148 | .border( 149 | width = 1.dp, 150 | color = Colors.unmutedMicBackground, 151 | shape = RoundedCornerShape(12.dp) 152 | ) 153 | .clip(RoundedCornerShape(12.dp)), 154 | ) { 155 | val arguments: MutableMap = remember { mutableStateMapOf() } 156 | 157 | Column( 158 | Modifier 159 | .fillMaxWidth() 160 | ) { 161 | Row( 162 | Modifier 163 | .fillMaxWidth() 164 | .background(Colors.lightGrey) 165 | .padding(12.dp), 166 | verticalAlignment = Alignment.CenterVertically 167 | ) { 168 | Text( 169 | modifier = Modifier.weight(1f), 170 | text = "${action.service} : ${action.action}", 171 | fontSize = 16.sp, 172 | fontWeight = FontWeight.W700, 173 | style = TextStyles.base 174 | ) 175 | 176 | Box( 177 | Modifier 178 | .border(1.dp, Colors.logoBorder, RoundedCornerShape(6.dp)) 179 | .clip(RoundedCornerShape(6.dp)) 180 | .background(Color.White) 181 | .clickable { 182 | voiceClientManager 183 | .action( 184 | service = action.service, 185 | action = action.action, 186 | args = arguments 187 | ) 188 | ?.withCallback { 189 | resultDialogText.add(it) 190 | } 191 | } 192 | .padding(8.dp) 193 | ) { 194 | Text( 195 | text = "Send", 196 | fontSize = 14.sp, 197 | style = TextStyles.base, 198 | fontWeight = FontWeight.W700, 199 | ) 200 | } 201 | } 202 | 203 | Column( 204 | verticalArrangement = Arrangement.spacedBy(8.dp) 205 | ) { 206 | action.arguments.forEach { arg -> 207 | 208 | val argValue = arguments[arg.name] 209 | 210 | @Composable 211 | fun Textbox( 212 | toValue: (String) -> Value?, 213 | ) { 214 | var textValue by remember { mutableStateOf("") } 215 | 216 | var isValid by remember { mutableStateOf(true) } 217 | 218 | val borderColor by animateColorAsState( 219 | if (isValid) { 220 | Colors.logoBorder 221 | } else { 222 | Colors.mutedMicBackground 223 | } 224 | ) 225 | 226 | val type = when (arg.type) { 227 | Type.Str -> "string" 228 | Type.Bool -> "bool" 229 | Type.Number -> "number" 230 | Type.Array -> "JSON array" 231 | Type.Object -> "JSON object" 232 | } 233 | 234 | val shape = RoundedCornerShape(12.dp) 235 | 236 | Box( 237 | Modifier.padding(12.dp) 238 | ) { 239 | TextField( 240 | modifier = Modifier 241 | .fillMaxWidth() 242 | .border(1.dp, borderColor, shape), 243 | value = textValue, 244 | onValueChange = { 245 | textValue = it 246 | val newValue = toValue(it) 247 | isValid = (newValue != null) 248 | 249 | if (newValue != null) { 250 | arguments[arg.name] = newValue 251 | } else { 252 | arguments[arg.name] = Value.Null 253 | } 254 | }, 255 | label = { 256 | Text( 257 | text = "${arg.name} ($type)", 258 | fontSize = 13.sp, 259 | fontWeight = FontWeight.W500, 260 | style = TextStyles.base 261 | ) 262 | }, 263 | colors = textFieldColors(), 264 | textStyle = TextStyles.base, 265 | shape = shape 266 | ) 267 | } 268 | } 269 | 270 | when (arg.type) { 271 | Type.Str -> { 272 | Textbox { Value.Str(it) } 273 | } 274 | 275 | Type.Bool -> { 276 | Row(verticalAlignment = Alignment.CenterVertically) { 277 | Checkbox( 278 | checked = (argValue as? Value.Bool)?.value ?: false, 279 | onCheckedChange = { 280 | arguments[arg.name] = Value.Bool(it) 281 | } 282 | ) 283 | Text( 284 | text = "${arg.name} (bool)", 285 | fontSize = 13.sp, 286 | fontWeight = FontWeight.W500, 287 | style = TextStyles.base 288 | ) 289 | } 290 | } 291 | 292 | Type.Number -> { 293 | Textbox { 294 | Value.Number( 295 | it.toDoubleOrNull() ?: return@Textbox null 296 | ) 297 | } 298 | } 299 | 300 | Type.Array, Type.Object -> { 301 | Textbox { 302 | try { 303 | Json.decodeFromString(it) 304 | } catch (e: Exception) { 305 | null 306 | } 307 | } 308 | } 309 | } 310 | } 311 | } 312 | } 313 | } 314 | } 315 | } 316 | 317 | resultDialogText.firstOrNull()?.let { 318 | AlertDialog( 319 | onDismissRequest = { resultDialogText.removeFirst() }, 320 | confirmButton = { 321 | Button( 322 | onClick = { resultDialogText.removeFirst() } 323 | ) { 324 | Text(text = "Close", fontSize = 16.sp) 325 | } 326 | }, 327 | title = { 328 | Text(text = "Action result", fontSize = 20.sp, fontWeight = FontWeight.W700) 329 | }, 330 | text = { 331 | Text(text = when (it) { 332 | is Result.Err -> "Error: ${it.error.description}" 333 | is Result.Ok -> { 334 | JSON_PRETTY.encodeToString(Value.serializer(), it.value) 335 | } 336 | }, fontSize = 16.sp) 337 | } 338 | ) 339 | } 340 | 341 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/Logo.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.R 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.draw.shadow 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 | 21 | @Composable 22 | fun Logo(modifier: Modifier) { 23 | 24 | val shape = RoundedCornerShape(12.dp) 25 | 26 | Box( 27 | modifier = modifier 28 | .size(64.dp) 29 | .shadow(5.dp, shape) 30 | .border(1.dp, Colors.logoBorder, shape) 31 | .clip(shape) 32 | .background(Color.White), 33 | contentAlignment = Alignment.Center 34 | ) { 35 | Image( 36 | modifier = Modifier.size(40.dp, 44.dp), 37 | painter = painterResource(id = R.drawable.logo), 38 | contentDescription = "RTVI" 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | @Preview 45 | fun PreviewLogo() { 46 | Logo(Modifier) 47 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/PermissionScreen.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.ui.theme.Colors 4 | import co.daily.bots.demo.ui.theme.TextStyles 5 | import android.Manifest 6 | import android.util.Log 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.border 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.Button 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.ui.draw.shadow 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import androidx.compose.ui.window.Dialog 28 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 29 | import com.google.accompanist.permissions.isGranted 30 | import com.google.accompanist.permissions.rememberPermissionState 31 | 32 | @OptIn(ExperimentalPermissionsApi::class) 33 | @Composable 34 | fun PermissionScreen() { 35 | val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA) 36 | val micPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO) 37 | 38 | val requestPermissionLauncher = rememberLauncherForActivityResult( 39 | ActivityResultContracts.RequestMultiplePermissions() 40 | ) { isGranted -> 41 | Log.i("MainActivity", "Permissions granted: $isGranted") 42 | } 43 | 44 | if (!cameraPermission.status.isGranted || !micPermission.status.isGranted) { 45 | 46 | Dialog( 47 | onDismissRequest = {}, 48 | ) { 49 | val dialogShape = RoundedCornerShape(16.dp) 50 | 51 | Column( 52 | Modifier 53 | .shadow(6.dp, dialogShape) 54 | .border(2.dp, Colors.logoBorder, dialogShape) 55 | .clip(dialogShape) 56 | .background(Color.White) 57 | .padding(28.dp) 58 | ) { 59 | Text( 60 | text = "Permissions", 61 | fontSize = 24.sp, 62 | fontWeight = FontWeight.W700, 63 | style = TextStyles.base 64 | ) 65 | 66 | Spacer(modifier = Modifier.height(8.dp)) 67 | 68 | Text( 69 | text = "Please grant camera and mic permissions to continue", 70 | fontSize = 18.sp, 71 | fontWeight = FontWeight.W400, 72 | style = TextStyles.base 73 | ) 74 | 75 | Spacer(modifier = Modifier.height(36.dp)) 76 | 77 | Button( 78 | modifier = Modifier.align(Alignment.End), 79 | shape = RoundedCornerShape(12.dp), 80 | onClick = { 81 | requestPermissionLauncher.launch( 82 | arrayOf( 83 | Manifest.permission.CAMERA, 84 | Manifest.permission.RECORD_AUDIO 85 | ) 86 | ) 87 | } 88 | ) { 89 | Text( 90 | text = "Grant permissions", 91 | fontSize = 16.sp, 92 | fontWeight = FontWeight.W700, 93 | style = TextStyles.base 94 | ) 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/Timer.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.R 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import co.daily.bots.demo.utils.Timestamp 6 | import co.daily.bots.demo.utils.formatTimer 7 | import co.daily.bots.demo.utils.rtcStateSecs 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.layout.widthIn 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import java.time.Duration 29 | 30 | @Composable 31 | fun Timer( 32 | expiryTime: Timestamp, 33 | modifier: Modifier, 34 | ) { 35 | val now by rtcStateSecs() 36 | 37 | val shape = RoundedCornerShape( 38 | topStart = 12.dp, 39 | bottomStart = 12.dp, 40 | ) 41 | 42 | Row( 43 | modifier = modifier 44 | .widthIn(min = 100.dp) 45 | .clip(shape) 46 | .background(Colors.lightGrey) 47 | .padding(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 16.dp), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | Icon( 51 | painter = painterResource(id = R.drawable.timer_outline), 52 | contentDescription = null, 53 | modifier = Modifier.size(20.dp), 54 | tint = Colors.expiryTimerForeground 55 | ) 56 | 57 | Spacer(Modifier.width(8.dp)) 58 | 59 | Text( 60 | text = formatTimer(duration = expiryTime - now), 61 | fontSize = 16.sp, 62 | fontWeight = FontWeight.W600, 63 | color = Colors.expiryTimerForeground 64 | ) 65 | } 66 | } 67 | 68 | @Composable 69 | @Preview 70 | fun PreviewExpiryTimer() { 71 | Timer(Timestamp.now() + Duration.ofMinutes(5), Modifier) 72 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/UserCamButton.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.R 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import ai.rtvi.client.daily.VoiceClientVideoView 6 | import ai.rtvi.client.types.MediaTrackId 7 | import androidx.compose.animation.animateColorAsState 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.border 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.shape.CircleShape 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.State 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.draw.shadow 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.viewinterop.AndroidView 29 | 30 | @Composable 31 | fun UserCamButton( 32 | onClick: () -> Unit, 33 | camEnabled: Boolean, 34 | camTrackId: MediaTrackId?, 35 | modifier: Modifier, 36 | ) { 37 | Box( 38 | modifier = modifier.padding(15.dp).size(96.dp), 39 | contentAlignment = Alignment.Center 40 | ) { 41 | val color by animateColorAsState( 42 | if (camEnabled) { 43 | Colors.unmutedMicBackground 44 | } else { 45 | Colors.mutedMicBackground 46 | } 47 | ) 48 | 49 | Box( 50 | Modifier 51 | .fillMaxSize() 52 | .shadow(3.dp, CircleShape) 53 | .border(6.dp, Color.White, CircleShape) 54 | .border(1.dp, Colors.lightGrey, CircleShape) 55 | .clip(CircleShape) 56 | .background(color) 57 | .clickable(onClick = onClick), 58 | contentAlignment = Alignment.Center, 59 | ) { 60 | if (camTrackId != null) { 61 | AndroidView( 62 | factory = { context -> 63 | VoiceClientVideoView(context) 64 | }, 65 | update = { view -> 66 | view.voiceClientTrack = camTrackId 67 | } 68 | ) 69 | } else { 70 | Icon( 71 | modifier = Modifier.size(30.dp), 72 | painter = painterResource( 73 | if (camEnabled) { 74 | R.drawable.video 75 | } else { 76 | R.drawable.video_off 77 | } 78 | ), 79 | tint = Color.White, 80 | contentDescription = if (camEnabled) { 81 | "Disable camera" 82 | } else { 83 | "Enable camera" 84 | }, 85 | ) 86 | } 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | @Preview 93 | fun PreviewUserCamButton() { 94 | UserCamButton( 95 | onClick = {}, 96 | camTrackId = null, 97 | camEnabled = true, 98 | modifier = Modifier, 99 | ) 100 | } 101 | 102 | @Composable 103 | @Preview 104 | fun PreviewUserCamButtonMuted() { 105 | UserCamButton( 106 | onClick = {}, 107 | camTrackId = null, 108 | camEnabled = false, 109 | modifier = Modifier, 110 | ) 111 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/UserMicButton.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.R 4 | import co.daily.bots.demo.ui.theme.Colors 5 | import androidx.compose.animation.animateColorAsState 6 | import androidx.compose.animation.core.animateDpAsState 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.border 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.FloatState 17 | import androidx.compose.runtime.State 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableFloatStateOf 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.draw.shadow 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.res.painterResource 28 | import androidx.compose.ui.tooling.preview.Preview 29 | import androidx.compose.ui.unit.dp 30 | 31 | @Composable 32 | fun UserMicButton( 33 | onClick: () -> Unit, 34 | micEnabled: Boolean, 35 | modifier: Modifier, 36 | isTalking: State, 37 | audioLevel: FloatState, 38 | ) { 39 | Box( 40 | modifier = modifier.padding(15.dp), 41 | contentAlignment = Alignment.Center 42 | ) { 43 | val borderThickness by animateDpAsState( 44 | if (isTalking.value) { 45 | (24.dp * Math.pow(audioLevel.floatValue.toDouble(), 0.3).toFloat()) + 3.dp 46 | } else { 47 | 6.dp 48 | } 49 | ) 50 | 51 | val color by animateColorAsState( 52 | if (!micEnabled) { 53 | Colors.mutedMicBackground 54 | } else if (isTalking.value) { 55 | Color.Black 56 | } else { 57 | Colors.unmutedMicBackground 58 | } 59 | ) 60 | 61 | Box( 62 | Modifier 63 | .shadow(3.dp, CircleShape) 64 | .border(borderThickness, Color.White, CircleShape) 65 | .border(1.dp, Colors.lightGrey, CircleShape) 66 | .clip(CircleShape) 67 | .background(color) 68 | .clickable(onClick = onClick) 69 | .padding(36.dp), 70 | contentAlignment = Alignment.Center, 71 | ) { 72 | Icon( 73 | modifier = Modifier.size(48.dp), 74 | painter = painterResource( 75 | if (micEnabled) { 76 | R.drawable.microphone 77 | } else { 78 | R.drawable.microphone_off 79 | } 80 | ), 81 | tint = Color.White, 82 | contentDescription = if (micEnabled) { 83 | "Mute microphone" 84 | } else { 85 | "Unmute microphone" 86 | }, 87 | ) 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | @Preview 94 | fun PreviewUserMicButton() { 95 | UserMicButton( 96 | onClick = {}, 97 | micEnabled = true, 98 | modifier = Modifier, 99 | isTalking = remember { mutableStateOf(false) }, 100 | audioLevel = remember { mutableFloatStateOf(1.0f) } 101 | ) 102 | } 103 | 104 | @Composable 105 | @Preview 106 | fun PreviewUserMicButtonMuted() { 107 | UserMicButton( 108 | onClick = {}, 109 | micEnabled = false, 110 | modifier = Modifier, 111 | isTalking = remember { mutableStateOf(false) }, 112 | audioLevel = remember { mutableFloatStateOf(1.0f) } 113 | ) 114 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/VoiceClientSettingsPanel.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui 2 | 3 | import co.daily.bots.demo.ConfigConstants 4 | import co.daily.bots.demo.LastInitOptions 5 | import co.daily.bots.demo.NamedOption 6 | import co.daily.bots.demo.NamedOptionList 7 | import co.daily.bots.demo.Preferences 8 | import co.daily.bots.demo.VoiceClientManager 9 | import co.daily.bots.demo.ui.theme.Colors 10 | import co.daily.bots.demo.ui.theme.RTVIClientTheme 11 | import co.daily.bots.demo.ui.theme.TextStyles 12 | import androidx.compose.foundation.background 13 | import androidx.compose.foundation.border 14 | import androidx.compose.foundation.clickable 15 | import androidx.compose.foundation.layout.Column 16 | import androidx.compose.foundation.layout.ColumnScope 17 | import androidx.compose.foundation.layout.Row 18 | import androidx.compose.foundation.layout.Spacer 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.foundation.layout.height 21 | import androidx.compose.foundation.layout.padding 22 | import androidx.compose.foundation.layout.width 23 | import androidx.compose.foundation.rememberScrollState 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.foundation.verticalScroll 26 | import androidx.compose.material3.HorizontalDivider 27 | import androidx.compose.material3.RadioButton 28 | import androidx.compose.material3.Text 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.runtime.MutableState 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.draw.clip 34 | import androidx.compose.ui.graphics.Color 35 | import androidx.compose.ui.text.font.FontWeight 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.unit.sp 39 | 40 | @Composable 41 | fun VoiceClientSettingsPanel( 42 | initOptions: VoiceClientManager.InitOptions, 43 | runtimeOptions: VoiceClientManager.RuntimeOptions, 44 | ) { 45 | val scrollState = rememberScrollState() 46 | 47 | val pref = Preferences.lastInitOptions 48 | 49 | fun updatePref(action: LastInitOptions.() -> LastInitOptions) { 50 | pref.value = action(pref.value ?: LastInitOptions.from(initOptions, runtimeOptions)) 51 | } 52 | 53 | Column( 54 | Modifier 55 | .fillMaxWidth() 56 | .verticalScroll(scrollState) 57 | .padding(horizontal = 20.dp) 58 | ) { 59 | Header("Bot Configuration") 60 | 61 | RadioGroup( 62 | label = "Bot Profile", 63 | onSelect = { updatePref { copy(botProfile = it.id) } }, 64 | selected = initOptions.botProfile, 65 | options = ConfigConstants.botProfiles, 66 | ) 67 | 68 | Header("Text to Speech") 69 | 70 | RadioGroup( 71 | label = "Service", 72 | onSelect = { updatePref { copy(ttsProvider = it.id) } }, 73 | selected = initOptions.ttsProvider, 74 | options = initOptions.botProfile.ttsProviders, 75 | ) 76 | 77 | RadioGroup( 78 | label = "Voice", 79 | onSelect = { updatePref { copy(ttsVoice = it.id) } }, 80 | selected = runtimeOptions.ttsVoice, 81 | options = initOptions.ttsProvider.voices 82 | ) 83 | 84 | Header("Language Model") 85 | 86 | RadioGroup( 87 | label = "Service", 88 | onSelect = { updatePref { copy(llmProvider = it.id) } }, 89 | selected = initOptions.llmProvider, 90 | options = initOptions.botProfile.llmProviders 91 | ) 92 | 93 | RadioGroup( 94 | label = "Model", 95 | onSelect = { updatePref { copy(llmModel = it.id) } }, 96 | selected = runtimeOptions.llmModel, 97 | options = initOptions.llmProvider.models 98 | ) 99 | 100 | Header("Speech to Text") 101 | 102 | RadioGroup( 103 | label = "Service", 104 | onSelect = { updatePref { copy(sttProvider = it.id) } }, 105 | selected = initOptions.sttProvider, 106 | options = initOptions.botProfile.sttProviders 107 | ) 108 | 109 | RadioGroup( 110 | label = "Model", 111 | onSelect = { updatePref { copy(sttModel = it.id) } }, 112 | selected = runtimeOptions.sttModel, 113 | options = initOptions.sttProvider.models 114 | ) 115 | 116 | RadioGroup( 117 | label = "Language", 118 | onSelect = { updatePref { copy(sttLanguage = it.id) } }, 119 | selected = runtimeOptions.sttLanguage, 120 | options = runtimeOptions.sttModel.languages 121 | ) 122 | 123 | Spacer(Modifier.height(48.dp)) 124 | } 125 | } 126 | 127 | @Composable 128 | private fun ColumnScope.Header(text: String) { 129 | 130 | Spacer(Modifier.height(42.dp)) 131 | 132 | Text( 133 | text = text, 134 | fontSize = 22.sp, 135 | fontWeight = FontWeight.W700, 136 | style = TextStyles.base 137 | ) 138 | } 139 | 140 | @Composable 141 | private fun ColumnScope.RadioGroup( 142 | label: String, 143 | onSelect: (E) -> Unit, 144 | selected: E, 145 | options: NamedOptionList, 146 | ) { 147 | Spacer(Modifier.height(26.dp)) 148 | 149 | Text( 150 | text = label, 151 | fontSize = 18.sp, 152 | fontWeight = FontWeight.W700, 153 | style = TextStyles.base 154 | ) 155 | 156 | Spacer(Modifier.height(12.dp)) 157 | 158 | val shape = RoundedCornerShape(12.dp) 159 | 160 | Column( 161 | modifier = Modifier 162 | .fillMaxWidth() 163 | .border(1.dp, Colors.textFieldBorder, shape) 164 | .clip(shape) 165 | .background(Color.White) 166 | ) { 167 | var first = true 168 | 169 | for (option in options.options) { 170 | 171 | if (first) { 172 | first = false 173 | } else { 174 | HorizontalDivider(color = Colors.textFieldBorder, thickness = 1.dp) 175 | } 176 | 177 | Row( 178 | modifier = Modifier 179 | .fillMaxWidth() 180 | .clickable { onSelect(option) } 181 | .padding(vertical = 16.dp, horizontal = 20.dp), 182 | verticalAlignment = Alignment.CenterVertically 183 | ) { 184 | Text( 185 | modifier = Modifier.weight(1f), 186 | text = option.name, 187 | fontSize = 18.sp, 188 | fontWeight = FontWeight.W500, 189 | style = TextStyles.base 190 | ) 191 | 192 | Spacer(Modifier.width(8.dp)) 193 | 194 | RadioButton(selected = selected == option, onClick = null) 195 | } 196 | } 197 | } 198 | } 199 | 200 | private fun MutableState.update(action: E.() -> E) { 201 | value = action(value) 202 | } 203 | 204 | @Composable 205 | @Preview 206 | private fun PreviewVoiceClientSettingsPanel() { 207 | RTVIClientTheme { 208 | VoiceClientSettingsPanel( 209 | initOptions = VoiceClientManager.InitOptions.default(), 210 | runtimeOptions = VoiceClientManager.RuntimeOptions.default() 211 | ) 212 | } 213 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object Colors { 6 | val buttonNormal = Color(0xFF374151) 7 | val buttonWarning = Color(0xFFE53935) 8 | val buttonSection = Color(0xFFDFF1FF) 9 | 10 | val activityBackground = Color(0xFFF9FAFB) 11 | val mainSurfaceBackground = Color.White 12 | 13 | val lightGrey = Color(0x7FE5E7EB) 14 | val expiryTimerForeground = Color.Black 15 | val logoBorder = Color(0xFFE2E8F0) 16 | val endButton = Color(0xFF0F172A) 17 | val textFieldBorder = Color(0xFFDFE6EF) 18 | 19 | val botIndicatorBackground = Color(0xFF374151) 20 | val mutedMicBackground = Color(0xFFF04A4A) 21 | val unmutedMicBackground = Color(0xFF616978) 22 | } -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.TextFieldDefaults 5 | import androidx.compose.material3.lightColorScheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | 9 | private val LightColorScheme = lightColorScheme( 10 | primary = Colors.buttonNormal, 11 | secondary = Colors.buttonWarning, 12 | background = Colors.activityBackground, 13 | surface = Colors.mainSurfaceBackground 14 | ) 15 | 16 | @Composable 17 | fun RTVIClientTheme( 18 | content: @Composable () -> Unit 19 | ) { 20 | val colorScheme = LightColorScheme 21 | 22 | MaterialTheme( 23 | colorScheme = colorScheme, 24 | typography = Typography, 25 | content = content 26 | ) 27 | } 28 | 29 | @Composable 30 | fun textFieldColors() = TextFieldDefaults.colors().copy( 31 | unfocusedContainerColor = Colors.activityBackground, 32 | focusedContainerColor = Colors.activityBackground, 33 | focusedIndicatorColor = Color.Transparent, 34 | disabledIndicatorColor = Color.Transparent, 35 | unfocusedIndicatorColor = Color.Transparent, 36 | ) -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.ui.theme 2 | 3 | import co.daily.bots.demo.R 4 | import androidx.compose.material3.Typography 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontFamily 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.unit.sp 10 | 11 | object TextStyles { 12 | val base = TextStyle(fontFamily = FontFamily(Font(R.font.inter))) 13 | } 14 | 15 | // Set of Material typography styles to start with 16 | val Typography = Typography( 17 | bodyLarge = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.Normal, 20 | fontSize = 16.sp, 21 | lineHeight = 24.sp, 22 | letterSpacing = 0.5.sp 23 | ) 24 | /* Other default text styles to override 25 | titleLarge = TextStyle( 26 | fontFamily = FontFamily.Default, 27 | fontWeight = FontWeight.Normal, 28 | fontSize = 22.sp, 29 | lineHeight = 28.sp, 30 | letterSpacing = 0.sp 31 | ), 32 | labelSmall = TextStyle( 33 | fontFamily = FontFamily.Default, 34 | fontWeight = FontWeight.Medium, 35 | fontSize = 11.sp, 36 | lineHeight = 16.sp, 37 | letterSpacing = 0.5.sp 38 | ) 39 | */ 40 | ) -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/utils/RealTimeClock.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.flow.flow 7 | 8 | private val rtcFlowSecs = flow { 9 | while(true) { 10 | val now = Timestamp.now().toEpochMilli() 11 | 12 | val rounded = ((now + 500) / 1000) * 1000 13 | emit(Timestamp.ofEpochMilli(rounded)) 14 | 15 | val target = rounded + 1000 16 | delay(target - now) 17 | } 18 | } 19 | 20 | @Composable 21 | fun rtcStateSecs() = rtcFlowSecs.collectAsState(initial = Timestamp.now()) -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/java/co/daily/bots/demo/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package co.daily.bots.demo.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import java.time.Duration 6 | import java.time.Instant 7 | import java.time.format.DateTimeFormatter 8 | import java.util.Date 9 | 10 | // Wrapper for Compose stability 11 | @Immutable 12 | @JvmInline 13 | value class Timestamp( 14 | val value: Instant 15 | ) : Comparable { 16 | val isInPast: Boolean 17 | get() = value < Instant.now() 18 | 19 | val isInFuture: Boolean 20 | get() = value > Instant.now() 21 | 22 | fun toEpochMilli() = value.toEpochMilli() 23 | 24 | operator fun plus(duration: Duration) = Timestamp(value + duration) 25 | 26 | operator fun minus(duration: Duration) = Timestamp(value - duration) 27 | 28 | operator fun minus(rhs: Timestamp) = Duration.between(rhs.value, value) 29 | 30 | override operator fun compareTo(other: Timestamp) = value.compareTo(other.value) 31 | 32 | fun toISOString(): String = DateTimeFormatter.ISO_INSTANT.format(value) 33 | 34 | override fun toString() = toISOString() 35 | 36 | companion object { 37 | fun now() = Timestamp(Instant.now()) 38 | 39 | fun ofEpochMilli(value: Long) = Timestamp(Instant.ofEpochMilli(value)) 40 | 41 | fun ofEpochSecs(value: Long) = ofEpochMilli(value * 1000) 42 | 43 | fun parse(value: CharSequence) = Timestamp(Instant.parse(value)) 44 | 45 | fun from(date: Date) = Timestamp(date.toInstant()) 46 | } 47 | } 48 | 49 | @Composable 50 | fun formatTimer(duration: Duration): String { 51 | 52 | if (duration.seconds < 0) { 53 | return "0s" 54 | } 55 | 56 | val mins = duration.seconds / 60 57 | val secs = duration.seconds % 60 58 | 59 | return if (mins == 0L) { 60 | "${secs}s" 61 | } else { 62 | "${mins}m ${secs}s" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/chevron_down.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/chevron_right.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/cog.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/console.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/microphone.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/microphone_off.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/phone_hangup.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/timer_outline.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/video.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/drawable/video_off.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/font/inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/font/inter.ttf -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/daily-bots-android-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RTVI Basic Demo 3 | -------------------------------------------------------------------------------- /daily-bots-android-demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |