Add shortcut for Assist (#3615)

- Adds a shortcut to the app that allows accessing Assist from the home screen, including options for pipeline and voice/text
This commit is contained in:
Joris Pelgröm 2023-06-30 04:19:13 +02:00 committed by GitHub
parent 1ed0f6a094
commit 48a7755e62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 335 additions and 21 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/holo_red_dark" />
<foreground android:drawable="@drawable/ic_assist_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_assist_launcher_foreground" />
</adaptive-icon>

View File

@ -397,6 +397,17 @@
</intent-filter>
</activity>
<activity android:name=".widgets.assist.AssistShortcutActivity"
android:label="@string/assist_shortcut"
android:icon="@mipmap/ic_assist_launcher"
android:exported="true"
android:excludeFromRecents="true"
android:theme="@style/Theme.HomeAssistant.Config">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@ -31,6 +31,7 @@ import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.webview.WebViewActivity
import io.homeassistant.companion.android.widgets.assist.AssistShortcutActivity
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -46,7 +47,9 @@ class ManageShortcutsViewModel @Inject constructor(
private lateinit var iconPack: IconPack
private var shortcutManager = application.applicationContext.getSystemService<ShortcutManager>()!!
val canPinShortcuts = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && shortcutManager.isRequestPinShortcutSupported
var pinnedShortcuts: MutableList<ShortcutInfo> = shortcutManager.pinnedShortcuts
var pinnedShortcuts = shortcutManager.pinnedShortcuts
.filter { !it.id.startsWith(AssistShortcutActivity.SHORTCUT_PREFIX) }
.toMutableList()
private set
var dynamicShortcuts: MutableList<ShortcutInfo> = 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) }
)
}
}

View File

@ -20,7 +20,7 @@ import io.homeassistant.companion.android.common.R as commonR
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ServerExposedDropdownMenu(servers: List<Server>, current: Int?, onSelected: (Int) -> Unit, modifier: Modifier = Modifier) {
fun ExposedDropdownMenu(label: String, keys: List<String>, 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<Server>, 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<Server>, 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
)
}

View File

@ -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()
}
}

View File

@ -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<Server>,
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<String?>(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))
}
}
}
}
}

View File

@ -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<Boolean?>(null)
private set
var pipelines by mutableStateOf<AssistPipelineListResponse?>(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()
}
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_assist_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_assist_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,13 @@
<!-- drawable/comment_processing_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:scaleX="0.45466667"
android:scaleY="0.45466667"
android:translateX="6.544"
android:translateY="6.544">
<path android:fillColor="#fff" android:pathData="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M17,11H15V9H17V11M13,11H11V9H13V11M9,11H7V9H9V11Z" />
</group>
</vector>

View File

@ -1079,7 +1079,10 @@
<string name="assist_how_can_i_assist">How can I assist?</string>
<string name="assist_log_in">Log in to Home Assistant to start using Assist</string>
<string name="assist_permission">To use Assist with your voice, allow Home Assistant to access the microphone</string>
<string name="assist_pipeline">Assistant</string>
<string name="assist_preferred_pipeline">Preferred assistant (%1$s)</string>
<string name="assist_send_text">Send text</string>
<string name="assist_shortcut">Assist shortcut</string>
<string name="assist_start_listening">Start listening</string>
<string name="assist_stop_listening">Stop listening</string>
<string name="not_registered">Please open the Home Assistant app and log in to start using Assist</string>

View File

@ -1,13 +0,0 @@
<!-- drawable/comment_processing_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:scaleX="0.45466667"
android:scaleY="0.45466667"
android:translateX="6.544"
android:translateY="6.544">
<path android:fillColor="#fff" android:pathData="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M17,11H15V9H17V11M13,11H11V9H13V11M9,11H7V9H9V11Z" />
</group>
</vector>