diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index c2740b2dc..c7209c3a7 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -35,6 +35,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt
new file mode 100644
index 000000000..7ef43dc6f
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2025 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.snippets.m3.tile
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.core.app.TaskStackBuilder
+import androidx.core.net.toUri
+import androidx.wear.protolayout.ActionBuilders
+import androidx.wear.protolayout.ActionBuilders.launchAction
+import androidx.wear.protolayout.LayoutElementBuilders
+import androidx.wear.protolayout.ResourceBuilders.Resources
+import androidx.wear.protolayout.TimelineBuilders.Timeline
+import androidx.wear.protolayout.expression.dynamicDataMapOf
+import androidx.wear.protolayout.expression.intAppDataKey
+import androidx.wear.protolayout.expression.mapTo
+import androidx.wear.protolayout.expression.stringAppDataKey
+import androidx.wear.protolayout.material3.MaterialScope
+import androidx.wear.protolayout.material3.Typography.BODY_LARGE
+import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
+import androidx.wear.protolayout.material3.text
+import androidx.wear.protolayout.material3.textButton
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.loadAction
+import androidx.wear.protolayout.types.layoutString
+import androidx.wear.tiles.RequestBuilders
+import androidx.wear.tiles.RequestBuilders.ResourcesRequest
+import androidx.wear.tiles.TileBuilders.Tile
+import androidx.wear.tiles.TileService
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.Locale
+import kotlin.random.Random
+
+private const val RESOURCES_VERSION = "1"
+
+abstract class BaseTileService : TileService() {
+
+ override fun onTileRequest(
+ requestParams: RequestBuilders.TileRequest
+ ): ListenableFuture =
+ Futures.immediateFuture(
+ Tile.Builder()
+ .setResourcesVersion(RESOURCES_VERSION)
+ .setTileTimeline(
+ Timeline.fromLayoutElement(
+ materialScope(this, requestParams.deviceConfiguration) {
+ tileLayout(requestParams)
+ }
+ )
+ )
+ .build()
+ )
+
+ override fun onTileResourcesRequest(
+ requestParams: ResourcesRequest
+ ): ListenableFuture =
+ Futures.immediateFuture(
+ Resources.Builder().setVersion(requestParams.version).build()
+ )
+
+ abstract fun MaterialScope.tileLayout(
+ requestParams: RequestBuilders.TileRequest
+ ): LayoutElementBuilders.LayoutElement
+}
+
+class HelloTileService : BaseTileService() {
+ override fun MaterialScope.tileLayout(
+ requestParams: RequestBuilders.TileRequest
+ ) = primaryLayout(mainSlot = { text("Hello, World!".layoutString) })
+}
+
+class InteractionRefresh : BaseTileService() {
+ override fun MaterialScope.tileLayout(
+ requestParams: RequestBuilders.TileRequest
+ ) =
+ primaryLayout(
+ // Output a debug code so we can see the layout changing
+ titleSlot = {
+ text(
+ String.format(
+ Locale.ENGLISH,
+ "Debug %06d",
+ Random.nextInt(0, 1_000_000),
+ )
+ .layoutString
+ )
+ },
+ mainSlot = {
+ // [START android_wear_m3_interaction_refresh]
+ textButton(
+ onClick = clickable(loadAction()),
+ labelContent = { text("Refresh".layoutString) },
+ )
+ // [END android_wear_m3_interaction_refresh]
+ },
+ )
+}
+
+class InteractionDeepLink : TileService() {
+
+ // [START android_wear_m3_interaction_deeplink_tile]
+ override fun onTileRequest(
+ requestParams: RequestBuilders.TileRequest
+ ): ListenableFuture {
+ val lastClickableId = requestParams.currentState.lastClickableId
+ if (lastClickableId == "foo") {
+ TaskStackBuilder.create(this)
+ .addNextIntentWithParentStack(
+ Intent(
+ Intent.ACTION_VIEW,
+ "googleandroidsnippets://app/message_detail/1".toUri(),
+ this,
+ TileActivity::class.java,
+ )
+ )
+ .startActivities()
+ }
+ // ... User didn't tap a button (either first load or tapped somewhere else)
+ // [START_EXCLUDE]
+ return Futures.immediateFuture(
+ Tile.Builder()
+ .setResourcesVersion(RESOURCES_VERSION)
+ .setTileTimeline(
+ Timeline.fromLayoutElement(
+ materialScope(this, requestParams.deviceConfiguration) {
+ tileLayout(requestParams)
+ }
+ )
+ )
+ .build()
+ )
+ // [END_EXCLUDE]
+ }
+
+ // [END android_wear_m3_interaction_deeplink_tile]
+
+ override fun onTileResourcesRequest(
+ requestParams: ResourcesRequest
+ ): ListenableFuture =
+ Futures.immediateFuture(
+ Resources.Builder().setVersion(requestParams.version).build()
+ )
+
+ fun MaterialScope.tileLayout(requestParams: RequestBuilders.TileRequest) =
+ primaryLayout(
+ mainSlot = {
+ // [START android_wear_m3_interaction_deeplink_layout]
+ textButton(
+ labelContent = {
+ text("Deep Link me!".layoutString, typography = BODY_LARGE)
+ },
+ onClick = clickable(id = "foo", action = loadAction()),
+ )
+ // [END android_wear_m3_interaction_deeplink_layout]
+ }
+ )
+}
+
+class InteractionLoadAction : BaseTileService() {
+
+ override fun onTileRequest(
+ requestParams: RequestBuilders.TileRequest
+ ): ListenableFuture {
+
+ val name: String?
+ val age: Int?
+
+ // When triggered by loadAction(), "name" will be "Javier", and "age" will
+ // be 37.
+ with(requestParams.currentState.stateMap) {
+ name = this[stringAppDataKey("name")]
+ age = this[intAppDataKey("age")]
+ }
+
+ return Futures.immediateFuture(
+ Tile.Builder()
+ .setResourcesVersion(RESOURCES_VERSION)
+ .setTileTimeline(
+ Timeline.fromLayoutElement(
+ materialScope(this, requestParams.deviceConfiguration) {
+ tileLayout(requestParams)
+ }
+ )
+ )
+ .build()
+ )
+ }
+
+ override fun MaterialScope.tileLayout(
+ requestParams: RequestBuilders.TileRequest
+ ) =
+ primaryLayout(
+ // Output a debug code so we can verify that the reload happens
+ titleSlot = {
+ text(
+ String.format(
+ Locale.ENGLISH,
+ "Debug %06d",
+ Random.nextInt(0, 1_000_000),
+ )
+ .layoutString
+ )
+ },
+ mainSlot = {
+ // [START android_wear_m3_interaction_loadaction_layout]
+ textButton(
+ labelContent = {
+ text("loadAction()".layoutString, typography = BODY_LARGE)
+ },
+ onClick =
+ clickable(
+ action =
+ loadAction(
+ dynamicDataMapOf(
+ stringAppDataKey("name") mapTo "Javier",
+ intAppDataKey("age") mapTo 37,
+ )
+ )
+ ),
+ )
+ // [END android_wear_m3_interaction_loadaction_layout]
+ },
+ )
+}
+
+class InteractionLaunchAction : BaseTileService() {
+
+ override fun MaterialScope.tileLayout(
+ requestParams: RequestBuilders.TileRequest
+ ) =
+ primaryLayout(
+ mainSlot = {
+ // [START android_wear_m3_interactions_launchaction]
+ textButton(
+ labelContent = {
+ text("launchAction()".layoutString, typography = BODY_LARGE)
+ },
+ onClick =
+ clickable(
+ action =
+ launchAction(
+ ComponentName(
+ "com.example.wear",
+ "com.example.wear.snippets.m3.tile.TileActivity",
+ ),
+ mapOf(
+ "name" to ActionBuilders.stringExtra("Bartholomew"),
+ "age" to ActionBuilders.intExtra(21),
+ ),
+ )
+ ),
+ )
+ // [END android_wear_m3_interactions_launchaction]
+ }
+ )
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/m3/tile/TileActivity.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/TileActivity.kt
new file mode 100644
index 000000000..314898c39
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/TileActivity.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2021 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.snippets.m3.tile
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.navigation.navDeepLink
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.navigation.SwipeDismissableNavHost
+import androidx.wear.compose.navigation.composable
+import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
+import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
+import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
+import com.example.wear.R
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+
+class TileActivity : ComponentActivity() {
+ // [START android_wear_m3_interactions_launchaction_activity]
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // When this activity is launched from the tile InteractionLaunchAction,
+ // "name" will be "Bartholomew" and "age" will be 21
+ val name = intent.getStringExtra("name")
+ val age = intent.getStringExtra("age")
+
+ // [START_EXCLUDE]
+ setContent { MainContent() }
+ // [END_EXCLUDE]
+ }
+}
+
+// [END android_wear_m3_interactions_launchaction_activity]
+
+@Composable
+fun MainContent() {
+ // [START android_wear_m3_interaction_deeplink_activity]
+ AppScaffold {
+ val navController = rememberSwipeDismissableNavController()
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = "message_list",
+ ) {
+ // [START_EXCLUDE]
+ composable(
+ route = "message_list",
+ deepLinks =
+ listOf(
+ navDeepLink {
+ uriPattern = "googleandroidsnippets://app/message_list"
+ }
+ ),
+ ) {
+ MessageList(
+ onMessageClick = { id ->
+ navController.navigate("message_detail/$id")
+ }
+ )
+ }
+ // [END_EXCLUDE]
+ composable(
+ route = "message_detail/{id}",
+ deepLinks =
+ listOf(
+ navDeepLink {
+ uriPattern = "googleandroidsnippets://app/message_detail/{id}"
+ }
+ ),
+ ) {
+ val id = it.arguments?.getString("id") ?: "0"
+ MessageDetails(details = "message $id")
+ }
+ }
+ }
+ // [END android_wear_m3_interaction_deeplink_activity]
+}
+
+// Implementation of one of the screens in the navigation
+@Composable
+fun MessageDetails(details: String) {
+ val scrollState = rememberTransformingLazyColumnState()
+
+ val padding = rememberResponsiveColumnPadding(first = ColumnItemType.BodyText)
+
+ ScreenScaffold(scrollState = scrollState, contentPadding = padding) {
+ scaffoldPaddingValues ->
+ TransformingLazyColumn(
+ state = scrollState,
+ contentPadding = scaffoldPaddingValues,
+ ) {
+ item {
+ ListHeader() { Text(text = stringResource(R.string.message_detail)) }
+ }
+ item {
+ Text(
+ text = details,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun MessageList(onMessageClick: (String) -> Unit) {
+ val scrollState = rememberTransformingLazyColumnState()
+
+ val padding =
+ rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+
+ ScreenScaffold(scrollState = scrollState, contentPadding = padding) {
+ contentPadding ->
+ TransformingLazyColumn(
+ state = scrollState,
+ contentPadding = contentPadding,
+ ) {
+ item {
+ ListHeader() { Text(text = stringResource(R.string.message_list)) }
+ }
+ item {
+ Button(
+ onClick = { onMessageClick("message1") },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(text = "Message 1")
+ }
+ }
+ item {
+ Button(
+ onClick = { onMessageClick("message2") },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(text = "Message 2")
+ }
+ }
+ }
+ }
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+fun MessageDetailPreview() {
+ MessageDetails("message 7")
+}
+
+@WearPreviewDevices
+@WearPreviewFontScales
+@Composable
+fun MessageListPreview() {
+ MessageList(onMessageClick = {})
+}
diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml
index fc59c67b8..90f5cb258 100644
--- a/wear/src/main/res/values/strings.xml
+++ b/wear/src/main/res/values/strings.xml
@@ -3,6 +3,7 @@
Voice Input
Voice Text Entry
Message List
+ Message Detail
Hello Tile
Hello Tile Description
\ No newline at end of file