mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Support multiple Template tiles on Wear OS (#3783)
* Support multiple Template tiles on Wear OS * Add `TemplateTileConfig` data class * Fix migration * `Pair` -> `TemplateTileConfig` fixes * Fix `getAllTemplateTiles` implementation * Initial work on companion <-> wearable device communication * More work on phone <-> wear device communication * Save updated template in phone app * Get the template to render using the right method * Fix CI complaints * Work on Wear UI for multiple template tiles * Update wear manifest * Wear migration and navigation fixes * Fix Template tile IDs in mobile app * Make adding a new Template tile on Wear device work * Small cleanups and TODO fixes * Try to fix template config refresh in settings * Fix after rebase * Adopt blocking approach for reacting to tile events, inspired by #3974 * Use `OpenTileSettingsActivity` for template tile * Adopt Material 3 and other UI-related changes * Show help text in phone app if no template tiles have been added yet * Reference the view model variable inside the function By having the view model variable outside the block, the updated template tile might not be propagated to the template settings view. * Reload template tiles when opening the template tiles from settings * Replace null key with -1 for old template tile * Lint complaints fixes * remove TODO * Store error * Scrollable list of template tiles * Move "Configure template tile" to header * Replace with methods with copy * Show template as secondary text * Fix scrolling * Update app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTileList.kt Co-authored-by: Joris Pelgröm <jpelgrom@users.noreply.github.com> * Remove unused field * Move padding to "no tiles" text * Add deep link --------- Co-authored-by: Joris Pelgröm <jpelgrom@users.noreply.github.com>
This commit is contained in:
parent
26a472afc6
commit
5d52735e36
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>()
|
||||
|
@ -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 {
|
||||
|
@ -164,7 +187,7 @@ class SettingsWearViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
templateTileContentRendered.value = ""
|
||||
templateTilesRenderedTemplates[tileId] = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -48,23 +50,42 @@ 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) {
|
||||
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 = settingsWearViewModel.templateTileContent.value,
|
||||
renderedTemplate = settingsWearViewModel.templateTileContentRendered.value,
|
||||
refreshInterval = settingsWearViewModel.templateTileRefreshInterval.value,
|
||||
onContentChanged = {
|
||||
settingsWearViewModel.setTemplateContent(it)
|
||||
template = it.template,
|
||||
renderedTemplate = renderedTemplate ?: "",
|
||||
refreshInterval = it.refreshInterval,
|
||||
onContentChanged = { templateContent ->
|
||||
settingsWearViewModel.setTemplateTileContent(tileId!!, templateContent)
|
||||
settingsWearViewModel.sendTemplateTileInfo()
|
||||
},
|
||||
onRefreshIntervalChanged = {
|
||||
settingsWearViewModel.templateTileRefreshInterval.value = it
|
||||
onRefreshIntervalChanged = { refreshInterval ->
|
||||
settingsWearViewModel.setTemplateTileRefreshInterval(tileId!!, refreshInterval)
|
||||
settingsWearViewModel.sendTemplateTileInfo()
|
||||
},
|
||||
onBackClicked = {
|
||||
|
@ -74,6 +95,7 @@ fun LoadSettingsHomeView(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
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 = {}
|
||||
)
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
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>
|
||||
|
@ -7,10 +9,11 @@ interface WearPrefsRepository {
|
|||
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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.homeassistant.companion.android.common.data.prefs
|
||||
|
||||
import io.homeassistant.companion.android.common.data.LocalStorage
|
||||
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
|
||||
import io.homeassistant.companion.android.common.util.toStringList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONArray
|
||||
|
@ -15,19 +16,23 @@ class WearPrefsRepositoryImpl @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private const val MIGRATION_PREF = "migration"
|
||||
private const val MIGRATION_VERSION = 1
|
||||
private const val MIGRATION_VERSION = 2
|
||||
|
||||
private const val PREF_TILE_SHORTCUTS = "tile_shortcuts_list"
|
||||
private const val PREF_SHOW_TILE_SHORTCUTS_TEXT = "show_tile_shortcuts_text"
|
||||
private const val PREF_TILE_TEMPLATE = "tile_template"
|
||||
private const val PREF_TILE_TEMPLATE_REFRESH_INTERVAL = "tile_template_refresh_interval"
|
||||
private const val PREF_TILE_TEMPLATES = "tile_templates"
|
||||
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
|
||||
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
|
||||
private const val PREF_WEAR_FAVORITES_ONLY = "wear_favorites_only"
|
||||
|
||||
private const val UNKNOWN_TEMPLATE_TILE_ID = -1
|
||||
}
|
||||
|
||||
init {
|
||||
runBlocking {
|
||||
val legacyPrefTileTemplate = "tile_template"
|
||||
val legacyPrefTileTemplateRefreshInterval = "tile_template_refresh_interval"
|
||||
|
||||
val currentVersion = localStorage.getInt(MIGRATION_PREF)
|
||||
if (currentVersion == null || currentVersion < 1) {
|
||||
integrationStorage.getString(PREF_TILE_SHORTCUTS)?.let {
|
||||
|
@ -36,11 +41,11 @@ class WearPrefsRepositoryImpl @Inject constructor(
|
|||
integrationStorage.getBooleanOrNull(PREF_SHOW_TILE_SHORTCUTS_TEXT)?.let {
|
||||
localStorage.putBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT, it)
|
||||
}
|
||||
integrationStorage.getString(PREF_TILE_TEMPLATE)?.let {
|
||||
localStorage.putString(PREF_TILE_TEMPLATE, it)
|
||||
integrationStorage.getString(legacyPrefTileTemplate)?.let {
|
||||
localStorage.putString(legacyPrefTileTemplate, it)
|
||||
}
|
||||
integrationStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL)?.let {
|
||||
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, it)
|
||||
integrationStorage.getInt(legacyPrefTileTemplateRefreshInterval)?.let {
|
||||
localStorage.putInt(legacyPrefTileTemplateRefreshInterval, it)
|
||||
}
|
||||
integrationStorage.getBooleanOrNull(PREF_WEAR_HAPTIC_FEEDBACK)?.let {
|
||||
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, it)
|
||||
|
@ -51,6 +56,26 @@ class WearPrefsRepositoryImpl @Inject constructor(
|
|||
|
||||
localStorage.putInt(MIGRATION_PREF, MIGRATION_VERSION)
|
||||
}
|
||||
|
||||
if (currentVersion == null || currentVersion < 2) {
|
||||
val template = localStorage.getString(legacyPrefTileTemplate)
|
||||
val templateRefreshInterval = localStorage.getInt(
|
||||
legacyPrefTileTemplateRefreshInterval
|
||||
)
|
||||
|
||||
if (template != null && templateRefreshInterval != null) {
|
||||
val templates = mapOf(
|
||||
UNKNOWN_TEMPLATE_TILE_ID.toString() to TemplateTileConfig(template, templateRefreshInterval).toJSONObject()
|
||||
)
|
||||
|
||||
localStorage.putString(PREF_TILE_TEMPLATES, JSONObject(templates).toString())
|
||||
}
|
||||
|
||||
localStorage.remove(legacyPrefTileTemplate)
|
||||
localStorage.remove(legacyPrefTileTemplateRefreshInterval)
|
||||
|
||||
localStorage.putInt(MIGRATION_PREF, MIGRATION_VERSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,10 +143,6 @@ class WearPrefsRepositoryImpl @Inject constructor(
|
|||
return entities
|
||||
}
|
||||
|
||||
override suspend fun getTemplateTileContent(): String {
|
||||
return localStorage.getString(PREF_TILE_TEMPLATE) ?: ""
|
||||
}
|
||||
|
||||
override suspend fun getShowShortcutText(): Boolean {
|
||||
return localStorage.getBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT)
|
||||
}
|
||||
|
@ -130,16 +151,56 @@ class WearPrefsRepositoryImpl @Inject constructor(
|
|||
localStorage.putBoolean(PREF_SHOW_TILE_SHORTCUTS_TEXT, enabled)
|
||||
}
|
||||
|
||||
override suspend fun setTemplateTileContent(content: String) {
|
||||
localStorage.putString(PREF_TILE_TEMPLATE, content)
|
||||
override suspend fun getAllTemplateTiles(): Map<Int, TemplateTileConfig> {
|
||||
return localStorage.getString(PREF_TILE_TEMPLATES)?.let { jsonStr ->
|
||||
val jsonObject = JSONObject(jsonStr)
|
||||
buildMap {
|
||||
jsonObject.keys().forEach { tileId ->
|
||||
val id = tileId.toInt()
|
||||
val templateTileConfig = TemplateTileConfig(jsonObject.getJSONObject(tileId))
|
||||
put(id, templateTileConfig)
|
||||
}
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
override suspend fun getTemplateTileRefreshInterval(): Int {
|
||||
return localStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL) ?: 0
|
||||
override suspend fun getTemplateTileAndSaveTileId(tileId: Int): TemplateTileConfig {
|
||||
val tileIdToTemplatesMap = getAllTemplateTiles()
|
||||
return if (UNKNOWN_TEMPLATE_TILE_ID in tileIdToTemplatesMap && tileId !in tileIdToTemplatesMap) {
|
||||
// if there are Templates with an unknown (-1) tileId key from a previous installation,
|
||||
// and the tileId parameter is not already present in the map, associate it with that Template
|
||||
val templateData = removeTemplateTile(UNKNOWN_TEMPLATE_TILE_ID)!!
|
||||
setTemplateTile(tileId, templateData.template, templateData.refreshInterval)
|
||||
templateData
|
||||
} else {
|
||||
var templateData = tileIdToTemplatesMap[tileId]
|
||||
if (templateData == null) {
|
||||
templateData = setTemplateTile(tileId, "", 0)
|
||||
}
|
||||
templateData
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTemplateTileRefreshInterval(interval: Int) {
|
||||
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, interval)
|
||||
override suspend fun setAllTemplateTiles(templateTiles: Map<Int, TemplateTileConfig>) {
|
||||
val templateTilesJson = templateTiles.map { (tileId, templateTileConfig) ->
|
||||
tileId.toString() to templateTileConfig.toJSONObject()
|
||||
}.toMap()
|
||||
val jsonStr = JSONObject(templateTilesJson).toString()
|
||||
localStorage.putString(PREF_TILE_TEMPLATES, jsonStr)
|
||||
}
|
||||
|
||||
override suspend fun setTemplateTile(tileId: Int, content: String, refreshInterval: Int): TemplateTileConfig {
|
||||
val templateTileConfig = TemplateTileConfig(content, refreshInterval)
|
||||
val map = getAllTemplateTiles() + mapOf(tileId to templateTileConfig)
|
||||
setAllTemplateTiles(map)
|
||||
return templateTileConfig
|
||||
}
|
||||
|
||||
override suspend fun removeTemplateTile(tileId: Int): TemplateTileConfig? {
|
||||
val templateTilesMap = getAllTemplateTiles().toMutableMap()
|
||||
val templateTile = templateTilesMap.remove(tileId)
|
||||
setAllTemplateTiles(templateTilesMap)
|
||||
return templateTile
|
||||
}
|
||||
|
||||
override suspend fun getWearHapticFeedback(): Boolean {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package io.homeassistant.companion.android.common.data.prefs.impl.entities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.json.JSONObject
|
||||
|
||||
const val FIELD_TEMPLATE = "template"
|
||||
const val FIELD_REFRESH_INTERVAL = "refresh_interval"
|
||||
|
||||
data class TemplateTileConfig(
|
||||
@JsonProperty(value = FIELD_TEMPLATE)
|
||||
val template: String,
|
||||
@JsonProperty(value = FIELD_REFRESH_INTERVAL)
|
||||
val refreshInterval: Int
|
||||
) {
|
||||
constructor(jsonObject: JSONObject) : this(
|
||||
jsonObject.getString(FIELD_TEMPLATE),
|
||||
jsonObject.getInt(FIELD_REFRESH_INTERVAL)
|
||||
)
|
||||
|
||||
fun toJSONObject(): JSONObject {
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
FIELD_TEMPLATE to template,
|
||||
FIELD_REFRESH_INTERVAL to refreshInterval
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,8 +17,7 @@ object WearDataMessages {
|
|||
const val CONFIG_SERVER_REFRESH_TOKEN = "serverRefreshToken"
|
||||
const val CONFIG_SUPPORTED_DOMAINS = "supportedDomains"
|
||||
const val CONFIG_FAVORITES = "favorites"
|
||||
const val CONFIG_TEMPLATE_TILE = "templateTile"
|
||||
const val CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
|
||||
const val CONFIG_TEMPLATE_TILES = "templateTiles"
|
||||
|
||||
const val LOGIN_RESULT_EXCEPTION = "exception"
|
||||
}
|
||||
|
|
|
@ -846,7 +846,12 @@
|
|||
<string name="tag_reader_title">Processing tag</string>
|
||||
<string name="template">Template</string>
|
||||
<string name="template_tile">Template tile</string>
|
||||
<string name="template_tile_set_on_watch">Set template to display on Wear OS tile</string>
|
||||
<string name="template_tile_n">Template tile #%d</string>
|
||||
<string name="template_tiles">Template tiles</string>
|
||||
<string name="template_tile_configure">Configure Template tiles</string>
|
||||
<string name="template_tile_select">Select Template tile to manage</string>
|
||||
<string name="template_tile_no_tiles_yet">There are no Template tiles added yet - add one from the watch face to set it up</string>
|
||||
<string name="template_tile_set_on_watch">Configure templates to use as Wear OS tiles</string>
|
||||
<string name="template_tile_change_message">Change template in phone settings</string>
|
||||
<string name="template_tile_content">Template tile content</string>
|
||||
<string name="template_tile_desc">Renders and displays a template</string>
|
||||
|
|
|
@ -97,6 +97,11 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.google.android.clockwork.tiles.category.PROVIDER_CONFIG" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="ConfigTemplateTile" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.google.android.clockwork.tiles.category.PROVIDER_CONFIG" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- To show confirmations and failures -->
|
||||
|
@ -160,6 +165,14 @@
|
|||
|
||||
<meta-data android:name="androidx.wear.tiles.PREVIEW"
|
||||
android:resource="@drawable/template_tile_example" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.clockwork.tiles.MULTI_INSTANCES_SUPPORTED"
|
||||
android:value="true" /> <!-- This is supported starting from Wear OS 3 -->
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.clockwork.tiles.PROVIDER_CONFIG_ACTION"
|
||||
android:value="ConfigTemplateTile" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".tiles.ConversationTile"
|
||||
|
@ -252,7 +265,7 @@
|
|||
<data android:scheme="wear" android:host="*"
|
||||
android:path="/updateFavorites" />
|
||||
<data android:scheme="wear" android:host="*"
|
||||
android:path="/updateTemplateTile" />
|
||||
android:path="/updateTemplateTiles" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.home.views.DEEPLINK_PREFIX_SET_CAMERA_TILE
|
||||
import io.homeassistant.companion.android.home.views.DEEPLINK_PREFIX_SET_SHORTCUT_TILE
|
||||
import io.homeassistant.companion.android.home.views.DEEPLINK_PREFIX_SET_TEMPLATE_TILE
|
||||
import io.homeassistant.companion.android.home.views.LoadHomePage
|
||||
import io.homeassistant.companion.android.onboarding.OnboardingActivity
|
||||
import io.homeassistant.companion.android.sensors.SensorReceiver
|
||||
|
@ -57,6 +58,16 @@ class HomeActivity : ComponentActivity(), HomeView {
|
|||
context,
|
||||
HomeActivity::class.java
|
||||
)
|
||||
|
||||
fun getTemplateTileSettingsIntent(
|
||||
context: Context,
|
||||
tileId: Int
|
||||
) = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"$DEEPLINK_PREFIX_SET_TEMPLATE_TILE/$tileId".toUri(),
|
||||
context,
|
||||
HomeActivity::class.java
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.homeassistant.companion.android.home
|
||||
|
||||
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.websocket.WebSocketState
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryUpdatedEvent
|
||||
|
@ -48,9 +49,9 @@ interface HomePresenter {
|
|||
suspend fun setWearToastConfirmation(enabled: Boolean)
|
||||
suspend fun getShowShortcutText(): Boolean
|
||||
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
|
||||
suspend fun getTemplateTileContent(): String
|
||||
suspend fun getTemplateTileRefreshInterval(): Int
|
||||
suspend fun setTemplateTileRefreshInterval(interval: Int)
|
||||
suspend fun getAllTemplateTiles(): Map<Int, TemplateTileConfig>
|
||||
suspend fun setTemplateTileRefreshInterval(tileId: Int, interval: Int)
|
||||
|
||||
suspend fun getWearFavoritesOnly(): Boolean
|
||||
suspend fun setWearFavoritesOnly(enabled: Boolean)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.homeassistant.companion.android.common.data.integration.DeviceRegistra
|
|||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.EntityExt
|
||||
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
|
||||
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.data.websocket.WebSocketState
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
|
||||
|
@ -270,16 +271,14 @@ class HomePresenterImpl @Inject constructor(
|
|||
wearPrefsRepository.setShowShortcutTextEnabled(enabled)
|
||||
}
|
||||
|
||||
override suspend fun getTemplateTileContent(): String {
|
||||
return wearPrefsRepository.getTemplateTileContent()
|
||||
override suspend fun getAllTemplateTiles(): Map<Int, TemplateTileConfig> {
|
||||
return wearPrefsRepository.getAllTemplateTiles()
|
||||
}
|
||||
|
||||
override suspend fun getTemplateTileRefreshInterval(): Int {
|
||||
return wearPrefsRepository.getTemplateTileRefreshInterval()
|
||||
override suspend fun setTemplateTileRefreshInterval(tileId: Int, interval: Int) {
|
||||
getAllTemplateTiles()[tileId]?.let {
|
||||
wearPrefsRepository.setTemplateTile(tileId, it.template, interval)
|
||||
}
|
||||
|
||||
override suspend fun setTemplateTileRefreshInterval(interval: Int) {
|
||||
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
|
||||
}
|
||||
|
||||
override suspend fun getWearFavoritesOnly(): Boolean {
|
||||
|
|
|
@ -19,6 +19,7 @@ import io.homeassistant.companion.android.BuildConfig
|
|||
import io.homeassistant.companion.android.HomeAssistantApplication
|
||||
import io.homeassistant.companion.android.common.data.integration.Entity
|
||||
import io.homeassistant.companion.android.common.data.integration.domain
|
||||
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
|
||||
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.DeviceRegistryResponse
|
||||
|
@ -121,9 +122,7 @@ class MainViewModel @Inject constructor(
|
|||
private set
|
||||
var isShowShortcutTextEnabled = mutableStateOf(false)
|
||||
private set
|
||||
var templateTileContent = mutableStateOf("")
|
||||
private set
|
||||
var templateTileRefreshInterval = mutableStateOf(0)
|
||||
var templateTiles = mutableStateMapOf<Int, TemplateTileConfig>()
|
||||
private set
|
||||
var isFavoritesOnly by mutableStateOf(false)
|
||||
private set
|
||||
|
@ -148,8 +147,8 @@ class MainViewModel @Inject constructor(
|
|||
isHapticEnabled.value = homePresenter.getWearHapticFeedback()
|
||||
isToastEnabled.value = homePresenter.getWearToastConfirmation()
|
||||
isShowShortcutTextEnabled.value = homePresenter.getShowShortcutText()
|
||||
templateTileContent.value = homePresenter.getTemplateTileContent()
|
||||
templateTileRefreshInterval.value = homePresenter.getTemplateTileRefreshInterval()
|
||||
templateTiles.clear()
|
||||
templateTiles.putAll(homePresenter.getAllTemplateTiles())
|
||||
isFavoritesOnly = homePresenter.getWearFavoritesOnly()
|
||||
|
||||
val assistantAppComponent = ComponentName(
|
||||
|
@ -171,6 +170,13 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun loadTemplateTiles() {
|
||||
viewModelScope.launch {
|
||||
templateTiles.clear()
|
||||
templateTiles.putAll(homePresenter.getAllTemplateTiles())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadEntities() {
|
||||
if (!homePresenter.isConnected()) return
|
||||
viewModelScope.launch {
|
||||
|
@ -485,10 +491,12 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setTemplateTileRefreshInterval(interval: Int) {
|
||||
fun setTemplateTileRefreshInterval(tileId: Int, interval: Int) {
|
||||
viewModelScope.launch {
|
||||
homePresenter.setTemplateTileRefreshInterval(interval)
|
||||
templateTileRefreshInterval.value = interval
|
||||
homePresenter.setTemplateTileRefreshInterval(tileId, interval)
|
||||
templateTiles[tileId]?.let {
|
||||
templateTiles[tileId] = it.copy(refreshInterval = interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ private const val ARG_SCREEN_SENSOR_MANAGER_ID = "sensorManagerId"
|
|||
private const val ARG_SCREEN_CAMERA_TILE_ID = "cameraTileId"
|
||||
private const val ARG_SCREEN_SHORTCUTS_TILE_ID = "shortcutsTileId"
|
||||
private const val ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX = "shortcutsTileEntityIndex"
|
||||
private const val ARG_SCREEN_TEMPLATE_TILE_ID = "templateTileId"
|
||||
|
||||
private const val SCREEN_LANDING = "landing"
|
||||
private const val SCREEN_ENTITY_DETAIL = "entity_detail"
|
||||
|
@ -38,7 +39,9 @@ private const val SCREEN_SET_CAMERA_TILE = "set_camera_tile"
|
|||
private const val SCREEN_SET_CAMERA_TILE_ENTITY = "entity"
|
||||
private const val SCREEN_SET_CAMERA_TILE_REFRESH_INTERVAL = "refresh_interval"
|
||||
private const val ROUTE_SHORTCUTS_TILE = "shortcuts_tile"
|
||||
private const val ROUTE_TEMPLATE_TILE = "template_tile"
|
||||
private const val SCREEN_SELECT_SHORTCUTS_TILE = "select_shortcuts_tile"
|
||||
private const val SCREEN_SELECT_TEMPLATE_TILE = "select_template_tile"
|
||||
private const val SCREEN_SET_SHORTCUTS_TILE = "set_shortcuts_tile"
|
||||
private const val SCREEN_SHORTCUTS_TILE_CHOOSE_ENTITY = "shortcuts_tile_choose_entity"
|
||||
private const val SCREEN_SET_TILE_TEMPLATE = "set_tile_template"
|
||||
|
@ -47,6 +50,7 @@ private const val SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL = "set_tile_template
|
|||
const val DEEPLINK_SENSOR_MANAGER = "ha_wear://$SCREEN_SINGLE_SENSOR_MANAGER"
|
||||
const val DEEPLINK_PREFIX_SET_CAMERA_TILE = "ha_wear://$SCREEN_SET_CAMERA_TILE"
|
||||
const val DEEPLINK_PREFIX_SET_SHORTCUT_TILE = "ha_wear://$SCREEN_SET_SHORTCUTS_TILE"
|
||||
const val DEEPLINK_PREFIX_SET_TEMPLATE_TILE = "ha_wear://$SCREEN_SET_TILE_TEMPLATE"
|
||||
|
||||
@Composable
|
||||
fun LoadHomePage(
|
||||
|
@ -161,7 +165,10 @@ fun LoadHomePage(
|
|||
onClickCameraTile = {
|
||||
swipeDismissableNavController.navigate("$ROUTE_CAMERA_TILE/$SCREEN_SELECT_CAMERA_TILE")
|
||||
},
|
||||
onClickTemplateTile = { swipeDismissableNavController.navigate(SCREEN_SET_TILE_TEMPLATE) },
|
||||
onClickTemplateTiles = {
|
||||
mainViewModel.loadTemplateTiles()
|
||||
swipeDismissableNavController.navigate("$ROUTE_TEMPLATE_TILE/$SCREEN_SELECT_TEMPLATE_TILE")
|
||||
},
|
||||
onAssistantAppAllowed = mainViewModel::setAssistantApp
|
||||
)
|
||||
}
|
||||
|
@ -317,21 +324,49 @@ fun LoadHomePage(
|
|||
}
|
||||
)
|
||||
}
|
||||
composable(SCREEN_SET_TILE_TEMPLATE) {
|
||||
composable("$ROUTE_TEMPLATE_TILE/$SCREEN_SELECT_TEMPLATE_TILE") {
|
||||
SelectTemplateTileView(
|
||||
templateTiles = mainViewModel.templateTiles,
|
||||
onSelectTemplateTile = { tileId ->
|
||||
swipeDismissableNavController.navigate("$ROUTE_TEMPLATE_TILE/$tileId/$SCREEN_SET_TILE_TEMPLATE")
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = "$ROUTE_TEMPLATE_TILE/{$ARG_SCREEN_TEMPLATE_TILE_ID}/$SCREEN_SET_TILE_TEMPLATE",
|
||||
arguments = listOf(
|
||||
navArgument(name = ARG_SCREEN_TEMPLATE_TILE_ID) {
|
||||
type = NavType.StringType
|
||||
}
|
||||
),
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = "$DEEPLINK_PREFIX_SET_TEMPLATE_TILE/{$ARG_SCREEN_TEMPLATE_TILE_ID}" }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val tileId = backStackEntry.arguments!!.getString(ARG_SCREEN_TEMPLATE_TILE_ID)!!.toIntOrNull()
|
||||
|
||||
TemplateTileSettingsView(
|
||||
templateContent = mainViewModel.templateTileContent.value,
|
||||
refreshInterval = mainViewModel.templateTileRefreshInterval.value
|
||||
templateContent = mainViewModel.templateTiles[tileId]?.template ?: "",
|
||||
refreshInterval = mainViewModel.templateTiles[tileId]?.refreshInterval ?: 0
|
||||
) {
|
||||
swipeDismissableNavController.navigate(
|
||||
SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL
|
||||
"$ROUTE_TEMPLATE_TILE/$tileId/$SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL"
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL) {
|
||||
composable(
|
||||
route = "$ROUTE_TEMPLATE_TILE/{$ARG_SCREEN_TEMPLATE_TILE_ID}/$SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL",
|
||||
arguments = listOf(
|
||||
navArgument(name = ARG_SCREEN_TEMPLATE_TILE_ID) {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val tileId = backStackEntry.arguments!!.getString(ARG_SCREEN_TEMPLATE_TILE_ID)!!.toInt()
|
||||
RefreshIntervalPickerView(
|
||||
currentInterval = mainViewModel.templateTileRefreshInterval.value
|
||||
currentInterval = mainViewModel.templateTiles[tileId]?.refreshInterval ?: 0
|
||||
) {
|
||||
mainViewModel.setTemplateTileRefreshInterval(it)
|
||||
mainViewModel.setTemplateTileRefreshInterval(tileId, it)
|
||||
TileService.getUpdater(context).requestUpdate(TemplateTile::class.java)
|
||||
swipeDismissableNavController.navigateUp()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package io.homeassistant.companion.android.home.views
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.wear.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.wear.compose.material3.Button
|
||||
import androidx.wear.compose.material3.Text
|
||||
import androidx.wear.tooling.preview.devices.WearDevices
|
||||
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
|
||||
import io.homeassistant.companion.android.theme.WearAppTheme
|
||||
import io.homeassistant.companion.android.theme.getFilledTonalButtonColors
|
||||
import io.homeassistant.companion.android.views.ListHeader
|
||||
import io.homeassistant.companion.android.views.ThemeLazyColumn
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
@Composable
|
||||
fun SelectTemplateTileView(
|
||||
templateTiles: Map<Int, TemplateTileConfig>,
|
||||
onSelectTemplateTile: (tileId: Int) -> Unit
|
||||
) {
|
||||
WearAppTheme {
|
||||
ThemeLazyColumn {
|
||||
item {
|
||||
ListHeader(id = commonR.string.template_tiles)
|
||||
}
|
||||
if (templateTiles.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(commonR.string.template_tile_no_tiles_yet),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(templateTiles.keys.toList()) { index, templateTileId ->
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
label = {
|
||||
Text(stringResource(commonR.string.template_tile_n, index + 1))
|
||||
},
|
||||
onClick = { onSelectTemplateTile(templateTileId) },
|
||||
colors = getFilledTonalButtonColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(device = WearDevices.LARGE_ROUND)
|
||||
@Composable
|
||||
private fun PreviewSelectTemplateTileView() {
|
||||
SelectTemplateTileView(
|
||||
templateTiles = mapOf(
|
||||
-1 to TemplateTileConfig("Old template", 0),
|
||||
1111 to TemplateTileConfig("New template #1", 10),
|
||||
2222 to TemplateTileConfig("New template #2", 20)
|
||||
),
|
||||
onSelectTemplateTile = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(device = WearDevices.LARGE_ROUND)
|
||||
@Composable
|
||||
private fun PreviewSelectTemplateTileEmptyView() {
|
||||
SelectTemplateTileView(
|
||||
templateTiles = mapOf(),
|
||||
onSelectTemplateTile = {}
|
||||
)
|
||||
}
|
|
@ -72,7 +72,7 @@ fun SettingsView(
|
|||
onToastEnabled: (Boolean) -> Unit,
|
||||
setFavoritesOnly: (Boolean) -> Unit,
|
||||
onClickCameraTile: () -> Unit,
|
||||
onClickTemplateTile: () -> Unit,
|
||||
onClickTemplateTiles: () -> Unit,
|
||||
onAssistantAppAllowed: (Boolean) -> Unit
|
||||
) {
|
||||
WearAppTheme {
|
||||
|
@ -187,8 +187,8 @@ fun SettingsView(
|
|||
item {
|
||||
SecondarySettingsChip(
|
||||
icon = CommunityMaterial.Icon3.cmd_text_box,
|
||||
label = stringResource(commonR.string.template_tile),
|
||||
onClick = onClickTemplateTile
|
||||
label = stringResource(commonR.string.template_tiles),
|
||||
onClick = onClickTemplateTiles
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
@ -272,7 +272,7 @@ private fun PreviewSettingsView() {
|
|||
onToastEnabled = {},
|
||||
setFavoritesOnly = {},
|
||||
onClickCameraTile = {},
|
||||
onClickTemplateTile = {},
|
||||
onClickTemplateTiles = {},
|
||||
onAssistantAppAllowed = {}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import io.homeassistant.companion.android.common.data.integration.DeviceRegistra
|
|||
import io.homeassistant.companion.android.common.data.keychain.KeyChainRepository
|
||||
import io.homeassistant.companion.android.common.data.keychain.KeyStoreRepositoryImpl
|
||||
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
|
||||
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
|
||||
|
@ -103,8 +104,7 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
}
|
||||
dataMap.putString(WearDataMessages.CONFIG_SUPPORTED_DOMAINS, objectMapper.writeValueAsString(HomePresenterImpl.supportedDomains))
|
||||
dataMap.putString(WearDataMessages.CONFIG_FAVORITES, objectMapper.writeValueAsString(currentFavorites))
|
||||
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILE, wearPrefsRepository.getTemplateTileContent())
|
||||
dataMap.putInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, wearPrefsRepository.getTemplateTileRefreshInterval())
|
||||
dataMap.putString(WearDataMessages.CONFIG_TEMPLATE_TILES, objectMapper.writeValueAsString(wearPrefsRepository.getAllTemplateTiles()))
|
||||
setUrgent()
|
||||
asPutDataRequest()
|
||||
}
|
||||
|
@ -129,8 +129,8 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
"/updateFavorites" -> {
|
||||
saveFavorites(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
"/updateTemplateTile" -> {
|
||||
saveTileTemplate(DataMapItem.fromDataItem(item).dataMap)
|
||||
"/updateTemplateTiles" -> {
|
||||
saveTemplateTiles(DataMapItem.fromDataItem(item).dataMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,11 +246,15 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
|||
}
|
||||
}
|
||||
|
||||
private fun saveTileTemplate(dataMap: DataMap) = mainScope.launch {
|
||||
val content = dataMap.getString(WearDataMessages.CONFIG_TEMPLATE_TILE, "")
|
||||
val interval = dataMap.getInt(WearDataMessages.CONFIG_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
|
||||
wearPrefsRepository.setTemplateTileContent(content)
|
||||
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
|
||||
private fun saveTemplateTiles(dataMap: DataMap) = mainScope.launch {
|
||||
val templateTilesFromPhone: Map<Int, TemplateTileConfig> = objectMapper.readValue(
|
||||
dataMap.getString(
|
||||
WearDataMessages.CONFIG_TEMPLATE_TILES,
|
||||
"{}"
|
||||
)
|
||||
)
|
||||
|
||||
wearPrefsRepository.setAllTemplateTiles(templateTilesFromPhone)
|
||||
}
|
||||
|
||||
private fun updateTiles() = mainScope.launch {
|
||||
|
|
|
@ -34,6 +34,15 @@ class OpenTileSettingsActivity : AppCompatActivity() {
|
|||
tileId = it
|
||||
)
|
||||
}
|
||||
"ConfigTemplateTile" -> {
|
||||
lifecycleScope.launch {
|
||||
wearPrefsRepository.getTemplateTileAndSaveTileId(tileId)
|
||||
}
|
||||
HomeActivity.getTemplateTileSettingsIntent(
|
||||
context = this,
|
||||
tileId = it
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
|
|||
import androidx.wear.protolayout.ResourceBuilders
|
||||
import androidx.wear.protolayout.ResourceBuilders.Resources
|
||||
import androidx.wear.protolayout.TimelineBuilders.Timeline
|
||||
import androidx.wear.tiles.EventBuilders
|
||||
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
|
||||
import androidx.wear.tiles.RequestBuilders.TileRequest
|
||||
import androidx.wear.tiles.TileBuilders.Tile
|
||||
|
@ -28,11 +29,14 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.common.data.prefs.WearPrefsRepository
|
||||
import io.homeassistant.companion.android.common.data.prefs.impl.entities.TemplateTileConfig
|
||||
import io.homeassistant.companion.android.common.data.servers.ServerManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.guava.future
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
|
@ -53,14 +57,17 @@ class TemplateTile : TileService() {
|
|||
if (wearPrefsRepository.getWearHapticFeedback()) hapticClick(applicationContext)
|
||||
}
|
||||
|
||||
val tileId = requestParams.tileId
|
||||
val templateTileConfig = getTemplateTileConfig(tileId)
|
||||
|
||||
Tile.Builder()
|
||||
.setResourcesVersion("1")
|
||||
.setFreshnessIntervalMillis(
|
||||
wearPrefsRepository.getTemplateTileRefreshInterval().toLong() * 1000
|
||||
templateTileConfig.refreshInterval.toLong() * 1_000
|
||||
)
|
||||
.setTileTimeline(
|
||||
if (serverManager.isRegistered()) {
|
||||
timeline()
|
||||
timeline(templateTileConfig)
|
||||
} else {
|
||||
loggedOutTimeline(
|
||||
this@TemplateTile,
|
||||
|
@ -88,17 +95,43 @@ class TemplateTile : TileService() {
|
|||
.build()
|
||||
}
|
||||
|
||||
override fun onTileAddEvent(requestParams: EventBuilders.TileAddEvent): Unit = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
/**
|
||||
* When the app is updated from an older version (which only supported a single Template Tile),
|
||||
* and the user is adding a new Template Tile, we can't tell for sure if it's the 1st or 2nd Tile.
|
||||
* Even though we may have the Template tile config stored in the prefs, it doesn't guarantee that
|
||||
* the tile was actually added to the Tiles carousel.
|
||||
* The [WearPrefsRepositoryImpl::getTemplateTileAndSaveTileId] method will handle both of the following cases:
|
||||
* 1. There was no Tile added, but there was a Template tile config stored in the prefs.
|
||||
* In this case, the stored config will be associated to the new tileId.
|
||||
* 2. There was a single Tile added, and there was a Template tile config stored in the prefs.
|
||||
* If there was a Tile update since updating the app, the tileId will be already
|
||||
* associated to the config, because it also calls [getTemplateTileAndSaveTileId].
|
||||
* If there was no Tile update yet, the new Tile will "steal" the config from the existing Tile,
|
||||
* and the old Tile will behave as it is the new Tile. This is needed because
|
||||
* we don't know if it's the 1st or 2nd Tile.
|
||||
*/
|
||||
wearPrefsRepository.getTemplateTileAndSaveTileId(requestParams.tileId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileRemoveEvent(requestParams: EventBuilders.TileRemoveEvent): Unit = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
wearPrefsRepository.removeTemplateTile(requestParams.tileId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Cleans up the coroutine
|
||||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
private suspend fun timeline(): Timeline {
|
||||
val template = wearPrefsRepository.getTemplateTileContent()
|
||||
private suspend fun timeline(templateTileConfig: TemplateTileConfig): Timeline {
|
||||
val renderedText = try {
|
||||
if (serverManager.isRegistered()) {
|
||||
serverManager.integrationRepository().renderTemplate(template, mapOf()).toString()
|
||||
serverManager.integrationRepository().renderTemplate(templateTileConfig.template, mapOf()).toString()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
@ -115,6 +148,10 @@ class TemplateTile : TileService() {
|
|||
return Timeline.fromLayoutElement(layout(renderedText))
|
||||
}
|
||||
|
||||
private suspend fun getTemplateTileConfig(tileId: Int): TemplateTileConfig {
|
||||
return wearPrefsRepository.getTemplateTileAndSaveTileId(tileId)
|
||||
}
|
||||
|
||||
fun layout(renderedText: String): LayoutElement = Box.Builder().apply {
|
||||
if (renderedText.isEmpty()) {
|
||||
addContent(
|
||||
|
|
Loading…
Reference in a new issue