From 5d52735e3694cafc63c3130743fd252b9610309b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Wed, 24 Jan 2024 17:56:39 +0200 Subject: [PATCH] Support multiple Template tiles on Wear OS (#3783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Remove unused field * Move padding to "no tiles" text * Add deep link --------- Co-authored-by: Joris Pelgröm --- .../settings/wear/SettingsWearViewModel.kt | 62 +++++++--- .../wear/views/SettingsWearHomeView.kt | 48 +++++-- .../wear/views/SettingsWearLandingView.kt | 2 +- .../wear/views/SettingsWearMainView.kt | 3 +- .../wear/views/SettingsWearTemplateTile.kt | 14 +++ .../views/SettingsWearTemplateTileList.kt | 117 ++++++++++++++++++ .../common/data/prefs/WearPrefsRepository.kt | 11 +- .../data/prefs/WearPrefsRepositoryImpl.kt | 95 +++++++++++--- .../prefs/impl/entities/TemplateTileConfig.kt | 28 +++++ .../android/common/util/WearDataMessages.kt | 3 +- common/src/main/res/values/strings.xml | 7 +- wear/src/main/AndroidManifest.xml | 15 ++- .../companion/android/home/HomeActivity.kt | 11 ++ .../companion/android/home/HomePresenter.kt | 7 +- .../android/home/HomePresenterImpl.kt | 15 ++- .../companion/android/home/MainViewModel.kt | 24 ++-- .../companion/android/home/views/HomeView.kt | 51 ++++++-- .../home/views/SelectTemplateTileView.kt | 74 +++++++++++ .../android/home/views/SettingsView.kt | 8 +- .../android/phone/PhoneSettingsListener.kt | 22 ++-- .../android/tiles/OpenTileSettingsActivity.kt | 9 ++ .../companion/android/tiles/TemplateTile.kt | 47 ++++++- 22 files changed, 571 insertions(+), 102 deletions(-) create mode 100644 app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTileList.kt create mode 100644 common/src/main/java/io/homeassistant/companion/android/common/data/prefs/impl/entities/TemplateTileConfig.kt create mode 100644 wear/src/main/java/io/homeassistant/companion/android/home/views/SelectTemplateTileView.kt diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/SettingsWearViewModel.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/SettingsWearViewModel.kt index fabab805e..2a26f85e3 100644 --- a/app/src/full/java/io/homeassistant/companion/android/settings/wear/SettingsWearViewModel.kt +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/SettingsWearViewModel.kt @@ -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() private set - var templateTileContent = mutableStateOf("") + var templateTiles = mutableStateMapOf() private set - var templateTileContentRendered = mutableStateOf("") - private set - var templateTileRefreshInterval = mutableStateOf(0) + var templateTilesRenderedTemplates = mutableStateMapOf() private set private val _resultSnackbar = MutableSharedFlow() @@ -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) { + 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().getString( + templateTilesRenderedTemplates[tileId] = getApplication().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 = objectMapper.readValue( + data.getString( + WearDataMessages.CONFIG_TEMPLATE_TILES, + "{}" + ) + ) + setTemplateTiles(templateTilesFromWear) _hasData.value = true } diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearHomeView.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearHomeView.kt index c4bce7605..39245db6d 100644 --- a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearHomeView.kt +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearHomeView.kt @@ -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,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() + } + ) + } + } } } } diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearLandingView.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearLandingView.kt index e8bc4ce47..6db3cadd6 100644 --- a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearLandingView.kt +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearLandingView.kt @@ -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, diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearMainView.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearMainView.kt index 864829a37..936b08dea 100644 --- a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearMainView.kt +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearMainView.kt @@ -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, url: String?): Intent { currentNodes = wearNodes diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTile.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTile.kt index 0e5ff2e8c..08adc3ac3 100644 --- a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTile.kt +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTile.kt @@ -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 = {} + ) +} diff --git a/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTileList.kt b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTileList.kt new file mode 100644 index 000000000..85329f965 --- /dev/null +++ b/app/src/full/java/io/homeassistant/companion/android/settings/wear/views/SettingsWearTemplateTileList.kt @@ -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, + 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 = {} + ) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt index b2beb6b84..b038baac1 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt @@ -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> suspend fun getTileShortcutsAndSaveTileId(tileId: Int): List @@ -7,10 +9,11 @@ interface WearPrefsRepository { suspend fun removeTileShortcuts(tileId: Int?): List? 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 + suspend fun getTemplateTileAndSaveTileId(tileId: Int): TemplateTileConfig + suspend fun setAllTemplateTiles(templateTiles: Map) + 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 diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt index 7f0bf4595..1fad324d7 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt @@ -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 { + 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) { + 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 { diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/impl/entities/TemplateTileConfig.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/impl/entities/TemplateTileConfig.kt new file mode 100644 index 000000000..b49ed84a1 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/impl/entities/TemplateTileConfig.kt @@ -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 + ) + ) + } +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/util/WearDataMessages.kt b/common/src/main/java/io/homeassistant/companion/android/common/util/WearDataMessages.kt index 278c8ee7a..2f065f1cc 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/util/WearDataMessages.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/util/WearDataMessages.kt @@ -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" } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 5cb57d008..412c7f06b 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -846,7 +846,12 @@ Processing tag Template Template tile - Set template to display on Wear OS tile + Template tile #%d + Template tiles + Configure Template tiles + Select Template tile to manage + There are no Template tiles added yet - add one from the watch face to set it up + Configure templates to use as Wear OS tiles Change template in phone settings Template tile content Renders and displays a template diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 19dc629b3..b16e17708 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -97,6 +97,11 @@ + + + + + @@ -160,6 +165,14 @@ + + + + + android:path="/updateTemplateTiles" /> diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt index c9ad3f8a0..70664b404 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt @@ -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?) { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt index a99de612e..a39105c49 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt @@ -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 + suspend fun setTemplateTileRefreshInterval(tileId: Int, interval: Int) + suspend fun getWearFavoritesOnly(): Boolean suspend fun setWearFavoritesOnly(enabled: Boolean) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt index 257ee915b..c91c48492 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt @@ -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 { + return wearPrefsRepository.getAllTemplateTiles() } - override suspend fun getTemplateTileRefreshInterval(): Int { - return wearPrefsRepository.getTemplateTileRefreshInterval() - } - - override suspend fun setTemplateTileRefreshInterval(interval: Int) { - wearPrefsRepository.setTemplateTileRefreshInterval(interval) + override suspend fun setTemplateTileRefreshInterval(tileId: Int, interval: Int) { + getAllTemplateTiles()[tileId]?.let { + wearPrefsRepository.setTemplateTile(tileId, it.template, interval) + } } override suspend fun getWearFavoritesOnly(): Boolean { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt index ed7cfd071..8c2bd85c2 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt @@ -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() 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) + } } } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt index 6f790e873..1ffd48b78 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt @@ -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() } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectTemplateTileView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectTemplateTileView.kt new file mode 100644 index 000000000..2be0d0b20 --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SelectTemplateTileView.kt @@ -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, + 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 = {} + ) +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt index 4fdea148a..b412756b1 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt @@ -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 = {} ) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt b/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt index 17ee1c147..a4b356a39 100755 --- a/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt @@ -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 = objectMapper.readValue( + dataMap.getString( + WearDataMessages.CONFIG_TEMPLATE_TILES, + "{}" + ) + ) + + wearPrefsRepository.setAllTemplateTiles(templateTilesFromPhone) } private fun updateTiles() = mainScope.launch { diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenTileSettingsActivity.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenTileSettingsActivity.kt index 4ea43bbed..fc7d3ad5e 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenTileSettingsActivity.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/OpenTileSettingsActivity.kt @@ -34,6 +34,15 @@ class OpenTileSettingsActivity : AppCompatActivity() { tileId = it ) } + "ConfigTemplateTile" -> { + lifecycleScope.launch { + wearPrefsRepository.getTemplateTileAndSaveTileId(tileId) + } + HomeActivity.getTemplateTileSettingsIntent( + context = this, + tileId = it + ) + } else -> null } diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/TemplateTile.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/TemplateTile.kt index e53476f49..8e638f710 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/tiles/TemplateTile.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/TemplateTile.kt @@ -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(