Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple Template tiles on Wear OS #3783

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4f421da
Support multiple Template tiles on Wear OS
slovdahl Aug 10, 2023
d86f38a
Add `TemplateTileConfig` data class
slovdahl Aug 12, 2023
517096e
Fix migration
slovdahl Aug 12, 2023
2681031
`Pair` -> `TemplateTileConfig` fixes
slovdahl Aug 12, 2023
d60b9d1
Fix `getAllTemplateTiles` implementation
slovdahl Aug 12, 2023
81d39a4
Initial work on companion <-> wearable device communication
slovdahl Aug 12, 2023
94d0cb7
More work on phone <-> wear device communication
slovdahl Aug 13, 2023
c2ef04c
Save updated template in phone app
slovdahl Aug 13, 2023
59e2302
Get the template to render using the right method
slovdahl Aug 13, 2023
63fa86a
Fix CI complaints
slovdahl Aug 14, 2023
a15f957
Work on Wear UI for multiple template tiles
slovdahl Aug 14, 2023
768078f
Update wear manifest
slovdahl Aug 17, 2023
75c83a0
Wear migration and navigation fixes
slovdahl Aug 17, 2023
bfde743
Fix Template tile IDs in mobile app
slovdahl Aug 11, 2023
79ef7c3
Make adding a new Template tile on Wear device work
slovdahl Aug 20, 2023
8441574
Small cleanups and TODO fixes
slovdahl Aug 25, 2023
24e26b0
Try to fix template config refresh in settings
slovdahl Aug 25, 2023
65fa664
Fix after rebase
slovdahl Sep 16, 2023
89f484d
Adopt blocking approach for reacting to tile events, inspired by #3974
slovdahl Oct 27, 2023
3a5ad69
Use `OpenTileSettingsActivity` for template tile
slovdahl Oct 27, 2023
dac13bb
Adopt Material 3 and other UI-related changes
slovdahl Oct 27, 2023
57f996f
Show help text in phone app if no template tiles have been added yet
slovdahl Oct 27, 2023
7134fa4
Reference the view model variable inside the function
slovdahl Oct 27, 2023
88e9458
Reload template tiles when opening the template tiles from settings
slovdahl Oct 29, 2023
8fc2726
Replace null key with -1 for old template tile
slovdahl Nov 26, 2023
5c9e7a6
Lint complaints fixes
slovdahl Dec 19, 2023
6379399
remove TODO
slovdahl Dec 20, 2023
32535bb
Store error
slovdahl Jan 18, 2024
f633367
Scrollable list of template tiles
slovdahl Jan 18, 2024
32ef674
Move "Configure template tile" to header
slovdahl Jan 18, 2024
1919bcc
Replace with methods with copy
slovdahl Jan 18, 2024
9add05c
Show template as secondary text
slovdahl Jan 18, 2024
9a4cbb0
Fix scrolling
slovdahl Jan 18, 2024
97717bb
Update app/src/full/java/io/homeassistant/companion/android/settings/…
slovdahl Jan 19, 2024
bdd2c26
Remove unused field
slovdahl Jan 19, 2024
14c5519
Move padding to "no tiles" text
slovdahl Jan 19, 2024
d046771
Add deep link
slovdahl Jan 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.app.Application
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -23,6 +22,7 @@ import com.google.android.gms.wearable.Wearable
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.util.WearDataMessages
import io.homeassistant.companion.android.database.server.Server
Expand Down Expand Up @@ -74,11 +74,9 @@ class SettingsWearViewModel @Inject constructor(
private set
var favoriteEntityIds = mutableStateListOf<String>()
private set
var templateTileContent = mutableStateOf("")
var templateTiles = mutableStateMapOf<Int, TemplateTileConfig>()
private set
var templateTileContentRendered = mutableStateOf("")
private set
var templateTileRefreshInterval = mutableStateOf(0)
var templateTilesRenderedTemplates = mutableStateMapOf<Int, String>()
private set

private val _resultSnackbar = MutableSharedFlow<String>()
Expand Down Expand Up @@ -144,17 +142,42 @@ class SettingsWearViewModel @Inject constructor(
}
}

fun setTemplateContent(template: String) {
templateTileContent.value = template
fun setTemplateTileContent(tileId: Int, updatedTemplateTileContent: String) {
val templateTileConfig = templateTiles[tileId]
templateTileConfig?.let {
templateTiles[tileId] = it.copy(template = updatedTemplateTileContent)
renderTemplate(tileId, updatedTemplateTileContent)
}
}

fun setTemplateTileRefreshInterval(tileId: Int, refreshInterval: Int) {
val templateTileConfig = templateTiles[tileId]
templateTileConfig?.let {
templateTiles[tileId] = it.copy(refreshInterval = refreshInterval)
}
}

private fun setTemplateTiles(newTemplateTiles: Map<Int, TemplateTileConfig>) {
templateTiles.clear()
templateTilesRenderedTemplates.clear()

templateTiles.putAll(newTemplateTiles)
templateTiles.forEach {
renderTemplate(it.key, it.value.template)
}
}

private fun renderTemplate(tileId: Int, template: String) {
if (template.isNotEmpty() && serverId != 0) {
viewModelScope.launch {
try {
templateTileContentRendered.value =
serverManager.integrationRepository(serverId).renderTemplate(template, mapOf()).toString()
templateTilesRenderedTemplates[tileId] = serverManager
.integrationRepository(serverId)
.renderTemplate(template, mapOf()).toString()
} catch (e: Exception) {
Log.e(TAG, "Exception while rendering template", e)
Log.e(TAG, "Exception while rendering template for tile ID $tileId", e)
// JsonMappingException suggests that template is not a String (= error)
templateTileContentRendered.value = getApplication<Application>().getString(
templateTilesRenderedTemplates[tileId] = getApplication<Application>().getString(
if (e.cause is JsonMappingException) {
commonR.string.template_error
} else {
Expand All @@ -164,7 +187,7 @@ class SettingsWearViewModel @Inject constructor(
}
}
} else {
templateTileContentRendered.value = ""
templateTilesRenderedTemplates[tileId] = ""
}
}

Expand Down Expand Up @@ -254,9 +277,8 @@ class SettingsWearViewModel @Inject constructor(
}

fun sendTemplateTileInfo() {
val putDataRequest = PutDataMapRequest.create("/updateTemplateTile").run {
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILE, templateTileContent.value)
dataMap.putInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, templateTileRefreshInterval.value)
val putDataRequest = PutDataMapRequest.create("/updateTemplateTiles").run {
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILES, objectMapper.writeValueAsString(templateTiles))
setUrgent()
asPutDataRequest()
}
Expand Down Expand Up @@ -308,8 +330,14 @@ class SettingsWearViewModel @Inject constructor(
favoriteEntityIdList.forEach { entityId ->
favoriteEntityIds.add(entityId)
}
setTemplateContent(data.getString(WearDataMessages.CONFIG_TEMPLATE_TILE, ""))
templateTileRefreshInterval.value = data.getInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, 0)

val templateTilesFromWear: Map<Int, TemplateTileConfig> = objectMapper.readValue(
data.getString(
WearDataMessages.CONFIG_TEMPLATE_TILES,
"{}"
)
)
setTemplateTiles(templateTilesFromWear)

_hasData.value = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.google.accompanist.themeadapter.material.MdcTheme
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
Expand Down Expand Up @@ -48,30 +50,50 @@ fun LoadSettingsHomeView(
hasData = hasData,
isAuthed = isAuthenticated,
navigateFavorites = { navController.navigate(SettingsWearMainView.FAVORITES) },
navigateTemplateTile = { navController.navigate(SettingsWearMainView.TEMPLATE) },
navigateTemplateTile = { navController.navigate(SettingsWearMainView.TEMPLATES) },
loginWearOs = loginWearOs,
onBackClicked = onStartBackClicked,
events = settingsWearViewModel.resultSnackbar
)
}
composable(SettingsWearMainView.TEMPLATE) {
SettingsWearTemplateTile(
template = settingsWearViewModel.templateTileContent.value,
renderedTemplate = settingsWearViewModel.templateTileContentRendered.value,
refreshInterval = settingsWearViewModel.templateTileRefreshInterval.value,
onContentChanged = {
settingsWearViewModel.setTemplateContent(it)
settingsWearViewModel.sendTemplateTileInfo()
},
onRefreshIntervalChanged = {
settingsWearViewModel.templateTileRefreshInterval.value = it
settingsWearViewModel.sendTemplateTileInfo()
composable(SettingsWearMainView.TEMPLATES) {
SettingsWearTemplateTileList(
templateTiles = settingsWearViewModel.templateTiles,
onTemplateTileClicked = { tileId ->
navController.navigate(SettingsWearMainView.TEMPLATE_TILE.format(tileId))
},
onBackClicked = {
navController.navigateUp()
}
)
}
composable(
route = SettingsWearMainView.TEMPLATE_TILE.format("{tileId}"),
arguments = listOf(navArgument("tileId") { type = NavType.IntType })
) { backStackEntry ->
val tileId = backStackEntry.arguments?.getInt("tileId")
val templateTile = settingsWearViewModel.templateTiles[tileId]
val renderedTemplate = settingsWearViewModel.templateTilesRenderedTemplates[tileId]

templateTile?.let {
SettingsWearTemplateTile(
template = it.template,
renderedTemplate = renderedTemplate ?: "",
refreshInterval = it.refreshInterval,
onContentChanged = { templateContent ->
settingsWearViewModel.setTemplateTileContent(tileId!!, templateContent)
settingsWearViewModel.sendTemplateTileInfo()
},
onRefreshIntervalChanged = { refreshInterval ->
settingsWearViewModel.setTemplateTileRefreshInterval(tileId!!, refreshInterval)
settingsWearViewModel.sendTemplateTileInfo()
},
onBackClicked = {
navController.navigateUp()
}
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ fun SettingWearLandingView(
onClicked = navigateFavorites
)
SettingsRow(
primaryText = stringResource(commonR.string.template_tile),
primaryText = stringResource(commonR.string.template_tiles),
secondaryText = stringResource(commonR.string.template_tile_set_on_watch),
mdiIcon = CommunityMaterial.Icon3.cmd_text_box,
enabled = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class SettingsWearMainView : AppCompatActivity() {
private var registerUrl: String? = null
const val LANDING = "Landing"
const val FAVORITES = "Favorites"
const val TEMPLATE = "Template"
const val TEMPLATES = "Templates"
const val TEMPLATE_TILE = "Template/%s"

fun newInstance(context: Context, wearNodes: Set<Node>, url: String?): Intent {
currentNodes = wearNodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
Expand Down Expand Up @@ -141,3 +142,16 @@ private fun parseHtml(renderedText: String) = buildAnnotatedString {
}
}
}

@Preview
@Composable
private fun PreviewSettingsWearTemplateTile() {
SettingsWearTemplateTile(
template = "Example entity: {{ states('sensor.example_entity') }}",
renderedTemplate = "Example entity: Lorem ipsum",
refreshInterval = 300,
onContentChanged = {},
onRefreshIntervalChanged = {},
onBackClicked = {}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.homeassistant.companion.android.settings.wear.views

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
import io.homeassistant.companion.android.settings.views.SettingsRow
import io.homeassistant.companion.android.common.R as commonR

@Composable
fun SettingsWearTemplateTileList(
templateTiles: Map<Int, TemplateTileConfig>,
onTemplateTileClicked: (tileId: Int) -> Unit,
onBackClicked: () -> Unit
) {
Scaffold(
topBar = {
SettingsWearTopAppBar(
title = { Text(stringResource(commonR.string.template_tiles)) },
onBackClicked = onBackClicked,
docsLink = WEAR_DOCS_LINK
)
}
) { padding ->
Column(
jpelgrom marked this conversation as resolved.
Show resolved Hide resolved
Modifier
.verticalScroll(rememberScrollState())
.padding(padding)
) {
if (templateTiles.entries.isEmpty()) {
Text(
text = stringResource(commonR.string.template_tile_no_tiles_yet),
modifier = Modifier
.padding(all = 16.dp)
)
} else {
Row(
modifier = Modifier
.height(48.dp)
.padding(start = 72.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = commonR.string.template_tile_configure),
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary
)
}

var index = 1
for (templateTileEntry in templateTiles.entries) {
val template = templateTileEntry.value.template
SettingsRow(
primaryText = stringResource(commonR.string.template_tile_n, index++),
secondaryText = when {
template.length <= 100 -> template
else -> "${template.take(100)}…"
},
mdiIcon = CommunityMaterial.Icon3.cmd_text_box,
enabled = true,
onClicked = { onTemplateTileClicked(templateTileEntry.key) }
)
}
}
}
}
}

@Preview
@Composable
private fun PreviewSettingsWearTemplateTileList() {
SettingsWearTemplateTileList(
templateTiles = mapOf(
123 to TemplateTileConfig("Example entity 1: {{ states('sensor.example_entity_1') }}", 300),
51468 to TemplateTileConfig("Example entity 2: {{ states('sensor.example_entity_2') }}", 0)
),
onTemplateTileClicked = {},
onBackClicked = {}
)
}

@Preview
@Composable
private fun PreviewSettingsWearTemplateSingleLegacyTile() {
SettingsWearTemplateTileList(
templateTiles = mapOf(
-1 to TemplateTileConfig("Example entity 1: {{ states('sensor.example_entity_1') }}", 300)
),
onTemplateTileClicked = {},
onBackClicked = {}
)
}

@Preview
@Composable
private fun PreviewSettingsWearTemplateTileListEmpty() {
SettingsWearTemplateTileList(
templateTiles = mapOf(),
onTemplateTileClicked = {},
onBackClicked = {}
)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package io.homeassistant.companion.android.common.data.prefs

import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig

interface WearPrefsRepository {
suspend fun getAllTileShortcuts(): Map<Int?, List<String>>
suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List<String>
suspend fun setTileShortcuts(tileId: Int?, entities: List<String>)
suspend fun removeTileShortcuts(tileId: Int?): List<String>?
suspend fun getShowShortcutText(): Boolean
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
suspend fun getTemplateTileContent(): String
suspend fun setTemplateTileContent(content: String)
suspend fun getTemplateTileRefreshInterval(): Int
suspend fun setTemplateTileRefreshInterval(interval: Int)
suspend fun getAllTemplateTiles(): Map<Int, TemplateTileConfig>
suspend fun getTemplateTileAndSaveTileId(tileId: Int): TemplateTileConfig
suspend fun setAllTemplateTiles(templateTiles: Map<Int, TemplateTileConfig>)
suspend fun setTemplateTile(tileId: Int, content: String, refreshInterval: Int): TemplateTileConfig
suspend fun removeTemplateTile(tileId: Int): TemplateTileConfig?
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearToastConfirmation(): Boolean
Expand Down