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..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
@@ -15,8 +15,11 @@
*/
package com.android.ai.samples.geminilivetodo.ui
-import android.app.Activity
-import androidx.activity.compose.LocalActivity
+import android.Manifest
+import android.content.pm.PackageManager
+import android.widget.Toast
+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
@@ -66,11 +69,13 @@ 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.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.ai.samples.geminilivetodo.R
@@ -86,11 +91,21 @@ 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) {
- viewModel.initializeGeminiLive(activity)
+ viewModel.initializeGeminiLive()
}
Scaffold(
@@ -106,7 +121,20 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) {
floatingActionButton = {
MicButton(
uiState = uiState,
- onToggle = { viewModel.toggleLiveSession(activity) },
+ onToggle = {
+ when (PackageManager.PERMISSION_GRANTED) {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.RECORD_AUDIO,
+ ),
+ -> {
+ viewModel.toggleLiveSession()
+ }
+ else -> {
+ requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
+ }
+ }
+ },
)
},
floatingActionButtonPosition = FabPosition.Center,
@@ -121,6 +149,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) {
) {
TodoInput(
text = text,
+ modifier = Modifier,
onTextChange = { text = it },
onAddClick = {
viewModel.addTodo(text)
@@ -176,7 +205,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) {
}
@Composable
-fun TodoInput(text: String, onTextChange: (String) -> Unit, onAddClick: () -> Unit) {
+fun TodoInput(text: String, modifier: Modifier = Modifier, onTextChange: (String) -> Unit, onAddClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -201,7 +230,7 @@ fun TodoInput(text: String, onTextChange: (String) -> Unit, onAddClick: () -> Un
}
@Composable
-fun MicButton(uiState: TodoScreenUiState, 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
@@ -251,7 +280,7 @@ fun MicButton(uiState: TodoScreenUiState, onToggle: () -> Unit) {
}
@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) }
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..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
@@ -83,20 +78,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 +94,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 +221,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