├── .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 |
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 |
5 |
--------------------------------------------------------------------------------
/files/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/files/screenshot.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | accompanistPermissions = "0.34.0"
3 | agp = "8.5.2"
4 | constraintlayoutCompose = "1.0.1"
5 | rtviClientDaily = "0.2.1"
6 | kotlin = "2.0.20"
7 | coreKtx = "1.13.1"
8 | lifecycleRuntimeKtx = "2.8.6"
9 | activityCompose = "1.9.2"
10 | composeBom = "2024.09.03"
11 | kotlinxSerializationJson = "1.7.1"
12 | kotlinxSerializationPlugin = "2.0.20"
13 |
14 | [libraries]
15 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
16 | androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
17 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
18 | rtvi-client-daily = { module = "ai.rtvi:client-daily", version.ref = "rtviClientDaily" }
19 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
20 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
21 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
22 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
23 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
24 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
25 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
26 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
27 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
28 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
29 |
30 | [plugins]
31 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
32 | android-application = { id = "com.android.application", version.ref = "agp" }
33 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
34 | jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerializationPlugin" }
35 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daily-demos/rtvi-client-android-demo/6b36152ebf0225f7558e7ed707bfe91f873dfbeb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Aug 05 13:01:27 BST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Daily Bots Android Demo"
23 | include(":daily-bots-android-demo")
24 |
25 | /*
26 | include(":rtvi-client-android-daily")
27 | project(":rtvi-client-android-daily").projectDir = file("/../../rtvi-client-android-daily/rtvi-client-android-daily")
28 | */
29 |
--------------------------------------------------------------------------------