diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index ae2b182cf..b4e0ea750 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -708,6 +708,7 @@
Select up to 7 entities
Choose entities in settings
Show names on shortcuts tile
+ Log in to Home Assistant to add your first shortcut
Shortcuts
Show
Sharing logs with the Home Assistant team will help to solve issues. Please share the logs only if you have been asked to do so by a Home Assistant developer
@@ -764,6 +765,7 @@
Template tile content
Renders and displays a template
Set template in the phone settings
+ Log in to Home Assistant to set up a template
Error in template
Error rendering template
Provide a template below that will be displayed on the Wear OS template tile. See help for markup options.
@@ -1045,7 +1047,8 @@
You must be at least on Home Assistant 2023.1 and have the conversation integration enabled
Conversation
Assist
- Please launch the Home Assistant app and login before you can use the assist feature.
+ Log in to Home Assistant to start using Assist
+ Please launch the Home Assistant app and log in to start using Assist.
HA: Assist
Only Show Favorites
Beacon Monitor Scanning
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index d6b185369..d705fd020 100644
--- a/wear/build.gradle.kts
+++ b/wear/build.gradle.kts
@@ -124,6 +124,7 @@ dependencies {
implementation("com.google.guava:guava:31.1-android")
implementation("androidx.wear.tiles:tiles:1.1.0")
+ implementation("androidx.wear.tiles:tiles-material:1.1.0")
implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1")
diff --git a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt
index d5666f84a..67d86ea70 100644
--- a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt
+++ b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt
@@ -2,11 +2,15 @@ package io.homeassistant.companion.android.onboarding.integration
import android.content.Context
import android.util.Log
+import androidx.wear.tiles.TileService
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.onboarding.getMessagingToken
+import io.homeassistant.companion.android.tiles.ConversationTile
+import io.homeassistant.companion.android.tiles.ShortcutsTile
+import io.homeassistant.companion.android.tiles.TemplateTile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -47,10 +51,22 @@ class MobileAppIntegrationPresenterImpl @Inject constructor(
view.showError()
return@launch
}
+ updateTiles()
view.deviceRegistered()
}
}
+ private fun updateTiles() = mainScope.launch {
+ try {
+ val updater = TileService.getUpdater(view as Context)
+ updater.requestUpdate(ConversationTile::class.java)
+ updater.requestUpdate(ShortcutsTile::class.java)
+ updater.requestUpdate(TemplateTile::class.java)
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to request tiles update")
+ }
+ }
+
override fun onFinish() {
mainScope.cancel()
}
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 840ddf9b2..23b52f0c4 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
@@ -3,6 +3,7 @@ package io.homeassistant.companion.android.phone
import android.annotation.SuppressLint
import android.content.Intent
import android.util.Log
+import androidx.wear.tiles.TileService
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.wearable.DataClient
@@ -31,6 +32,9 @@ import io.homeassistant.companion.android.database.wear.replaceAll
import io.homeassistant.companion.android.home.HomeActivity
import io.homeassistant.companion.android.home.HomePresenterImpl
import io.homeassistant.companion.android.onboarding.getMessagingToken
+import io.homeassistant.companion.android.tiles.ConversationTile
+import io.homeassistant.companion.android.tiles.ShortcutsTile
+import io.homeassistant.companion.android.tiles.TemplateTile
import io.homeassistant.companion.android.util.UrlUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -150,6 +154,7 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
)
)
serverManager.convertTemporaryServer(serverId)
+ updateTiles()
val intent = HomeActivity.newInstance(applicationContext)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -184,4 +189,15 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
wearPrefsRepository.setTemplateTileContent(content)
wearPrefsRepository.setTemplateTileRefreshInterval(interval)
}
+
+ private fun updateTiles() = mainScope.launch {
+ try {
+ val updater = TileService.getUpdater(applicationContext)
+ updater.requestUpdate(ConversationTile::class.java)
+ updater.requestUpdate(ShortcutsTile::class.java)
+ updater.requestUpdate(TemplateTile::class.java)
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to request tiles update")
+ }
+ }
}
diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/ConversationTile.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/ConversationTile.kt
index 0bdd1d79f..5c0eb1f61 100755
--- a/wear/src/main/java/io/homeassistant/companion/android/tiles/ConversationTile.kt
+++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/ConversationTile.kt
@@ -16,33 +16,41 @@ import androidx.wear.tiles.ResourceBuilders.Resources
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
-import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.R
+import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.conversation.ConversationActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.guava.future
+import javax.inject.Inject
+import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
class ConversationTile : TileService() {
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
+ @Inject
+ lateinit var serverManager: ServerManager
+
override fun onTileRequest(requestParams: TileRequest): ListenableFuture =
serviceScope.future {
Tile.Builder()
.setResourcesVersion("1")
.setTimeline(
- Timeline.Builder().addTimelineEntry(
- TimelineEntry.Builder().setLayout(
- LayoutElementBuilders.Layout.Builder().setRoot(
- boxLayout()
- ).build()
- ).build()
- ).build()
+ if (serverManager.isRegistered()) {
+ Timeline.fromLayoutElement(boxLayout())
+ } else {
+ loggedOutTimeline(
+ this@ConversationTile,
+ requestParams,
+ commonR.string.assist,
+ commonR.string.assist_log_in
+ )
+ }
).build()
}
diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/LoggedOutTile.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/LoggedOutTile.kt
new file mode 100644
index 000000000..cf5da41b0
--- /dev/null
+++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/LoggedOutTile.kt
@@ -0,0 +1,76 @@
+package io.homeassistant.companion.android.tiles
+
+import android.content.Context
+import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.wear.tiles.ActionBuilders
+import androidx.wear.tiles.ColorBuilders.argb
+import androidx.wear.tiles.ModifiersBuilders
+import androidx.wear.tiles.RequestBuilders
+import androidx.wear.tiles.TimelineBuilders.Timeline
+import androidx.wear.tiles.material.ChipColors
+import androidx.wear.tiles.material.Colors
+import androidx.wear.tiles.material.CompactChip
+import androidx.wear.tiles.material.Text
+import androidx.wear.tiles.material.Typography
+import androidx.wear.tiles.material.layouts.PrimaryLayout
+import io.homeassistant.companion.android.R
+import io.homeassistant.companion.android.splash.SplashActivity
+import io.homeassistant.companion.android.common.R as commonR
+
+/**
+ * A [Timeline] with a single entry, asking the user to log in to the app to start using the tile
+ * with a button to open the app. The tile is using the 'Dialog' style.
+ */
+fun loggedOutTimeline(
+ context: Context,
+ requestParams: RequestBuilders.TileRequest,
+ @StringRes title: Int,
+ @StringRes text: Int
+): Timeline {
+ val theme = Colors(
+ ContextCompat.getColor(context, R.color.colorPrimary), // Primary
+ ContextCompat.getColor(context, R.color.colorOnPrimary), // On primary
+ ContextCompat.getColor(context, R.color.colorOverlay), // Surface
+ ContextCompat.getColor(context, android.R.color.white) // On surface
+ )
+ val chipColors = ChipColors.primaryChipColors(theme)
+ val chipAction = ModifiersBuilders.Clickable.Builder()
+ .setId("login")
+ .setOnClick(
+ ActionBuilders.LaunchAction.Builder()
+ .setAndroidActivity(
+ ActionBuilders.AndroidActivity.Builder()
+ .setClassName(SplashActivity::class.java.name)
+ .setPackageName(context.packageName)
+ .build()
+ ).build()
+ ).build()
+ return Timeline.fromLayoutElement(
+ PrimaryLayout.Builder(requestParams.deviceParameters!!)
+ .setPrimaryLabelTextContent(
+ Text.Builder(context, context.getString(title))
+ .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+ .setColor(argb(theme.primary))
+ .build()
+ )
+ .setContent(
+ Text.Builder(context, context.getString(text))
+ .setTypography(Typography.TYPOGRAPHY_BODY1)
+ .setMaxLines(10)
+ .setColor(argb(theme.onSurface))
+ .build()
+ )
+ .setPrimaryChipContent(
+ CompactChip.Builder(
+ context,
+ context.getString(commonR.string.login),
+ chipAction,
+ requestParams.deviceParameters!!
+ )
+ .setChipColors(chipColors)
+ .build()
+ )
+ .build()
+ )
+}
diff --git a/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt b/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt
index a7f18ff76..2c8f5b2ff 100644
--- a/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt
+++ b/wear/src/main/java/io/homeassistant/companion/android/tiles/ShortcutsTile.kt
@@ -13,7 +13,6 @@ import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.LayoutElementBuilders.Box
import androidx.wear.tiles.LayoutElementBuilders.Column
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
-import androidx.wear.tiles.LayoutElementBuilders.Layout
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
import androidx.wear.tiles.LayoutElementBuilders.Row
import androidx.wear.tiles.LayoutElementBuilders.Spacer
@@ -25,7 +24,6 @@ import androidx.wear.tiles.ResourceBuilders.Resources
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
-import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import com.google.common.util.concurrent.ListenableFuture
import com.mikepenz.iconics.IconicsColor
import com.mikepenz.iconics.IconicsDrawable
@@ -36,6 +34,7 @@ import com.mikepenz.iconics.utils.sizeDp
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.servers.ServerManager
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.util.getIcon
import kotlinx.coroutines.CoroutineScope
@@ -61,6 +60,9 @@ class ShortcutsTile : TileService() {
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
+ @Inject
+ lateinit var serverManager: ServerManager
+
@Inject
lateinit var wearPrefsRepository: WearPrefsRepository
@@ -77,18 +79,20 @@ class ShortcutsTile : TileService() {
}
val entities = getEntities()
- val showLabels = wearPrefsRepository.getShowShortcutText()
Tile.Builder()
.setResourcesVersion(entities.toString())
.setTimeline(
- Timeline.Builder().addTimelineEntry(
- TimelineEntry.Builder().setLayout(
- Layout.Builder().setRoot(
- layout(entities, showLabels)
- ).build()
- ).build()
- ).build()
+ if (serverManager.isRegistered()) {
+ timeline()
+ } else {
+ loggedOutTimeline(
+ this@ShortcutsTile,
+ requestParams,
+ commonR.string.shortcuts,
+ commonR.string.shortcuts_tile_log_in
+ )
+ }
).build()
}
@@ -149,6 +153,13 @@ class ShortcutsTile : TileService() {
return wearPrefsRepository.getTileShortcuts().map { SimplifiedEntity(it) }
}
+ private suspend fun timeline(): Timeline {
+ val entities = getEntities()
+ val showLabels = wearPrefsRepository.getShowShortcutText()
+
+ return Timeline.fromLayoutElement(layout(entities, showLabels))
+ }
+
fun layout(entities: List, showLabels: Boolean): LayoutElement = Column.Builder().apply {
if (entities.isEmpty()) {
addContent(
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 b8b0964ea..991e763ee 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
@@ -22,7 +22,6 @@ import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.LayoutElementBuilders.Box
import androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
-import androidx.wear.tiles.LayoutElementBuilders.Layout
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
import androidx.wear.tiles.ModifiersBuilders
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
@@ -32,7 +31,6 @@ import androidx.wear.tiles.ResourceBuilders.Resources
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
-import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import com.fasterxml.jackson.databind.JsonMappingException
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
@@ -73,36 +71,22 @@ class TemplateTile : TileService() {
}
}
- val template = wearPrefsRepository.getTemplateTileContent()
- val renderedText = try {
- if (serverManager.isRegistered()) {
- serverManager.integrationRepository().renderTemplate(template, mapOf()).toString()
- } else {
- ""
- }
- } catch (e: Exception) {
- Log.e("TemplateTile", "Exception while rendering template", e)
- // JsonMappingException suggests that template is not a String (= error)
- if (e.cause is JsonMappingException) {
- getString(commonR.string.template_error)
- } else {
- getString(commonR.string.template_render_error)
- }
- }
-
Tile.Builder()
.setResourcesVersion("1")
.setFreshnessIntervalMillis(
wearPrefsRepository.getTemplateTileRefreshInterval().toLong() * 1000
)
.setTimeline(
- Timeline.Builder().addTimelineEntry(
- TimelineEntry.Builder().setLayout(
- Layout.Builder().setRoot(
- layout(renderedText)
- ).build()
- ).build()
- ).build()
+ if (serverManager.isRegistered()) {
+ timeline()
+ } else {
+ loggedOutTimeline(
+ this@TemplateTile,
+ requestParams,
+ commonR.string.template,
+ commonR.string.template_tile_log_in
+ )
+ }
).build()
}
@@ -128,6 +112,27 @@ class TemplateTile : TileService() {
serviceJob.cancel()
}
+ private suspend fun timeline(): Timeline {
+ val template = wearPrefsRepository.getTemplateTileContent()
+ val renderedText = try {
+ if (serverManager.isRegistered()) {
+ serverManager.integrationRepository().renderTemplate(template, mapOf()).toString()
+ } else {
+ ""
+ }
+ } catch (e: Exception) {
+ Log.e("TemplateTile", "Exception while rendering template", e)
+ // JsonMappingException suggests that template is not a String (= error)
+ if (e.cause is JsonMappingException) {
+ getString(commonR.string.template_error)
+ } else {
+ getString(commonR.string.template_render_error)
+ }
+ }
+
+ return Timeline.fromLayoutElement(layout(renderedText))
+ }
+
fun layout(renderedText: String): LayoutElement = Box.Builder().apply {
if (renderedText.isEmpty()) {
addContent(