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
This commit is contained in:
Joris Pelgröm 2023-04-01 05:35:34 +02:00 committed by GitHub
parent b6688c6388
commit cc20ffe83b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 181 additions and 45 deletions

View File

@ -708,6 +708,7 @@
<string name="shortcuts_tile_description">Select up to 7 entities</string>
<string name="shortcuts_tile_empty">Choose entities in settings</string>
<string name="shortcuts_tile_text_setting">Show names on shortcuts tile</string>
<string name="shortcuts_tile_log_in">Log in to Home Assistant to add your first shortcut</string>
<string name="shortcuts">Shortcuts</string>
<string name="show">Show</string>
<string name="show_share_logs_summary">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</string>
@ -764,6 +765,7 @@
<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_log_in">Log in to Home Assistant to set up a template</string>
<string name="template_error">Error in template</string>
<string name="template_render_error">Error rendering template</string>
<string name="template_tile_help">Provide a template below that will be displayed on the Wear OS template tile. See help for markup options.</string>
@ -1045,7 +1047,8 @@
<string name="no_conversation_support">You must be at least on Home Assistant 2023.1 and have the conversation integration enabled</string>
<string name="conversation">Conversation</string>
<string name="assist">Assist</string>
<string name="not_registered">Please launch the Home Assistant app and login before you can use the assist feature.</string>
<string name="assist_log_in">Log in to Home Assistant to start using Assist</string>
<string name="not_registered">Please launch the Home Assistant app and log in to start using Assist.</string>
<string name="ha_assist">HA: Assist</string>
<string name="only_favorites">Only Show Favorites</string>
<string name="beacon_scanning">Beacon Monitor Scanning</string>

View File

@ -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")

View File

@ -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()
}

View File

@ -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")
}
}
}

View File

@ -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<Tile> =
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()
}

View File

@ -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()
)
}

View File

@ -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<SimplifiedEntity>, showLabels: Boolean): LayoutElement = Column.Builder().apply {
if (entities.isEmpty()) {
addContent(

View File

@ -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(