diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_assist_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_assist_launcher.xml
new file mode 100644
index 000000000..7652e59e9
--- /dev/null
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_assist_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a01f67e80..9399b9f0f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -397,6 +397,17 @@
+
+
+
+
+
+
()!!
val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported
- var pinnedShortcuts: MutableList = shortcutManager.pinnedShortcuts
+ var pinnedShortcuts = shortcutManager.pinnedShortcuts
+ .filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) }
+ .toMutableList()
private set
var dynamicShortcuts: MutableList = shortcutManager.dynamicShortcuts
private set
@@ -227,6 +230,10 @@ class ManageShortcutsViewModel @Inject constructor(
}
fun updatePinnedShortcuts() {
- pinnedShortcuts = shortcutManager.pinnedShortcuts
+ pinnedShortcuts.clear()
+ pinnedShortcuts.addAll(
+ shortcutManager.pinnedShortcuts
+ .filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) }
+ )
}
}
diff --git a/app/src/main/java/io/homeassistant/companion/android/util/compose/ServerExposedDropdownMenu.kt b/app/src/main/java/io/homeassistant/companion/android/util/compose/ExposedDropdownMenu.kt
similarity index 71%
rename from app/src/main/java/io/homeassistant/companion/android/util/compose/ServerExposedDropdownMenu.kt
rename to app/src/main/java/io/homeassistant/companion/android/util/compose/ExposedDropdownMenu.kt
index 91be18eba..58aa9a92e 100644
--- a/app/src/main/java/io/homeassistant/companion/android/util/compose/ServerExposedDropdownMenu.kt
+++ b/app/src/main/java/io/homeassistant/companion/android/util/compose/ExposedDropdownMenu.kt
@@ -20,7 +20,7 @@ import io.homeassistant.companion.android.common.R as commonR
@OptIn(ExperimentalMaterialApi::class)
@Composable
-fun ServerExposedDropdownMenu(servers: List, current: Int?, onSelected: (Int) -> Unit, modifier: Modifier = Modifier) {
+fun ExposedDropdownMenu(label: String, keys: List, currentIndex: Int?, onSelected: (Int) -> Unit, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
ExposedDropdownMenuBox(
@@ -30,23 +30,37 @@ fun ServerExposedDropdownMenu(servers: List, current: Int?, onSelected:
) {
OutlinedTextField(
readOnly = true,
- value = servers.firstOrNull { it.id == current }?.friendlyName ?: "",
+ value = currentIndex?.let { keys[it] } ?: "",
onValueChange = { },
- label = { Text(stringResource(commonR.string.server_select)) },
+ label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth()
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
- servers.forEach { server ->
+ keys.forEachIndexed { index, key ->
DropdownMenuItem(onClick = {
- onSelected(server.id)
+ onSelected(index)
expanded = false
focusManager.clearFocus()
}) {
- Text(server.friendlyName)
+ Text(key)
}
}
}
}
}
+
+@Composable
+fun ServerExposedDropdownMenu(servers: List, current: Int?, onSelected: (Int) -> Unit, modifier: Modifier = Modifier) {
+ val keys = servers.map { it.friendlyName }
+ val ids = servers.map { it.id }
+ val currentIndex = servers.indexOfFirst { it.id == current }.takeUnless { it == -1 }
+ ExposedDropdownMenu(
+ label = stringResource(commonR.string.server_select),
+ keys = keys,
+ currentIndex = currentIndex,
+ onSelected = { onSelected(ids[it]) },
+ modifier = modifier
+ )
+}
diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutActivity.kt
new file mode 100644
index 000000000..33df1e01c
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutActivity.kt
@@ -0,0 +1,64 @@
+package io.homeassistant.companion.android.widgets.assist
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import com.google.accompanist.themeadapter.material.MdcTheme
+import dagger.hilt.android.AndroidEntryPoint
+import io.homeassistant.companion.android.BaseActivity
+import io.homeassistant.companion.android.R
+import io.homeassistant.companion.android.assist.AssistActivity
+import java.util.UUID
+
+@AndroidEntryPoint
+class AssistShortcutActivity : BaseActivity() {
+
+ companion object {
+ const val SHORTCUT_PREFIX = ".ha_assist_"
+ }
+
+ val viewModel: AssistShortcutViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MdcTheme {
+ AssistShortcutView(
+ selectedServerId = viewModel.serverId,
+ servers = viewModel.servers,
+ supported = viewModel.supported,
+ pipelines = viewModel.pipelines,
+ onSetServer = viewModel::setServer,
+ onSubmit = this::setShortcutAndFinish
+ )
+ }
+ }
+ }
+
+ private fun setShortcutAndFinish(name: String, serverId: Int, pipelineId: String?, startListening: Boolean) {
+ val assistIntent = AssistActivity.newInstance(
+ context = this,
+ serverId = serverId,
+ pipelineId = pipelineId,
+ startListening = startListening,
+ fromFrontend = false
+ ).apply {
+ action = Intent.ACTION_VIEW
+ }
+ val shortcutInfo = ShortcutInfoCompat.Builder(this, "$SHORTCUT_PREFIX${UUID.randomUUID()}")
+ .setIntent(assistIntent)
+ .setShortLabel(name)
+ .setLongLabel(name)
+ .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_assist_launcher))
+ .build()
+ val resultIntent = ShortcutManagerCompat.createShortcutResultIntent(this, shortcutInfo)
+ setResult(Activity.RESULT_OK, resultIntent)
+ finish()
+ }
+}
diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutView.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutView.kt
new file mode 100644
index 000000000..64bd98701
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutView.kt
@@ -0,0 +1,140 @@
+package io.homeassistant.companion.android.widgets.assist
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Switch
+import androidx.compose.material.SwitchDefaults
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineListResponse
+import io.homeassistant.companion.android.database.server.Server
+import io.homeassistant.companion.android.util.compose.ExposedDropdownMenu
+import io.homeassistant.companion.android.util.compose.ServerExposedDropdownMenu
+import io.homeassistant.companion.android.common.R as commonR
+
+@Composable
+fun AssistShortcutView(
+ selectedServerId: Int,
+ servers: List,
+ supported: Boolean?,
+ pipelines: AssistPipelineListResponse?,
+ onSetServer: (Int) -> Unit,
+ onSubmit: (String, Int, String?, Boolean) -> Unit
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(commonR.string.assist_shortcut)) },
+ backgroundColor = colorResource(commonR.color.colorBackground),
+ contentColor = colorResource(commonR.color.colorOnBackground)
+ )
+ }
+ ) { padding ->
+ Box(modifier = Modifier.padding(padding).verticalScroll(rememberScrollState())) {
+ Column(modifier = Modifier.padding(all = 16.dp)) {
+ val assist = stringResource(commonR.string.assist)
+ var name by rememberSaveable { mutableStateOf(assist) }
+ var startListening by rememberSaveable { mutableStateOf(true) }
+ var pipelineId by rememberSaveable(selectedServerId) { mutableStateOf(null) }
+
+ OutlinedTextField(
+ value = name,
+ onValueChange = { name = it },
+ label = { Text(stringResource(commonR.string.widget_text_hint_label)) },
+ singleLine = true,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ )
+ if (servers.size > 1) {
+ ServerExposedDropdownMenu(
+ servers = servers,
+ current = selectedServerId,
+ onSelected = onSetServer,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ if (supported == true) {
+ if (pipelines != null && pipelines.pipelines.isNotEmpty()) {
+ ExposedDropdownMenu(
+ label = stringResource(commonR.string.assist_pipeline),
+ keys = listOf(
+ stringResource(
+ commonR.string.assist_preferred_pipeline,
+ pipelines.pipelines.first { it.id == pipelines.preferredPipeline }.name
+ )
+ ) +
+ pipelines.pipelines.map { it.name },
+ currentIndex = pipelineId?.let { pid -> 1 + pipelines.pipelines.indexOfFirst { it.id == pid } }
+ ?: 0,
+ onSelected = {
+ pipelineId = if (it == 0) null else pipelines.pipelines[it - 1].id
+ }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ Row(
+ modifier = Modifier.clickable { startListening = !startListening }
+ ) {
+ Text(
+ text = stringResource(commonR.string.assist_start_listening),
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp)
+ )
+ Switch(
+ checked = startListening,
+ onCheckedChange = null,
+ colors = SwitchDefaults.colors(uncheckedThumbColor = colorResource(commonR.color.colorSwitchUncheckedThumb))
+ )
+ }
+ } else if (supported == false) {
+ Text(
+ stringResource(
+ commonR.string.no_assist_support,
+ "2023.5",
+ stringResource(commonR.string.no_assist_support_assist_pipeline)
+ )
+ )
+ }
+
+ Button(
+ onClick = {
+ onSubmit(
+ name.ifBlank { assist },
+ selectedServerId,
+ pipelineId,
+ startListening
+ )
+ },
+ enabled = supported == true,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 32.dp)
+ ) {
+ Text(stringResource(commonR.string.add_shortcut))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutViewModel.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutViewModel.kt
new file mode 100644
index 000000000..8039f392f
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/widgets/assist/AssistShortcutViewModel.kt
@@ -0,0 +1,63 @@
+package io.homeassistant.companion.android.widgets.assist
+
+import android.app.Application
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import io.homeassistant.companion.android.common.data.servers.ServerManager
+import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineListResponse
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AssistShortcutViewModel @Inject constructor(
+ val serverManager: ServerManager,
+ application: Application
+) : AndroidViewModel(application) {
+
+ var serverId by mutableStateOf(ServerManager.SERVER_ID_ACTIVE)
+ private set
+
+ var servers by mutableStateOf(serverManager.defaultServers)
+ private set
+
+ var supported by mutableStateOf(null)
+ private set
+
+ var pipelines by mutableStateOf(null)
+ private set
+
+ init {
+ if (serverManager.isRegistered()) {
+ serverManager.getServer()?.id?.let { serverId = it }
+ getData()
+ } else {
+ supported = false
+ }
+ }
+
+ fun setServer(serverId: Int) {
+ if (serverId == this.serverId) return
+
+ this.serverId = serverId
+ getData()
+ }
+
+ private fun getData() {
+ viewModelScope.launch {
+ // Loading states
+ supported = null
+ pipelines = null
+
+ // Update data
+ supported = serverManager.getServer(serverId)?.version?.isAtLeast(2023, 5) == true &&
+ serverManager.webSocketRepository(serverId).getConfig()?.components?.contains("assist_pipeline") == true
+ if (supported == true) {
+ pipelines = serverManager.webSocketRepository(serverId).getAssistPipelines()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_assist_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_assist_launcher.xml
new file mode 100644
index 000000000..07ecae105
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_assist_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/drawable/ic_assist_launcher_foreground.xml b/common/src/main/res/drawable/ic_assist_launcher_foreground.xml
new file mode 100644
index 000000000..118dfe875
--- /dev/null
+++ b/common/src/main/res/drawable/ic_assist_launcher_foreground.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index 679ba7e9d..cb8c81aad 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -1079,7 +1079,10 @@
How can I assist?
Log in to Home Assistant to start using Assist
To use Assist with your voice, allow Home Assistant to access the microphone
+ Assistant
+ Preferred assistant (%1$s)
Send text
+ Assist shortcut
Start listening
Stop listening
Please open the Home Assistant app and log in to start using Assist
diff --git a/wear/src/main/res/drawable/ic_assist_launcher_foreground.xml b/wear/src/main/res/drawable/ic_assist_launcher_foreground.xml
deleted file mode 100755
index 78cfe8e88..000000000
--- a/wear/src/main/res/drawable/ic_assist_launcher_foreground.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file