Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional but there is a library you could use which would be slightly less code and not use Context here

https://github.com/google/accompanist/tree/main/permissions


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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional, but consider using Snackbar here instead of Toast. I think material design guidelines recommend snackbars instead

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a permission error, the best Material 3 UI component depends on the severity and context of the error. Critical, blocking errors that require immediate action should use a dialog, while less severe or temporary issues are best handled with a snackbar or banner.

}
},
)

LaunchedEffect(Unit) {
viewModel.initializeGeminiLive(activity)
viewModel.initializeGeminiLive()
}

Scaffold(
Expand All @@ -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,
Expand All @@ -121,6 +149,7 @@ fun TodoScreen(viewModel: TodoScreenViewModel = hiltViewModel()) {
) {
TodoInput(
text = text,
modifier = Modifier,
onTextChange = { text = it },
onAddClick = {
viewModel.addTodo(text)
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<string name="add_button">Add</string>
<string name="error_title">Error</string>
<string name="error_message">The live session model could not be initialized.</string>
<string name="error_permission">Enable audio recording permission.</string>
<string name="dismiss_button">Dismiss</string>
<string name="interact_with_todolist_by_voice">Button to start the live session and interact with the todo list by voice</string>
</resources>
Loading