Skip to content

Commit

Permalink
Template tile for wear OS (#2122)
Browse files Browse the repository at this point in the history
* Initial implementation of template tile

Includes settings on phone and watch. Watch settings will be removed in next commit

* Add refresh interval setting to wear app

* Update wear settings layout

* Add interval setting to phone app

* ktlint

* Add example image to the manifest

* Add preview of rendered template in phone settings

* Process review comments

* Use resources for interval strings
  • Loading branch information
leroyboerefijn authored Jan 13, 2022
1 parent 70e6207 commit 3a15c0f
Show file tree
Hide file tree
Showing 22 changed files with 664 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class SettingsWearViewModel @Inject constructor(
private const val KEY_UPDATE_TIME = "UpdateTime"
private const val KEY_IS_AUTHENTICATED = "isAuthenticated"
private const val KEY_FAVORITES = "favorites"
private const val KEY_TEMPLATE_TILE = "templateTile"
private const val KEY_TEMPLATE_TILE_REFRESH_INTERVAL = "templateTileRefreshInterval"
}

private val objectMapper = jacksonObjectMapper()
Expand All @@ -55,6 +57,12 @@ class SettingsWearViewModel @Inject constructor(
private set
var favoriteEntityIds = mutableStateListOf<String>()
private set
var templateTileContent = mutableStateOf("")
private set
var templateTileContentRendered = mutableStateOf("")
private set
var templateTileRefreshInterval = mutableStateOf(0)
private set

init {
Wearable.getDataClient(application).addListener(this)
Expand Down Expand Up @@ -96,6 +104,24 @@ class SettingsWearViewModel @Inject constructor(
Wearable.getDataClient(getApplication<HomeAssistantApplication>()).removeListener(this)
}

fun setTemplateContent(template: String) {
templateTileContent.value = template
if (template.isNotEmpty()) {
viewModelScope.launch {
try {
templateTileContentRendered.value =
integrationUseCase.renderTemplate(template, mapOf())
} catch (e: Exception) {
templateTileContentRendered.value = getApplication<Application>().getString(
commonR.string.template_tile_error
)
}
}
} else {
templateTileContentRendered.value = ""
}
}

fun onEntitySelected(checked: Boolean, entityId: String) {
if (checked)
favoriteEntityIds.add(entityId)
Expand Down Expand Up @@ -151,8 +177,22 @@ class SettingsWearViewModel @Inject constructor(
}

Wearable.getDataClient(getApplication<HomeAssistantApplication>()).putDataItem(putDataRequest).apply {
addOnSuccessListener { Log.d(TAG, "Successfully sent favorites to wear") }
addOnFailureListener { e -> Log.e(TAG, "Failed to send favorites to wear", e) }
addOnSuccessListener { Log.d(TAG, "Successfully sent auth to wear") }
addOnFailureListener { e -> Log.e(TAG, "Failed to send auth to wear", e) }
}
}

fun sendTemplateTileInfo() {
val putDataRequest = PutDataMapRequest.create("/updateTemplateTile").run {
dataMap.putString(KEY_TEMPLATE_TILE, templateTileContent.value)
dataMap.putInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, templateTileRefreshInterval.value)
setUrgent()
asPutDataRequest()
}

Wearable.getDataClient(getApplication<HomeAssistantApplication>()).putDataItem(putDataRequest).apply {
addOnSuccessListener { Log.d(TAG, "Successfully sent tile template to wear") }
addOnFailureListener { e -> Log.e(TAG, "Failed to send tile template to wear", e) }
}
}

Expand Down Expand Up @@ -180,6 +220,8 @@ class SettingsWearViewModel @Inject constructor(
favoriteEntityIdList.forEach { entityId ->
favoriteEntityIds.add(entityId)
}
setTemplateContent(data.getString(KEY_TEMPLATE_TILE, ""))
templateTileRefreshInterval.value = data.getInt(KEY_TEMPLATE_TILE_REFRESH_INTERVAL, 0)
hasData.value = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,28 @@ fun LoadSettingsHomeView(
hasData = settingsWearViewModel.hasData.value,
isAuthed = settingsWearViewModel.isAuthenticated.value,
navigateFavorites = { navController.navigate(SettingsWearMainView.FAVORITES) },
navigateTemplateTile = { navController.navigate(SettingsWearMainView.TEMPLATE) },
loginWearOs = loginWearOs
)
}
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()
},
onBackClicked = {
navController.navigateUp()
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fun SettingWearLandingView(
hasData: Boolean,
isAuthed: Boolean,
navigateFavorites: () -> Unit,
navigateTemplateTile: () -> Unit,
loginWearOs: () -> Unit
) {
val context = LocalContext.current
Expand Down Expand Up @@ -77,6 +78,14 @@ fun SettingWearLandingView(
) {
Text(stringResource(commonR.string.set_favorites_on_device))
}
Button(
onClick = navigateTemplateTile,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp, end = 10.dp)
) {
Text(stringResource(commonR.string.template_tile))
}
}
else -> {
Button(
Expand All @@ -101,6 +110,7 @@ private fun PreviewSettingWearLandingView() {
hasData = true,
isAuthed = true,
navigateFavorites = {},
navigateTemplateTile = {},
loginWearOs = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SettingsWearMainView : AppCompatActivity() {
private var currentNodes = setOf<Node>()
const val LANDING = "Landing"
const val FAVORITES = "Favorites"
const val TEMPLATE = "Template"

fun newInstance(context: Context, wearNodes: Set<Node>): Intent {
currentNodes = wearNodes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.homeassistant.companion.android.settings.wear.views

import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.Box
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.layout.width
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.util.IntervalToString
import io.homeassistant.companion.android.common.R as commonR

@Composable
fun SettingsWearTemplateTile(
template: String,
renderedTemplate: String,
refreshInterval: Int,
onContentChanged: (String) -> Unit,
onRefreshIntervalChanged: (Int) -> Unit,
onBackClicked: () -> Unit
) {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(commonR.string.template_tile)) },
navigationIcon = {
IconButton(
onClick = onBackClicked
) {
Image(
asset = CommunityMaterial.Icon.cmd_arrow_left,
colorFilter = ColorFilter.tint(colorResource(R.color.colorIcon))
)
}
},
actions = {
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(WEAR_DOCS_LINK))
context.startActivity(intent)
}) {
Icon(
Icons.Filled.HelpOutline,
contentDescription = stringResource(id = commonR.string.help)
)
}
}
)
}
) {
Column(Modifier.padding(all = 16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
asset = CommunityMaterial.Icon3.cmd_timer_cog,
colorFilter = ColorFilter.tint(colorResource(R.color.colorPrimary)),
modifier = Modifier
.height(24.dp)
.width(24.dp)
)
Text(
stringResource(commonR.string.refresh_interval),
modifier = Modifier.padding(start = 4.dp, end = 4.dp)
)
Box {
var dropdownExpanded by remember { mutableStateOf(false) }
OutlinedButton(
onClick = { dropdownExpanded = true }
) {
Text(IntervalToString(LocalContext.current, refreshInterval))
}
DropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
val options = listOf(0, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 60 * 60, 5 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60)
for (option in options) {
DropdownMenuItem(onClick = {
onRefreshIntervalChanged(option)
dropdownExpanded = false
}) {
Text(IntervalToString(LocalContext.current, option))
}
}
}
}
}
Text(stringResource(commonR.string.template_tile_help))
TextField(
value = template,
onValueChange = onContentChanged,
label = {
Text(stringResource(commonR.string.template_tile_content))
},
modifier = Modifier.padding(top = 8.dp),
maxLines = 10
)
Text(
renderedTemplate,
fontSize = 12.sp,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ interface IntegrationRepository {

suspend fun getTileShortcuts(): List<String>
suspend fun setTileShortcuts(entities: List<String>)
suspend fun getTemplateTileContent(): String
suspend fun setTemplateTileContent(content: String)
suspend fun getTemplateTileRefreshInterval(): Int
suspend fun setTemplateTileRefreshInterval(interval: Int)
suspend fun setWearHapticFeedback(enabled: Boolean)
suspend fun getWearHapticFeedback(): Boolean
suspend fun setWearToastConfirmation(enabled: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class IntegrationRepositoryImpl @Inject constructor(

private const val PREF_CHECK_SENSOR_REGISTRATION_NEXT = "sensor_reg_last"
private const val PREF_TILE_SHORTCUTS = "tile_shortcuts_list"
private const val PREF_TILE_TEMPLATE = "tile_template"
private const val PREF_TILE_TEMPLATE_REFRESH_INTERVAL = "tile_template_refresh_interval"
private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback"
private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation"
private const val PREF_HA_VERSION = "ha_version"
Expand Down Expand Up @@ -372,6 +374,22 @@ class IntegrationRepositoryImpl @Inject constructor(
localStorage.putString(PREF_TILE_SHORTCUTS, JSONArray(entities).toString())
}

override suspend fun getTemplateTileContent(): String {
return localStorage.getString(PREF_TILE_TEMPLATE) ?: ""
}

override suspend fun setTemplateTileContent(content: String) {
localStorage.putString(PREF_TILE_TEMPLATE, content)
}

override suspend fun getTemplateTileRefreshInterval(): Int {
return localStorage.getInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL) ?: 0
}

override suspend fun setTemplateTileRefreshInterval(interval: Int) {
localStorage.putInt(PREF_TILE_TEMPLATE_REFRESH_INTERVAL, interval)
}

override suspend fun setWearHapticFeedback(enabled: Boolean) {
localStorage.putBoolean(PREF_WEAR_HAPTIC_FEEDBACK, enabled)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.homeassistant.companion.android.util

import android.content.Context
import io.homeassistant.companion.android.common.R

fun IntervalToString(context: Context, interval: Int): String {
return when {
interval == 0 -> context.getString(R.string.interval_manual)
interval >= 60 * 60 -> context.resources.getQuantityString(R.plurals.interval_hours, interval / 60 / 60, interval / 60 / 60)
interval >= 60 -> context.resources.getQuantityString(R.plurals.interval_minutes, interval / 60, interval / 60)
else -> context.resources.getQuantityString(R.plurals.interval_seconds, interval, interval)
}
}
21 changes: 21 additions & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,19 @@
<string name="input_url_hint">https://example.duckdns.org:8123</string>
<string name="input_url">Home Assistant URL</string>
<string name="install_app">Install App on Wear Device</string>
<plurals name="interval_hours">
<item quantity="one">%d hour</item>
<item quantity="other">%d hours</item>
</plurals>
<string name="interval_manual">Manual</string>
<plurals name="interval_minutes">
<item quantity="one">%d minute</item>
<item quantity="other">%d minutes</item>
</plurals>
<plurals name="interval_seconds">
<item quantity="one">%d second</item>
<item quantity="other">%d seconds</item>
</plurals>
<string name="irreversible">This action is irreversible</string>
<string name="keep_screen_on_def">Do not lock screen when Lovelace dashboard is active</string>
<string name="keep_screen_on">Keep screen On</string>
Expand Down Expand Up @@ -350,6 +363,7 @@
<string name="refresh_internal">Refresh Internal URL</string>
<string name="refresh_log">Refresh logs</string>
<string name="refresh">Refresh</string>
<string name="refresh_interval">Refresh interval</string>
<string name="registerDevice">Register watch</string>
<string name="remaining">Remaining</string>
<string name="remember">Remember</string>
Expand Down Expand Up @@ -599,6 +613,13 @@
<string name="successful">Successful</string>
<string name="switches">Switches</string>
<string name="tag_reader_title">Processing Tag</string>
<string name="template_tile">Template tile</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>
<string name="template_tile_empty">Set template in the phone settings</string>
<string name="template_tile_error">Error in template</string>
<string name="template_tile_help">Provide a template below that will be displayed on the Wear OS template tile.</string>
<string name="template_widget_default">Enter Template Here</string>
<string name="template_widget_desc">Render any template with HTML formatting</string>
<string name="template_widget">Template Widget</string>
Expand Down
1 change: 1 addition & 0 deletions wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ dependencies {
implementation("androidx.wear:wear:1.2.0")
implementation("com.google.android.support:wearable:2.8.1")
implementation("com.google.android.gms:play-services-wearable:17.1.0")
implementation("androidx.wear:wear-input:1.2.0-alpha02")
compileOnly("com.google.android.wearable:wearable:2.8.1")

implementation("com.google.dagger:hilt-android:2.40.5")
Expand Down
Loading

0 comments on commit 3a15c0f

Please sign in to comment.