From 091230891841eae12eebd86fe6c106d9f7619dad Mon Sep 17 00:00:00 2001 From: Thomas Ezan Date: Thu, 24 Jul 2025 15:45:40 +0200 Subject: [PATCH 1/3] Address Live sample compose comments --- .../samples/geminilivetodo/ui/TodoScreen.kt | 36 ++++++++++++++++--- .../geminilivetodo/ui/TodoScreenViewModel.kt | 25 +++---------- .../src/main/res/values/strings.xml | 1 + 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt index 3de62179..5f7470a2 100644 --- a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt +++ b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt @@ -15,7 +15,10 @@ */ package com.android.ai.samples.geminilivetodo.ui +import android.Manifest import android.app.Activity +import android.content.pm.PackageManager +import android.widget.Toast import androidx.activity.compose.LocalActivity import androidx.compose.animation.Animatable import androidx.compose.animation.animateColor @@ -71,6 +74,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.ai.samples.geminilivetodo.R @@ -90,7 +95,8 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { val activity = LocalActivity.current as Activity LaunchedEffect(Unit) { - viewModel.initializeGeminiLive(activity) + requestAudioPermissionIfNeeded(activity) + viewModel.initializeGeminiLive() } Scaffold( @@ -106,7 +112,18 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { floatingActionButton = { MicButton( uiState = uiState, - onToggle = { viewModel.toggleLiveSession(activity) }, + modifier = Modifier, + onToggle = { + if (ContextCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO, + ) == PackageManager.PERMISSION_GRANTED + ) { + viewModel.toggleLiveSession() + } else { + Toast.makeText(activity, R.string.error_permission, Toast.LENGTH_SHORT).show() + } + }, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -121,6 +138,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { ) { TodoInput( text = text, + modifier = Modifier, onTextChange = { text = it }, onAddClick = { viewModel.addTodo(text) @@ -176,7 +194,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { } @Composable -fun TodoInput(text: String, onTextChange: (String) -> Unit, onAddClick: () -> Unit) { +fun TodoInput(text: String, modifier: Modifier, onTextChange: (String) -> Unit, onAddClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -201,7 +219,7 @@ fun TodoInput(text: String, onTextChange: (String) -> Unit, onAddClick: () -> Un } @Composable -fun MicButton(uiState: TodoScreenUiState, onToggle: () -> Unit) { +fun MicButton(uiState: TodoScreenUiState, modifier: Modifier, onToggle: () -> Unit) { if (uiState is TodoScreenUiState.Success) { val micIcon = when { uiState.liveSessionState is LiveSessionState.Ready -> Icons.Filled.MicOff @@ -283,3 +301,13 @@ fun TodoItem(modifier: Modifier, task: Todo, onToggle: () -> Unit, onDelete: () } } } + +private fun requestAudioPermissionIfNeeded(activity: Activity) { + if (ContextCompat.checkSelfPermission( + activity, + Manifest.permission.RECORD_AUDIO, + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 1) + } +} diff --git a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt index 1e0e83bd..8f532667 100644 --- a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt +++ b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt @@ -83,20 +83,14 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe } @SuppressLint("MissingPermission") - fun toggleLiveSession(activity: Activity) { + fun toggleLiveSession() { viewModelScope.launch { if (liveSessionState.value is LiveSessionState.NotReady) return@launch session?.let { if (liveSessionState.value is LiveSessionState.Ready) { - if (ContextCompat.checkSelfPermission( - activity, - Manifest.permission.RECORD_AUDIO, - ) == PackageManager.PERMISSION_GRANTED - ) { - it.startAudioConversation(::handleFunctionCall) - liveSessionState.value = LiveSessionState.Running - } + it.startAudioConversation(::handleFunctionCall) + liveSessionState.value = LiveSessionState.Running } else { it.stopAudioConversation() liveSessionState.value = LiveSessionState.Ready @@ -105,8 +99,7 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe } } - fun initializeGeminiLive(activity: Activity) { - requestAudioPermissionIfNeeded(activity) + fun initializeGeminiLive() { viewModelScope.launch { Log.d(TAG, "Start Gemini Live initialization") val liveGenerationConfig = liveGenerationConfig { @@ -233,14 +226,4 @@ class TodoScreenViewModel @Inject constructor(private val todoRepository: TodoRe } } } - - fun requestAudioPermissionIfNeeded(activity: Activity) { - if (ContextCompat.checkSelfPermission( - activity, - Manifest.permission.RECORD_AUDIO, - ) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } - } } diff --git a/ai-catalog/samples/gemini-live-todo/src/main/res/values/strings.xml b/ai-catalog/samples/gemini-live-todo/src/main/res/values/strings.xml index 60932bb7..6f536cbe 100644 --- a/ai-catalog/samples/gemini-live-todo/src/main/res/values/strings.xml +++ b/ai-catalog/samples/gemini-live-todo/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ Add Error The live session model could not be initialized. + Enable audio recording permission. Dismiss Button to start the live session and interact with the todo list by voice \ No newline at end of file From e7adebea0603a7071518071deac4bc0974c3dabc Mon Sep 17 00:00:00 2001 From: lethargicpanda <205574+lethargicpanda@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:48:37 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20Apply=20Spotless=20formattin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt index 8f532667..0ae8f3cf 100644 --- a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt +++ b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreenViewModel.kt @@ -15,13 +15,8 @@ */ package com.android.ai.samples.geminilivetodo.ui -import android.Manifest import android.annotation.SuppressLint -import android.app.Activity -import android.content.pm.PackageManager import android.util.Log -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.ai.samples.geminilivetodo.data.TodoRepository From 7918b720c29d5c250b3f12675b8915c6db9b0032 Mon Sep 17 00:00:00 2001 From: Thomas Ezan Date: Thu, 24 Jul 2025 16:30:05 +0200 Subject: [PATCH 3/3] Use rememberLauncherForActivityResult --- .../samples/geminilivetodo/ui/TodoScreen.kt | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt index 5f7470a2..68beeea1 100644 --- a/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt +++ b/ai-catalog/samples/gemini-live-todo/src/main/java/com/android/ai/samples/geminilivetodo/ui/TodoScreen.kt @@ -16,10 +16,10 @@ package com.android.ai.samples.geminilivetodo.ui import android.Manifest -import android.app.Activity import android.content.pm.PackageManager import android.widget.Toast -import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Animatable import androidx.compose.animation.animateColor import androidx.compose.animation.core.LinearEasing @@ -69,12 +69,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -91,11 +91,20 @@ import kotlin.collections.reversed fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() var text by remember { mutableStateOf("") } + val context = LocalContext.current - val activity = LocalActivity.current as Activity + val requestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted: Boolean -> + if (isGranted) { + viewModel.toggleLiveSession() + } else { + Toast.makeText(context, R.string.error_permission, Toast.LENGTH_SHORT).show() + } + }, + ) LaunchedEffect(Unit) { - requestAudioPermissionIfNeeded(activity) viewModel.initializeGeminiLive() } @@ -112,16 +121,18 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { floatingActionButton = { MicButton( uiState = uiState, - modifier = Modifier, onToggle = { - if (ContextCompat.checkSelfPermission( - activity, + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + context, Manifest.permission.RECORD_AUDIO, - ) == PackageManager.PERMISSION_GRANTED - ) { - viewModel.toggleLiveSession() - } else { - Toast.makeText(activity, R.string.error_permission, Toast.LENGTH_SHORT).show() + ), + -> { + viewModel.toggleLiveSession() + } + else -> { + requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } } }, ) @@ -194,7 +205,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) { } @Composable -fun TodoInput(text: String, modifier: Modifier, onTextChange: (String) -> Unit, onAddClick: () -> Unit) { +fun TodoInput(text: String, modifier: Modifier = Modifier, onTextChange: (String) -> Unit, onAddClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -219,7 +230,7 @@ fun TodoInput(text: String, modifier: Modifier, onTextChange: (String) -> Unit, } @Composable -fun MicButton(uiState: TodoScreenUiState, modifier: Modifier, onToggle: () -> Unit) { +fun MicButton(uiState: TodoScreenUiState, modifier: Modifier = Modifier, onToggle: () -> Unit) { if (uiState is TodoScreenUiState.Success) { val micIcon = when { uiState.liveSessionState is LiveSessionState.Ready -> Icons.Filled.MicOff @@ -269,7 +280,7 @@ fun MicButton(uiState: TodoScreenUiState, modifier: Modifier, onToggle: () -> Un } @Composable -fun TodoItem(modifier: Modifier, task: Todo, onToggle: () -> Unit, onDelete: () -> Unit) { +fun TodoItem(modifier: Modifier = Modifier, task: Todo, onToggle: () -> Unit, onDelete: () -> Unit) { val defaultBackgroundColor = Color.Transparent val backgroundColor = remember { Animatable(defaultBackgroundColor) } @@ -301,13 +312,3 @@ fun TodoItem(modifier: Modifier, task: Todo, onToggle: () -> Unit, onDelete: () } } } - -private fun requestAudioPermissionIfNeeded(activity: Activity) { - if (ContextCompat.checkSelfPermission( - activity, - Manifest.permission.RECORD_AUDIO, - ) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } -}