From cc20ffe83b3ea17dc022568ff9c5f8fdec7266b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Sat, 1 Apr 2023 05:35:34 +0200 Subject: [PATCH] Add logged out state to Wear tiles (#3440) - Adds a generic tile to be used when the Wear app isn't logged in - Simplify tile root layout building using Timeline.fromLayoutElement --- common/src/main/res/values/strings.xml | 5 +- wear/build.gradle.kts | 1 + .../MobileAppIntegrationPresenterImpl.kt | 16 ++++ .../android/phone/PhoneSettingsListener.kt | 16 ++++ .../android/tiles/ConversationTile.kt | 24 ++++-- .../companion/android/tiles/LoggedOutTile.kt | 76 +++++++++++++++++++ .../companion/android/tiles/ShortcutsTile.kt | 31 +++++--- .../companion/android/tiles/TemplateTile.kt | 57 +++++++------- 8 files changed, 181 insertions(+), 45 deletions(-) create mode 100644 wear/src/main/java/io/homeassistant/companion/android/tiles/LoggedOutTile.kt 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(