mirror of
https://github.com/home-assistant/android
synced 2024-07-09 04:15:51 +00:00
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:
parent
1ed0f6a094
commit
48a7755e62
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue
Block a user