diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistActivity.kt index 70d064e43a1..ce515958ccf 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistActivity.kt @@ -11,6 +11,7 @@ import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.lifecycle.lifecycleScope @@ -18,14 +19,14 @@ import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback import io.homeassistant.companion.android.BaseActivity import io.homeassistant.companion.android.assist.service.AssistVoiceInteractionService -import io.homeassistant.companion.android.assist.ui.AssistSheetView +import io.homeassistant.companion.android.assist.ui.AssistSheet import io.homeassistant.companion.android.common.assist.AssistViewModelBase +import io.homeassistant.companion.android.common.compose.theme.HATheme import io.homeassistant.companion.android.common.data.servers.ServerManager import io.homeassistant.companion.android.common.util.SdkVersion import io.homeassistant.companion.android.frontend.navigation.FrontendTarget import io.homeassistant.companion.android.launch.LaunchActivity import io.homeassistant.companion.android.launch.intentLaunchWithNavigateTo -import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme import javax.inject.Inject import kotlinx.coroutines.launch @@ -78,6 +79,7 @@ class AssistActivity : BaseActivity() { ActivityResultContracts.RequestPermission(), ) { viewModel.onPermissionResult(it) } + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) updateShowWhenLocked() @@ -126,8 +128,8 @@ class AssistActivity : BaseActivity() { return@setContent } - HomeAssistantAppTheme { - AssistSheetView( + HATheme { + AssistSheet( conversation = viewModel.conversation, pipelines = viewModel.pipelines, inputMode = viewModel.inputMode, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistViewModel.kt index 6581cc57ac1..5a9dc795c4e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/assist/AssistViewModel.kt @@ -3,6 +3,7 @@ package io.homeassistant.companion.android.assist import android.app.Application import android.content.Intent import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -67,7 +68,14 @@ class AssistViewModel @AssistedInject constructor( private val startMessage = AssistMessage(application.getString(commonR.string.assist_how_can_i_assist), isInput = false) private val _conversation = mutableStateListOf(startMessage) - val conversation: List = _conversation + + /** + * Conversation messages to display. The input placeholder marking an active voice recording is + * hidden: listening is already conveyed by the microphone button animation. + */ + val conversation: List by derivedStateOf { + _conversation.filterNot { it.isInput && it.isPlaceholder } + } private val _pipelines = mutableStateListOf() val pipelines: List = _pipelines diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheet.kt b/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheet.kt new file mode 100644 index 00000000000..618dd8c9540 --- /dev/null +++ b/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheet.kt @@ -0,0 +1,620 @@ +package io.homeassistant.companion.android.assist.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.outlined.Keyboard +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.common.assist.AssistViewModelBase.AssistInputMode +import io.homeassistant.companion.android.common.compose.composable.ButtonVariant +import io.homeassistant.companion.android.common.compose.composable.HAHorizontalDivider +import io.homeassistant.companion.android.common.compose.composable.HAIconButton +import io.homeassistant.companion.android.common.compose.composable.HAModalBottomSheet +import io.homeassistant.companion.android.common.compose.composable.HATextField +import io.homeassistant.companion.android.common.compose.composable.rememberHAModalBottomSheetState +import io.homeassistant.companion.android.common.compose.theme.HABorderWidth +import io.homeassistant.companion.android.common.compose.theme.HADimens +import io.homeassistant.companion.android.common.compose.theme.HARadius +import io.homeassistant.companion.android.common.compose.theme.HASize +import io.homeassistant.companion.android.common.compose.theme.HATextStyle +import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview +import io.homeassistant.companion.android.common.compose.theme.LocalHAColorScheme + +private val MIC_BUTTON_CONTAINER_SIZE = HADimens.SPACE16 +private val MIC_BUTTON_SIZE = HADimens.SPACE12 +private val MIC_ICON_SIZE = HADimens.SPACE7 +private const val MIC_RIPPLE_COUNT = 2 +private const val MIC_RIPPLE_DURATION_MS = 1400 +private const val MIC_RIPPLE_MAX_SCALE = 1.6f +private const val MIC_RIPPLE_START_ALPHA = 0.5f + +private const val BUBBLE_ENTER_DURATION_MS = 220 + +private const val TYPING_DOT_COUNT = 3 +private const val TYPING_DOT_PULSE_DURATION_MS = 300 +private const val TYPING_DOT_MIN_ALPHA = 0.3f + +/** + * Modal bottom sheet hosting the Assist conversation: a header with the pipeline selector, the + * conversation history, and the text/voice input controls. + * + * @param conversation Messages exchanged so far, oldest first. + * @param pipelines All pipelines the user can select from, across servers. + * @param inputMode Current input mode, or null while the pipeline is loading. + * @param currentPipeline The pipeline in use, or null while loading. + * @param fromFrontend Whether Assist was opened from the frontend, used to pick the sheet title. + * @param onSelectPipeline Invoked with the server ID and pipeline ID of the selected pipeline. + * @param onManagePipelines Invoked when the user opens pipeline management, or null to hide the entry. + * @param onChangeInput Invoked when the user toggles between text and voice input. + * @param onTextInput Invoked with the text the user submitted. + * @param onMicrophoneInput Invoked when the user taps the microphone button. + * @param onHide Invoked when the user dismisses the sheet. + * @param bottomSheetState State of the sheet, exposed so tests can provide an already expanded state. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AssistSheet( + conversation: List, + pipelines: List, + inputMode: AssistInputMode?, + currentPipeline: AssistUiPipeline?, + fromFrontend: Boolean, + onSelectPipeline: (Int, String) -> Unit, + onManagePipelines: (() -> Unit)?, + onChangeInput: () -> Unit, + onTextInput: (String) -> Unit, + onMicrophoneInput: () -> Unit, + onHide: () -> Unit, + modifier: Modifier = Modifier, + bottomSheetState: SheetState = rememberHAModalBottomSheetState(skipPartiallyExpanded = true), +) { + HAModalBottomSheet( + bottomSheetState = bottomSheetState, + onDismissRequest = onHide, + dragHandle = {}, + modifier = modifier, + ) { + AssistSheetContent( + conversation = conversation, + pipelines = pipelines, + inputMode = inputMode, + currentPipeline = currentPipeline, + fromFrontend = fromFrontend, + onSelectPipeline = onSelectPipeline, + onManagePipelines = onManagePipelines, + onChangeInput = onChangeInput, + onTextInput = onTextInput, + onMicrophoneInput = onMicrophoneInput, + ) + } +} + +@Composable +private fun AssistSheetContent( + conversation: List, + pipelines: List, + inputMode: AssistInputMode?, + currentPipeline: AssistUiPipeline?, + fromFrontend: Boolean, + onSelectPipeline: (Int, String) -> Unit, + onManagePipelines: (() -> Unit)?, + onChangeInput: () -> Unit, + onTextInput: (String) -> Unit, + onMicrophoneInput: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = HADimens.SPACE4) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .imePadding(), + ) { + AssistSheetHeader( + pipelines = pipelines, + currentPipeline = currentPipeline, + fromFrontend = fromFrontend, + onSelectPipeline = onSelectPipeline, + onManagePipelines = onManagePipelines, + modifier = Modifier.padding(top = HADimens.SPACE4), + ) + AssistConversation( + conversation = conversation, + modifier = Modifier.weight(1f, fill = false), + ) + AssistSheetControls( + inputMode = inputMode, + onChangeInput = onChangeInput, + onTextInput = onTextInput, + onMicrophoneInput = onMicrophoneInput, + ) + Spacer(modifier = Modifier.height(HADimens.SPACE2)) + } +} + +@Composable +private fun AssistSheetHeader( + pipelines: List, + currentPipeline: AssistUiPipeline?, + fromFrontend: Boolean, + onSelectPipeline: (Int, String) -> Unit, + onManagePipelines: (() -> Unit)?, + modifier: Modifier = Modifier, +) = Column(verticalArrangement = Arrangement.Center, modifier = modifier) { + Text( + text = stringResource(if (fromFrontend) commonR.string.assist else commonR.string.app_name), + style = HATextStyle.HeadlineMedium.copy(textAlign = TextAlign.Start), + ) + if (currentPipeline != null) { + AssistPipelineSelector( + pipelines = pipelines, + currentPipeline = currentPipeline, + onSelectPipeline = onSelectPipeline, + onManagePipelines = onManagePipelines, + ) + } +} + +@Composable +private fun AssistPipelineSelector( + pipelines: List, + currentPipeline: AssistUiPipeline, + onSelectPipeline: (Int, String) -> Unit, + onManagePipelines: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + val colorScheme = LocalHAColorScheme.current + Box(modifier = modifier) { + var showPipelineList by remember { mutableStateOf(false) } + val showServerName = remember(pipelines) { pipelines.distinctBy { it.serverId }.size > 1 } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(HARadius.S)) + .clickable(role = Role.DropdownList) { showPipelineList = !showPipelineList }, + ) { + Text( + text = pipelineDisplayName(currentPipeline, showServerName), + style = HATextStyle.BodyMedium.copy(textAlign = TextAlign.Start), + ) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(commonR.string.assist_change_pipeline), + tint = colorScheme.colorTextSecondary, + modifier = Modifier + .padding(start = HADimens.SPACE1) + .size(HADimens.SPACE4), + ) + } + + DropdownMenu( + expanded = showPipelineList, + onDismissRequest = { showPipelineList = false }, + containerColor = colorScheme.colorSurfaceDefault, + shape = RoundedCornerShape(HARadius.XL), + ) { + pipelines.forEach { pipeline -> + val isSelected = pipeline.serverId == currentPipeline.serverId && pipeline.id == currentPipeline.id + DropdownMenuItem( + text = { + Text( + text = pipelineDisplayName(pipeline, showServerName), + style = HATextStyle.Body, + color = colorScheme.colorTextPrimary, + ) + }, + trailingIcon = if (isSelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = colorScheme.colorFillPrimaryLoudResting, + modifier = Modifier.size(HADimens.SPACE5), + ) + } + } else { + null + }, + onClick = { + onSelectPipeline(pipeline.serverId, pipeline.id) + showPipelineList = false + }, + ) + } + if (onManagePipelines != null) { + HAHorizontalDivider() + DropdownMenuItem( + text = { + Text( + text = stringResource(commonR.string.assist_manage_pipelines), + style = HATextStyle.Body, + color = colorScheme.colorTextPrimary, + ) + }, + onClick = onManagePipelines, + ) + } + } + } +} + +private fun pipelineDisplayName(pipeline: AssistUiPipeline, showServerName: Boolean): String = + if (showServerName) "${pipeline.serverName}: ${pipeline.name}" else pipeline.name + +@Composable +private fun AssistConversation(conversation: List, modifier: Modifier = Modifier) { + val lazyListState = rememberLazyListState() + LaunchedEffect(conversation.size, conversation.lastOrNull()?.message?.length) { + lazyListState.animateScrollToItem(conversation.size) + } + + LazyColumn( + state = lazyListState, + modifier = modifier.padding(vertical = HADimens.SPACE4), + ) { + itemsIndexed(conversation, key = { index, _ -> index }) { _, message -> + SpeechBubble(message = message, modifier = Modifier.animateItem()) + } + } +} + +@Composable +private fun AssistSheetControls( + inputMode: AssistInputMode?, + onChangeInput: () -> Unit, + onTextInput: (String) -> Unit, + onMicrophoneInput: () -> Unit, + modifier: Modifier = Modifier, +) = Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) { + if (inputMode == null) { // Pipeline info has not yet loaded, empty space for now + Spacer(modifier = Modifier.height(HADimens.SPACE16)) + return@Row + } + + if (inputMode == AssistInputMode.BLOCKED) { // No info and not recoverable, no space + return@Row + } + + val focusRequester = remember { FocusRequester() } + LaunchedEffect(inputMode) { + if (inputMode == AssistInputMode.TEXT || inputMode == AssistInputMode.TEXT_ONLY) { + focusRequester.requestFocus() + } + } + + if (inputMode == AssistInputMode.TEXT || inputMode == AssistInputMode.TEXT_ONLY) { + AssistTextInput( + textOnly = inputMode == AssistInputMode.TEXT_ONLY, + focusRequester = focusRequester, + onChangeInput = onChangeInput, + onTextInput = onTextInput, + ) + } else { + AssistVoiceInput( + isActive = inputMode == AssistInputMode.VOICE_ACTIVE, + onChangeInput = onChangeInput, + onMicrophoneInput = onMicrophoneInput, + ) + } +} + +@Composable +private fun RowScope.AssistTextInput( + textOnly: Boolean, + focusRequester: FocusRequester, + onChangeInput: () -> Unit, + onTextInput: (String) -> Unit, +) { + var text by rememberSaveable { mutableStateOf("") } + val submit = { + if (text.isNotBlank()) { + onTextInput(text) + text = "" + } + } + HATextField( + value = text, + onValueChange = { text = it }, + placeholder = { Text(stringResource(commonR.string.assist_enter_a_request)) }, + modifier = Modifier + .weight(1f) + .focusRequester(focusRequester), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { submit() }), + ) + val inputIsSend = text.isNotBlank() || textOnly + HAIconButton( + icon = if (inputIsSend) Icons.AutoMirrored.Filled.Send else Icons.Filled.Mic, + contentDescription = stringResource( + if (inputIsSend) commonR.string.assist_send_text else commonR.string.assist_start_listening, + ), + onClick = { + if (text.isNotBlank()) { + submit() + } else if (!textOnly) { + onChangeInput() + } + }, + enabled = !textOnly || text.isNotBlank(), + variant = ButtonVariant.PRIMARY, + ) +} + +@Composable +private fun RowScope.AssistVoiceInput(isActive: Boolean, onChangeInput: () -> Unit, onMicrophoneInput: () -> Unit) { + Spacer(modifier = Modifier.size(HADimens.SPACE12)) + Spacer(modifier = Modifier.weight(0.5f)) + AssistMicrophoneButton(isActive = isActive, onClick = onMicrophoneInput) + Spacer(modifier = Modifier.weight(0.5f)) + HAIconButton( + icon = Icons.Outlined.Keyboard, + contentDescription = stringResource(commonR.string.assist_enter_text), + onClick = onChangeInput, + variant = ButtonVariant.NEUTRAL, + ) +} + +@Composable +private fun AssistMicrophoneButton(isActive: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val colorScheme = LocalHAColorScheme.current + Box( + modifier = modifier.size(MIC_BUTTON_CONTAINER_SIZE), + contentAlignment = Alignment.Center, + ) { + if (isActive) { + val transition = rememberInfiniteTransition(label = "micRipples") + repeat(MIC_RIPPLE_COUNT) { index -> + val progress by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(MIC_RIPPLE_DURATION_MS, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(index * MIC_RIPPLE_DURATION_MS / MIC_RIPPLE_COUNT), + ), + label = "micRipple$index", + ) + Box( + modifier = Modifier + .size(MIC_BUTTON_SIZE) + .graphicsLayer { + val scale = 1f + (MIC_RIPPLE_MAX_SCALE - 1f) * progress + scaleX = scale + scaleY = scale + alpha = MIC_RIPPLE_START_ALPHA * (1f - progress) + } + .background(color = colorScheme.colorFillPrimaryLoudResting, shape = CircleShape), + ) + } + } + val buttonBackground = if (isActive) { + Modifier.background(color = colorScheme.colorFillPrimaryLoudResting, shape = CircleShape) + } else { + Modifier.border( + width = HABorderWidth.S, + color = colorScheme.colorBorderNeutralNormal, + shape = CircleShape, + ) + } + Box( + modifier = Modifier + .size(MIC_BUTTON_SIZE) + .clip(CircleShape) + .then(buttonBackground) + .clickable(role = Role.Button) { onClick() }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Mic, + contentDescription = stringResource( + if (isActive) commonR.string.assist_stop_listening else commonR.string.assist_start_listening, + ), + tint = if (isActive) colorScheme.colorOnPrimaryLoud else colorScheme.colorTextPrimary, + modifier = Modifier.size(MIC_ICON_SIZE), + ) + } + } +} + +@Composable +private fun SpeechBubble(message: AssistMessage, modifier: Modifier = Modifier) { + val colorScheme = LocalHAColorScheme.current + val isInput = message.isInput + val backgroundColor = when { + message.isError -> colorScheme.colorFillDangerLoudResting + isInput -> colorScheme.colorFillPrimaryLoudResting + else -> colorScheme.colorFillPrimaryQuietResting + } + val contentColor = when { + message.isError -> colorScheme.colorOnDangerLoud + isInput -> colorScheme.colorOnPrimaryLoud + else -> colorScheme.colorTextPrimary + } + + // Animate each bubble in once, when it first appears in the conversation. + // In inspection mode (previews and screenshot tests) start visible, otherwise the static + // frame would capture the bubble before it animated in. + val startVisible = LocalInspectionMode.current + val enterTransition = remember { MutableTransitionState(startVisible).apply { targetState = true } } + AnimatedVisibility( + visibleState = enterTransition, + enter = fadeIn(tween(BUBBLE_ENTER_DURATION_MS)) + + slideInVertically(tween(BUBBLE_ENTER_DURATION_MS)) { it / 2 }, + modifier = modifier, + ) { + Row( + horizontalArrangement = if (isInput) Arrangement.End else Arrangement.Start, + modifier = Modifier + .fillMaxWidth() + .padding( + start = if (isInput) HADimens.SPACE6 else HADimens.SPACE0, + end = if (isInput) HADimens.SPACE0 else HADimens.SPACE6, + top = HADimens.SPACE2, + bottom = HADimens.SPACE2, + ), + ) { + Box( + modifier = Modifier + .background( + color = backgroundColor, + shape = RoundedCornerShape( + topStart = HARadius.XL, + topEnd = HARadius.XL, + bottomStart = if (isInput) HARadius.XL else HARadius.S, + bottomEnd = if (isInput) HARadius.S else HARadius.XL, + ), + ) + .padding(horizontal = HADimens.SPACE3, vertical = HADimens.SPACE2), + ) { + if (message.isPlaceholder) { + TypingIndicator(color = contentColor) + } else { + Text( + text = message.message, + style = HATextStyle.Body.copy(textAlign = TextAlign.Start, color = contentColor), + ) + } + } + } + } +} + +/** + * Three dots pulsing one after the other, shown while waiting for the other side of the + * conversation, mirroring the typing indicator on iOS. + */ +@Composable +private fun TypingIndicator(color: Color, modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "typingIndicator") + Row( + horizontalArrangement = Arrangement.spacedBy(HADimens.SPACE1), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.heightIn(min = HADimens.SPACE6), + ) { + repeat(TYPING_DOT_COUNT) { index -> + val alpha by transition.animateFloat( + initialValue = TYPING_DOT_MIN_ALPHA, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(TYPING_DOT_PULSE_DURATION_MS * TYPING_DOT_COUNT), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(index * TYPING_DOT_PULSE_DURATION_MS), + ), + label = "typingDotAlpha$index", + ) + Box( + modifier = Modifier + .size(HASize.X2S) + .graphicsLayer { this.alpha = alpha } + .background(color = color, shape = CircleShape), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun AssistSheetContentPreview() { + HAThemeForPreview(modifier = Modifier.fillMaxSize()) { + AssistSheet( + conversation = listOf( + AssistMessage("How can I assist?", isInput = false), + AssistMessage("Turn on the living room lights", isInput = true), + AssistMessage.placeholder(isInput = false), + ), + pipelines = emptyList(), + inputMode = AssistInputMode.VOICE_INACTIVE, + currentPipeline = AssistUiPipeline(serverId = 0, serverName = "Home", id = "pipeline", name = "Assist"), + fromFrontend = true, + onSelectPipeline = { _, _ -> }, + onManagePipelines = null, + onChangeInput = {}, + onTextInput = {}, + onMicrophoneInput = {}, + onHide = {}, + ) + } +} + +@Preview +@Composable +private fun SpeechBubblePreview() { + HAThemeForPreview { + Column { + SpeechBubble(message = AssistMessage("How can I assist?", isInput = false)) + SpeechBubble(message = AssistMessage("Turn on the lights", isInput = true)) + SpeechBubble(message = AssistMessage("Something went wrong", isInput = false, isError = true)) + SpeechBubble(message = AssistMessage.placeholder(isInput = false)) + } + } +} diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetView.kt b/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetView.kt deleted file mode 100644 index 5a35319fedb..00000000000 --- a/app/src/main/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetView.kt +++ /dev/null @@ -1,414 +0,0 @@ -package io.homeassistant.companion.android.assist.ui - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalContentColor -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.outlined.Keyboard -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.mikepenz.iconics.compose.Image -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import io.homeassistant.companion.android.R -import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.assist.AssistViewModelBase -import io.homeassistant.companion.android.util.compose.safeScreenHeight -import kotlinx.coroutines.launch - -private val HEADER_HEIGHT = 48.dp -private val CONTROLS_HEIGHT = 112.dp - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AssistSheetView( - conversation: List, - pipelines: List, - inputMode: AssistViewModelBase.AssistInputMode?, - currentPipeline: AssistUiPipeline?, - fromFrontend: Boolean, - onSelectPipeline: (Int, String) -> Unit, - onManagePipelines: (() -> Unit)?, - onChangeInput: () -> Unit, - onTextInput: (String) -> Unit, - onMicrophoneInput: () -> Unit, - onHide: () -> Unit, - modifier: Modifier = Modifier, -) { - val coroutineScope = rememberCoroutineScope() - val state = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Expanded, - skipHalfExpanded = true, - confirmValueChange = { - if (it == ModalBottomSheetValue.Hidden) { - coroutineScope.launch { onHide() } - } - true - }, - ) - - val sheetCornerRadius = dimensionResource(R.dimen.bottom_sheet_corner_radius) - - ModalBottomSheetLayout( - sheetState = state, - sheetShape = RoundedCornerShape(topStart = sheetCornerRadius, topEnd = sheetCornerRadius), - scrimColor = Color.Transparent, - modifier = modifier.fillMaxSize(), - sheetContent = { - Box( - Modifier - .navigationBarsPadding() - .imePadding() - .padding(16.dp), - ) { - Column { - val lazyListState = rememberLazyListState() - LaunchedEffect("${conversation.size}.${conversation.lastOrNull()?.message?.length}") { - lazyListState.animateScrollToItem(conversation.size) - } - - AssistSheetHeader( - pipelines = pipelines, - currentPipeline = currentPipeline, - fromFrontend = fromFrontend, - onSelectPipeline = onSelectPipeline, - onManagePipelines = onManagePipelines, - ) - LazyColumn( - state = lazyListState, - modifier = Modifier - .padding(vertical = 16.dp) - .heightIn( - max = safeScreenHeight() - HEADER_HEIGHT - CONTROLS_HEIGHT, - ), - ) { - items(conversation) { - SpeechBubble(text = it.message, isResponse = !it.isInput) - } - } - AssistSheetControls( - inputMode, - onChangeInput, - onTextInput, - onMicrophoneInput, - ) - } - } - }, - ) { - // The rest of the screen is empty - } -} - -@Composable -fun AssistSheetHeader( - pipelines: List, - currentPipeline: AssistUiPipeline?, - fromFrontend: Boolean, - onSelectPipeline: (Int, String) -> Unit, - onManagePipelines: (() -> Unit)?, - modifier: Modifier = Modifier, -) = Column(verticalArrangement = Arrangement.Center, modifier = modifier) { - Text( - text = stringResource(if (fromFrontend) commonR.string.assist else commonR.string.app_name), - fontSize = 20.sp, - letterSpacing = 0.25.sp, - ) - if (currentPipeline != null) { - val color = colorResource(commonR.color.colorOnSurfaceVariant) - - Row(modifier = Modifier.fillMaxWidth()) { - Box { - var pipelineShowList by remember { mutableStateOf(false) } - val pipelineShowServer by rememberSaveable(pipelines.size) { - mutableStateOf(pipelines.distinctBy { it.serverId }.size > 1) - } - Row( - modifier = Modifier.clickable { pipelineShowList = !pipelineShowList }, - ) { - Text( - text = if (pipelineShowServer) { - "${currentPipeline.serverName}: ${currentPipeline.name}" - } else { - currentPipeline.name - }, - color = color, - style = MaterialTheme.typography.caption, - ) - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = stringResource(commonR.string.assist_change_pipeline), - modifier = Modifier - .height(16.dp) - .padding(start = 4.dp), - tint = color, - ) - } - DropdownMenu( - expanded = pipelineShowList, - onDismissRequest = { pipelineShowList = false }, - ) { - pipelines.forEach { - val isSelected = - it.serverId == currentPipeline.serverId && it.id == currentPipeline.id - DropdownMenuItem(onClick = { - onSelectPipeline(it.serverId, it.id) - pipelineShowList = false - }) { - Text( - text = if (pipelineShowServer) "${it.serverName}: ${it.name}" else it.name, - fontWeight = if (isSelected) FontWeight.SemiBold else null, - ) - } - } - if (onManagePipelines != null) { - Divider() - DropdownMenuItem(onClick = { onManagePipelines() }) { - Text(stringResource(commonR.string.assist_manage_pipelines)) - } - } - } - } - } - } -} - -@Composable -fun AssistSheetControls( - inputMode: AssistViewModelBase.AssistInputMode?, - onChangeInput: () -> Unit, - onTextInput: (String) -> Unit, - onMicrophoneInput: () -> Unit, - modifier: Modifier = Modifier, -) = Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - if (inputMode == null) { // Pipeline info has not yet loaded, empty space for now - Spacer(modifier = Modifier.height(64.dp)) - return@Row - } - - if (inputMode == AssistViewModelBase.AssistInputMode.BLOCKED) { // No info and not recoverable, no space - return@Row - } - - val focusRequester = remember { FocusRequester() } - LaunchedEffect(inputMode) { - if (inputMode == AssistViewModelBase.AssistInputMode.TEXT || - inputMode == AssistViewModelBase.AssistInputMode.TEXT_ONLY - ) { - focusRequester.requestFocus() - } - } - - if (inputMode == AssistViewModelBase.AssistInputMode.TEXT || - inputMode == AssistViewModelBase.AssistInputMode.TEXT_ONLY - ) { - var text by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue()) - } - TextField( - value = text, - onValueChange = { text = it }, - label = { Text(stringResource(commonR.string.assist_enter_a_request)) }, - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester), - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), - keyboardActions = KeyboardActions(onSend = { - if (text.text.isNotBlank()) { - onTextInput(text.text) - text = TextFieldValue("") - } - }), - ) - IconButton( - onClick = { - if (text.text.isNotBlank()) { - onTextInput(text.text) - text = TextFieldValue("") - } else if (inputMode != AssistViewModelBase.AssistInputMode.TEXT_ONLY) { - onChangeInput() - } - }, - enabled = (inputMode != AssistViewModelBase.AssistInputMode.TEXT_ONLY || text.text.isNotBlank()), - ) { - val inputIsSend = text.text.isNotBlank() || inputMode == AssistViewModelBase.AssistInputMode.TEXT_ONLY - Image( - asset = if (inputIsSend) CommunityMaterial.Icon3.cmd_send else CommunityMaterial.Icon3.cmd_microphone, - contentDescription = stringResource( - if (inputIsSend) commonR.string.assist_send_text else commonR.string.assist_start_listening, - ), - colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), - modifier = Modifier.size(24.dp), - ) - } - } else { - Spacer(Modifier.size(48.dp)) - Spacer(Modifier.weight(0.5f)) - Box( - modifier = Modifier.size(64.dp), - contentAlignment = Alignment.Center, - ) { - val inputIsActive = inputMode == AssistViewModelBase.AssistInputMode.VOICE_ACTIVE - if (inputIsActive) { - val transition = rememberInfiniteTransition() - val scale by transition.animateFloat( - initialValue = 1f, - targetValue = 1.2f, - animationSpec = infiniteRepeatable( - animation = tween(600, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, - ), - ) - Box( - modifier = Modifier - .size(48.dp) - .scale(scale) - .background(color = colorResource(commonR.color.colorSpeechText), shape = CircleShape) - .clip(CircleShape), - ) - } - OutlinedButton( - onClick = { onMicrophoneInput() }, - modifier = Modifier.size(48.dp), - shape = CircleShape, - border = if (inputIsActive) null else ButtonDefaults.outlinedBorder, - colors = if (inputIsActive) { - ButtonDefaults.outlinedButtonColors(backgroundColor = Color.Transparent, contentColor = Color.Black) - } else { - ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface) - }, - contentPadding = PaddingValues(all = 0.dp), - ) { - Image( - asset = CommunityMaterial.Icon3.cmd_microphone, - contentDescription = stringResource( - if (inputIsActive) { - commonR.string.assist_stop_listening - } else { - commonR.string.assist_start_listening - }, - ), - colorFilter = ColorFilter.tint(LocalContentColor.current), - modifier = Modifier.size(28.dp), - ) - } - } - Spacer(Modifier.weight(0.5f)) - IconButton({ onChangeInput() }) { - Icon( - imageVector = Icons.Outlined.Keyboard, - contentDescription = stringResource(commonR.string.assist_enter_text), - tint = MaterialTheme.colors.onSurface, - ) - } - } -} - -@Composable -fun SpeechBubble(text: String, isResponse: Boolean, modifier: Modifier = Modifier) { - Row( - horizontalArrangement = if (isResponse) Arrangement.Start else Arrangement.End, - modifier = modifier - .fillMaxWidth() - .padding( - start = if (isResponse) 0.dp else 24.dp, - end = if (isResponse) 24.dp else 0.dp, - top = 8.dp, - bottom = 8.dp, - ), - ) { - Box( - modifier = Modifier - .background( - if (isResponse) { - colorResource(commonR.color.colorAccent) - } else { - colorResource(commonR.color.colorSpeechText) - }, - AbsoluteRoundedCornerShape( - topLeft = 12.dp, - topRight = 12.dp, - bottomLeft = if (isResponse) 0.dp else 12.dp, - bottomRight = if (isResponse) 12.dp else 0.dp, - ), - ) - .padding(4.dp), - ) { - Text( - text = text, - color = if (isResponse) { - Color.White - } else { - Color.Black - }, - modifier = Modifier - .padding(2.dp), - ) - } - } -} diff --git a/app/src/main/res/xml/changelog_master.xml b/app/src/main/res/xml/changelog_master.xml index 15a2cd55f3a..ae46c27e135 100755 --- a/app/src/main/res/xml/changelog_master.xml +++ b/app/src/main/res/xml/changelog_master.xml @@ -5,6 +5,7 @@ Android 17: added assistant volume level sensor + notification command for control Health Connect sleep duration sensor now ignores awake and out of bed time Modernized settings for entity widgets and tiles + Modernized the Assist conversation screen Bug fixes and dependency updates @@ -14,6 +15,7 @@ Android 17: added assistant volume level sensor + notification command for control Health Connect sleep duration sensor now ignores awake and out of bed time + Modernized the Assist conversation screen Bug fixes and dependency updates diff --git a/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest.kt b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest.kt new file mode 100644 index 00000000000..92a27aed5a6 --- /dev/null +++ b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest.kt @@ -0,0 +1,188 @@ +package io.homeassistant.companion.android.assist.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.android.tools.screenshot.PreviewTest +import io.homeassistant.companion.android.common.assist.AssistViewModelBase.AssistInputMode +import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview +import io.homeassistant.companion.android.util.compose.HAPreviews + +class AssistSheetScreenshotTest { + + private val singleServerPipelines = listOf( + AssistUiPipeline(serverId = 1, serverName = "Home", id = "assist", name = "Home Assistant"), + AssistUiPipeline(serverId = 1, serverName = "Home", id = "custom", name = "Custom assistant"), + ) + + private val multiServerPipelines = listOf( + AssistUiPipeline(serverId = 1, serverName = "Home", id = "assist", name = "Home Assistant"), + AssistUiPipeline(serverId = 2, serverName = "Vacation house", id = "assist", name = "Home Assistant"), + ) + + private val shortConversation = listOf( + AssistMessage("How can I assist?", isInput = false), + AssistMessage("Turn on the living room lights", isInput = true), + AssistMessage("Turned on the lights", isInput = false), + ) + + private val longAnswerConversation = listOf( + AssistMessage("How can I assist?", isInput = false), + AssistMessage("Tell me everything about lorem ipsum", isInput = true), + AssistMessage( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut " + + "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " + + "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " + + "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat " + + "cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. " + + "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque " + + "laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi " + + "architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit " + + "aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione " + + "voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit " + + "amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut " + + "labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum " + + "exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi " + + "consequatur. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam " + + "nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.", + isInput = false, + ), + ) + + @PreviewTest + @HAPreviews + @Composable + fun `Assist conversation with voice input`() { + AssistSheetScreenshot( + conversation = shortConversation, + inputMode = AssistInputMode.VOICE_INACTIVE, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist listening`() { + AssistSheetScreenshot( + conversation = listOf(AssistMessage("How can I assist?", isInput = false)), + inputMode = AssistInputMode.VOICE_ACTIVE, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist waiting for a response`() { + AssistSheetScreenshot( + conversation = listOf( + AssistMessage("How can I assist?", isInput = false), + AssistMessage("Turn on the living room lights", isInput = true), + AssistMessage.placeholder(isInput = false), + ), + inputMode = AssistInputMode.TEXT, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist text input with pipelines from multiple servers`() { + AssistSheetScreenshot( + conversation = shortConversation, + inputMode = AssistInputMode.TEXT, + pipelines = multiServerPipelines, + currentPipeline = multiServerPipelines.first(), + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist error response`() { + AssistSheetScreenshot( + conversation = listOf( + AssistMessage("How can I assist?", isInput = false), + AssistMessage("Turn on the living room lights", isInput = true), + AssistMessage("Something went wrong", isInput = false, isError = true), + ), + inputMode = AssistInputMode.VOICE_INACTIVE, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist blocked without pipeline`() { + AssistSheetScreenshot( + conversation = listOf(AssistMessage("Blocked", isInput = false)), + inputMode = AssistInputMode.BLOCKED, + pipelines = emptyList(), + currentPipeline = null, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist opened from outside the app shows the app name`() { + AssistSheetScreenshot( + conversation = shortConversation, + inputMode = AssistInputMode.VOICE_INACTIVE, + fromFrontend = false, + ) + } + + @PreviewTest + @HAPreviews + @Composable + fun `Assist long answer filling the screen`() { + AssistSheetScreenshot( + conversation = longAnswerConversation, + inputMode = AssistInputMode.VOICE_INACTIVE, + ) + } + + /** + * Renders [AssistSheet] with a [SheetState] already settled at [SheetValue.Expanded]: the + * default state starts hidden or partially expanded, which a static frame would capture as a + * closed or half-open sheet. + */ + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun AssistSheetScreenshot( + conversation: List, + inputMode: AssistInputMode?, + pipelines: List = singleServerPipelines, + currentPipeline: AssistUiPipeline? = singleServerPipelines.first(), + fromFrontend: Boolean = true, + ) { + HAThemeForPreview(modifier = Modifier.fillMaxSize()) { + AssistSheet( + conversation = conversation, + pipelines = pipelines, + inputMode = inputMode, + currentPipeline = currentPipeline, + fromFrontend = fromFrontend, + onSelectPipeline = { _, _ -> }, + onManagePipelines = null, + onChangeInput = {}, + onTextInput = {}, + onMicrophoneInput = {}, + onHide = {}, + bottomSheetState = remember { + SheetState( + skipPartiallyExpanded = true, + // Thresholds only affect drag gestures, which never happen in screenshots. + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + initialValue = SheetValue.Expanded, + ) + }, + ) + } + } +} diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_foldable_c908f502_0.png new file mode 100644 index 00000000000..b5ccdd1ef57 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_e05166be_0.png new file mode 100644 index 00000000000..90f0fa60c4d Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..16383de086c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..b7ddc251ca2 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..f787a8068a1 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..21fdba4d26c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist blocked without pipeline_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_foldable_c908f502_0.png new file mode 100644 index 00000000000..eee6c54b3b6 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_e05166be_0.png new file mode 100644 index 00000000000..70c05be445e Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..b2347d3afd0 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..243a31fed9b Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..eee585cab9c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..2334fad76d5 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist conversation with voice input_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_foldable_c908f502_0.png new file mode 100644 index 00000000000..c32c058928d Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_e05166be_0.png new file mode 100644 index 00000000000..6670d0bb90f Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..d18aea7f3bd Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..5fabb8909da Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..0da30c76ffe Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..a0948184ca0 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist error response_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_foldable_c908f502_0.png new file mode 100644 index 00000000000..7284691e777 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_e05166be_0.png new file mode 100644 index 00000000000..25fbcde4ff8 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..a47d1471c23 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..d83dcc2c20c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..4317d311347 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..3561aade2bd Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist listening_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_foldable_c908f502_0.png new file mode 100644 index 00000000000..d40b995fb5f Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_e05166be_0.png new file mode 100644 index 00000000000..03fb6d2d295 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..96de9f49f6f Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..b83bf7d2c90 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..665c89f6143 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..4ec046e556d Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist long answer filling the screen_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_foldable_c908f502_0.png new file mode 100644 index 00000000000..c2d5aca37cc Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_e05166be_0.png new file mode 100644 index 00000000000..e140f9c9dfb Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..dbff8107a9c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..171ac1dceb3 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..4543435a32c Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..1532496593a Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist opened from outside the app shows the app name_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_foldable_c908f502_0.png new file mode 100644 index 00000000000..ec9d2d7a5dc Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_e05166be_0.png new file mode 100644 index 00000000000..3cb3353e879 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..a205e1b41d4 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..5e9f46d3dd5 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..2620c375661 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..64c087182ab Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist text input with pipelines from multiple servers_tablet_landscape_62cae397_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_foldable_c908f502_0.png new file mode 100644 index 00000000000..0112481cc16 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_foldable_c908f502_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_e05166be_0.png new file mode 100644 index 00000000000..7a2bd168aa4 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..7b801bbd18b Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_phone_landscape_9e00b29d_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_small_phone_66e7bbf2_0.png new file mode 100644 index 00000000000..7b76a7fdd26 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..d18c06943aa Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..a771893cccc Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/assist/ui/AssistSheetScreenshotTest/Assist waiting for a response_tablet_landscape_62cae397_0.png differ