diff --git a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileLayout.kt b/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileLayout.kt index 3c45b1b3e..2f96ae981 100644 --- a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileLayout.kt +++ b/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileLayout.kt @@ -16,10 +16,12 @@ package com.example.wear.tiles.messaging import android.content.Context +import android.graphics.Bitmap import androidx.annotation.DrawableRes import androidx.wear.protolayout.DeviceParametersBuilders import androidx.wear.protolayout.ModifiersBuilders import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.ResourceBuilders.ImageResource import androidx.wear.protolayout.ResourceBuilders.Resources import androidx.wear.protolayout.material.Button import androidx.wear.protolayout.material.ButtonColors @@ -37,9 +39,6 @@ import com.example.wear.tiles.tools.emptyClickable /** * Layout definition for the Messaging Tile. - * - * By separating the layout completely, we can pass fake data for the [messagingTilePreview] so it - * can be rendered in Android Studio (use the "Split" or "Design" editor modes). */ internal fun messagingTileLayout( state: MessagingTileState, @@ -88,14 +87,14 @@ private fun contactLayout( } .build() -private fun Contact.imageResourceId() = "${MessagingTileRenderer.ID_CONTACT_PREFIX}$id" +private fun Contact.imageResourceId() = "${MessagingTileService.ID_CONTACT_PREFIX}$id" private fun searchLayout( context: Context, clickable: ModifiersBuilders.Clickable ) = Button.Builder(context, clickable) .setContentDescription(context.getString(R.string.tile_messaging_search)) - .setIconContent(MessagingTileRenderer.ID_IC_SEARCH) + .setIconContent(MessagingTileService.ID_IC_SEARCH) .setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors)) .build() @@ -113,7 +112,7 @@ private fun messagingTilePreview(context: Context): TilePreviewData { R.drawable.taylor ) addIdToImageMapping( - MessagingTileRenderer.ID_IC_SEARCH, + MessagingTileService.ID_IC_SEARCH, R.drawable.ic_search_24 ) }, @@ -148,7 +147,7 @@ private fun contactWithImagePreview(context: Context): TilePreviewData { return TilePreviewData( onTileResourceRequest = { Resources.Builder().addIdToImageMapping( - "${MessagingTileRenderer.ID_CONTACT_PREFIX}${contact.id}", + "${MessagingTileService.ID_CONTACT_PREFIX}${contact.id}", R.drawable.ali ).build() }, @@ -163,7 +162,7 @@ private fun contactWithImagePreview(context: Context): TilePreviewData { private fun searchButtonPreview(context: Context) = TilePreviewData( onTileResourceRequest = { Resources.Builder().addIdToImageMapping( - MessagingTileRenderer.ID_IC_SEARCH, + MessagingTileService.ID_IC_SEARCH, R.drawable.ic_search_24 ).build() }, @@ -177,7 +176,7 @@ fun Resources.Builder.addIdToImageMapping( id: String, @DrawableRes resId: Int ): Resources.Builder = addIdToImageMapping( - id, ResourceBuilders.ImageResource.Builder() + id, ImageResource.Builder() .setAndroidResourceByResId( ResourceBuilders.AndroidImageResourceByResId.Builder() .setResourceId(resId) @@ -185,3 +184,10 @@ fun Resources.Builder.addIdToImageMapping( ) .build() ) + +fun Resources.Builder.addIdToImageMapping( + id: String, + bitmap: Bitmap +): Resources.Builder = addIdToImageMapping( + id, bitmapToImageResource(bitmap) +) diff --git a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileRenderer.kt b/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileRenderer.kt deleted file mode 100644 index 2d81902a6..000000000 --- a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileRenderer.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.wear.tiles.messaging - -import android.content.Context -import android.graphics.Bitmap -import androidx.wear.protolayout.DeviceParametersBuilders -import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement -import androidx.wear.protolayout.ResourceBuilders.Resources -import com.example.wear.tiles.R -import com.google.android.horologist.tiles.images.drawableResToImageResource -import com.google.android.horologist.tiles.render.SingleTileLayoutRenderer - -class MessagingTileRenderer(context: Context) : - SingleTileLayoutRenderer>(context) { - - override fun renderTile( - state: MessagingTileState, - deviceParameters: DeviceParametersBuilders.DeviceParameters - ): LayoutElement { - return messagingTileLayout(state, context, deviceParameters) - } - - /** - * If we want to display an image in our layout, we have to add the image resource to the - * [Resources.Builder]. - */ - override fun Resources.Builder.produceRequestedResources( - resourceState: Map, - deviceParameters: DeviceParametersBuilders.DeviceParameters, - resourceIds: List - ) { - // If `resourceIds` is empty, it means all resources are being requested so we should add - // the Search image resource in both of these cases. - if (resourceIds.isEmpty() || resourceIds.contains(ID_IC_SEARCH)) { - addIdToImageMapping( - /* id = */ - ID_IC_SEARCH, - /* image = */ - R.drawable.ic_search_24 - ) - } - - // We already checked `resourceIds` in `MessagingTileService` because we needed to know - // which avatars needed to be fetched from the network; `resourceResults` was already - // filtered so it only contains bitmaps for the requested resources. - resourceState.forEach { (contact, bitmap) -> - addIdToImageMapping( - /* id = */ - "$ID_CONTACT_PREFIX${contact.id}", - /* image = */ - bitmapToImageResource(bitmap) - ) - } - } - - companion object { - - // Resource identifiers for images - internal const val ID_IC_SEARCH = "ic_search" - internal const val ID_CONTACT_PREFIX = "contact:" - } -} diff --git a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileService.kt b/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileService.kt index c4a45c0e5..960bd7afd 100644 --- a/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileService.kt +++ b/WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/MessagingTileService.kt @@ -17,13 +17,16 @@ package com.example.wear.tiles.messaging import android.graphics.Bitmap import androidx.lifecycle.lifecycleScope -import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders.Timeline import androidx.wear.tiles.RequestBuilders.ResourcesRequest import androidx.wear.tiles.RequestBuilders.TileRequest import androidx.wear.tiles.TileBuilders.Tile import coil.Coil import coil.ImageLoader +import com.example.wear.tiles.R import com.google.android.horologist.tiles.SuspendingTileService +import java.util.UUID import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -46,7 +49,6 @@ import kotlinx.coroutines.flow.stateIn class MessagingTileService : SuspendingTileService() { private lateinit var repo: MessagingRepo private lateinit var imageLoader: ImageLoader - private lateinit var renderer: MessagingTileRenderer private lateinit var tileStateFlow: StateFlow override fun onCreate() { @@ -54,7 +56,6 @@ class MessagingTileService : SuspendingTileService() { repo = MessagingRepo(this) imageLoader = Coil.imageLoader(this) - renderer = MessagingTileRenderer(this) tileStateFlow = repo.getFavoriteContacts() .map { contacts -> MessagingTileState(contacts) } @@ -66,12 +67,14 @@ class MessagingTileService : SuspendingTileService() { } /** - * Read the latest data, delegating to the [MessagingTileRenderer] which creates a layout to - * which it binds the state. + * Read the latest data, and create a layout to which it binds the state. */ override suspend fun tileRequest(requestParams: TileRequest): Tile { - val tileState: MessagingTileState = latestTileState() - return renderer.renderTimeline(tileState, requestParams) + val layoutElement = messagingTileLayout(latestTileState(), this, requestParams.deviceConfiguration) + val resourcesVersion = if (DEBUG_RESOURCES) UUID.randomUUID().toString() else "0" + return Tile.Builder().setResourcesVersion(resourcesVersion).setTileTimeline( + Timeline.fromLayoutElement(layoutElement) + ).build() } /** @@ -102,14 +105,36 @@ class MessagingTileService : SuspendingTileService() { } /** - * Downloads bitmaps from the network and passes them to [MessagingTileRenderer] to add as - * image resources (alongside any local resources). + * Downloads bitmaps from the network and adds them to the Resources object + * (alongside any local resources). */ override suspend fun resourcesRequest( requestParams: ResourcesRequest - ): ResourceBuilders.Resources { + ): Resources { val avatars = fetchAvatarsFromNetwork(requestParams) - return renderer.produceRequestedResources(avatars, requestParams) + val resourceState = avatars + val resourceIds = requestParams.resourceIds + return Resources.Builder() + .setVersion(requestParams.version) + .apply { + if (resourceIds.isEmpty() || resourceIds.contains(ID_IC_SEARCH)) { + addIdToImageMapping( + ID_IC_SEARCH, + R.drawable.ic_search_24 + ) + } + + // We already checked `resourceIds` in `MessagingTileService` because we needed to know + // which avatars needed to be fetched from the network; `resourceResults` was already + // filtered so it only contains bitmaps for the requested resources. + resourceState.forEach { (contact, bitmap) -> + addIdToImageMapping( + "$ID_CONTACT_PREFIX${contact.id}", + bitmap + ) + } + } + .build() } /** @@ -124,7 +149,7 @@ class MessagingTileService : SuspendingTileService() { tileState.contacts } else { tileState.contacts.filter { - requestParams.resourceIds.contains(MessagingTileRenderer.ID_CONTACT_PREFIX + it.id) + requestParams.resourceIds.contains(MessagingTileService.ID_CONTACT_PREFIX + it.id) } } @@ -139,4 +164,12 @@ class MessagingTileService : SuspendingTileService() { return images } + + companion object { + internal const val ID_IC_SEARCH = "ic_search" + internal const val ID_CONTACT_PREFIX = "contact:" + } + } + +const val DEBUG_RESOURCES = true