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