From 03e2b270974a3b7ee37976e793fe6b6d60444edd Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Tue, 29 Apr 2025 09:45:54 +0100 Subject: [PATCH 001/120] Refactor swipe-to-dismiss examples (#486) * Refactor swipe-to-dismiss examples - Update TodoItem data class for better readability - Refactor the swipe item to improve the overall performance and readability - Refactor SwipeItemExample for better readability - Refactor SwipeCardItemExample and rename it to SwipeItemWithAnimationExample for better description - Modify the swipe to dismiss animation for better user experience - Add comments to make code easy to read * Apply Spotless --------- Co-authored-by: JolandaVerhoef <6952116+JolandaVerhoef@users.noreply.github.com> --- .../snippets/components/SwipeToDismissBox.kt | 280 +++++++----------- 1 file changed, 104 insertions(+), 176 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt index 33911221b..22dc99303 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt @@ -16,14 +16,12 @@ package com.example.compose.snippets.components -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -34,15 +32,17 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.OutlinedCard import androidx.compose.material3.SwipeToDismissBox -import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart +import androidx.compose.material3.SwipeToDismissBoxValue.Settled +import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd import androidx.compose.material3.Text import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.lerp @@ -63,42 +63,31 @@ fun SwipeToDismissBoxExamples() { Text("Swipe to dismiss with change of background", fontWeight = FontWeight.Bold) SwipeItemExample() Text("Swipe to dismiss with a cross-fade animation", fontWeight = FontWeight.Bold) - SwipeCardItemExample() + SwipeItemWithAnimationExample() } } // [START android_compose_components_todoitem] data class TodoItem( - var isItemDone: Boolean, - var itemDescription: String + val itemDescription: String, + var isItemDone: Boolean = false ) // [END android_compose_components_todoitem] // [START android_compose_components_swipeitem] @Composable -fun SwipeItem( +fun TodoListItem( todoItem: TodoItem, - startToEndAction: (TodoItem) -> Unit, - endToStartAction: (TodoItem) -> Unit, + onToggleDone: (TodoItem) -> Unit, + onRemove: (TodoItem) -> Unit, modifier: Modifier = Modifier, - content: @Composable (TodoItem) -> Unit ) { val swipeToDismissBoxState = rememberSwipeToDismissBoxState( confirmValueChange = { - when (it) { - SwipeToDismissBoxValue.StartToEnd -> { - startToEndAction(todoItem) - // Do not dismiss this item. - false - } - SwipeToDismissBoxValue.EndToStart -> { - endToStartAction(todoItem) - true - } - SwipeToDismissBoxValue.Settled -> { - false - } - } + if (it == StartToEnd) onToggleDone(todoItem) + else if (it == EndToStart) onRemove(todoItem) + // Reset item when toggling done status + it != StartToEnd } ) @@ -106,59 +95,39 @@ fun SwipeItem( state = swipeToDismissBoxState, modifier = modifier.fillMaxSize(), backgroundContent = { - Row( - modifier = Modifier - .background( - when (swipeToDismissBoxState.dismissDirection) { - SwipeToDismissBoxValue.StartToEnd -> { - Color.Blue - } - SwipeToDismissBoxValue.EndToStart -> { - Color.Red - } - SwipeToDismissBoxValue.Settled -> { - Color.LightGray - } - } + when (swipeToDismissBoxState.dismissDirection) { + StartToEnd -> { + Icon( + if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, + contentDescription = if (todoItem.isItemDone) "Done" else "Not done", + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .wrapContentSize(Alignment.CenterStart) + .padding(12.dp), + tint = Color.White ) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - when (swipeToDismissBoxState.dismissDirection) { - SwipeToDismissBoxValue.StartToEnd -> { - val icon = if (todoItem.isItemDone) { - Icons.Default.CheckBox - } else { - Icons.Default.CheckBoxOutlineBlank - } - - val contentDescription = if (todoItem.isItemDone) "Done" else "Not done" - - Icon( - icon, - contentDescription, - Modifier.padding(12.dp), - tint = Color.White - ) - } - - SwipeToDismissBoxValue.EndToStart -> { - Spacer(modifier = Modifier) - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Remove item", - tint = Color.White, - modifier = Modifier.padding(12.dp) - ) - } - - SwipeToDismissBoxValue.Settled -> {} } + EndToStart -> { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + modifier = Modifier + .fillMaxSize() + .background(Color.Red) + .wrapContentSize(Alignment.CenterEnd) + .padding(12.dp), + tint = Color.White + ) + } + Settled -> {} } } ) { - content(todoItem) + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) } } // [END android_compose_components_swipeitem] @@ -169,10 +138,8 @@ fun SwipeItem( private fun SwipeItemExample() { val todoItems = remember { mutableStateListOf( - TodoItem(isItemDone = false, itemDescription = "Pay bills"), - TodoItem(isItemDone = false, itemDescription = "Buy groceries"), - TodoItem(isItemDone = false, itemDescription = "Go to gym"), - TodoItem(isItemDone = false, itemDescription = "Get dinner") + TodoItem("Pay bills"), TodoItem("Buy groceries"), + TodoItem("Go to gym"), TodoItem("Get dinner") ) } @@ -181,20 +148,16 @@ private fun SwipeItemExample() { items = todoItems, key = { it.itemDescription } ) { todoItem -> - SwipeItem( + TodoListItem( todoItem = todoItem, - startToEndAction = { + onToggleDone = { todoItem -> todoItem.isItemDone = !todoItem.isItemDone }, - endToStartAction = { + onRemove = { todoItem -> todoItems -= todoItem - } - ) { - ListItem( - headlineContent = { Text(text = todoItem.itemDescription) }, - supportingContent = { Text(text = "swipe me to update or remove.") } - ) - } + }, + modifier = Modifier.animateItem() + ) } } } @@ -202,103 +165,74 @@ private fun SwipeItemExample() { // [START android_compose_components_swipecarditem] @Composable -fun SwipeCardItem( +fun TodoListItemWithAnimation( todoItem: TodoItem, - startToEndAction: (TodoItem) -> Unit, - endToStartAction: (TodoItem) -> Unit, + onToggleDone: (TodoItem) -> Unit, + onRemove: (TodoItem) -> Unit, modifier: Modifier = Modifier, - content: @Composable (TodoItem) -> Unit ) { - val swipeToDismissState = rememberSwipeToDismissBoxState( - positionalThreshold = { totalDistance -> totalDistance * 0.25f }, - // [START_EXCLUDE] + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( confirmValueChange = { - when (it) { - SwipeToDismissBoxValue.StartToEnd -> { - startToEndAction(todoItem) - // Do not dismiss this item. - false - } - SwipeToDismissBoxValue.EndToStart -> { - endToStartAction(todoItem) - true - } - SwipeToDismissBoxValue.Settled -> { - false - } - } + if (it == StartToEnd) onToggleDone(todoItem) + else if (it == EndToStart) onRemove(todoItem) + // Reset item when toggling done status + it != StartToEnd } ) - // [END_EXCLUDE] SwipeToDismissBox( - modifier = Modifier, - state = swipeToDismissState, + state = swipeToDismissBoxState, + modifier = modifier.fillMaxSize(), backgroundContent = { - // Cross-fade the background color as the drag gesture progresses. - val color by animateColorAsState( - when (swipeToDismissState.targetValue) { - SwipeToDismissBoxValue.Settled -> Color.LightGray - SwipeToDismissBoxValue.StartToEnd -> - lerp(Color.LightGray, Color.Blue, swipeToDismissState.progress) - - SwipeToDismissBoxValue.EndToStart -> - lerp(Color.LightGray, Color.Red, swipeToDismissState.progress) - }, - label = "swipeable card item background color" - ) - // [START_EXCLUDE] - Row( - modifier = Modifier - .background(color) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - when (swipeToDismissState.dismissDirection) { - SwipeToDismissBoxValue.StartToEnd -> { - val icon = if (todoItem.isItemDone) { - Icons.Default.CheckBox - } else { - Icons.Default.CheckBoxOutlineBlank - } - - val contentDescription = if (todoItem.isItemDone) "Done" else "Not done" - - Icon(icon, contentDescription, Modifier.padding(12.dp), tint = Color.White) - } - - SwipeToDismissBoxValue.EndToStart -> { - Spacer(modifier = Modifier) - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Remove item", - tint = Color.White, - modifier = Modifier.padding(12.dp) - ) - } - - SwipeToDismissBoxValue.Settled -> {} + when (swipeToDismissBoxState.dismissDirection) { + StartToEnd -> { + Icon( + if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, + contentDescription = if (todoItem.isItemDone) "Done" else "Not done", + modifier = Modifier + .fillMaxSize() + .drawBehind { + drawRect(lerp(Color.LightGray, Color.Blue, swipeToDismissBoxState.progress)) + } + .wrapContentSize(Alignment.CenterStart) + .padding(12.dp), + tint = Color.White + ) } + EndToStart -> { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + modifier = Modifier + .fillMaxSize() + .background(lerp(Color.LightGray, Color.Red, swipeToDismissBoxState.progress)) + .wrapContentSize(Alignment.CenterEnd) + .padding(12.dp), + tint = Color.White + ) + } + Settled -> {} } } ) { - content(todoItem) + OutlinedCard(shape = RectangleShape) { + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) + } } - // [END_EXCLUDE] } // [END android_compose_components_swipecarditem] -// [START android_compose_components_swipecarditemexample] @Preview +// [START android_compose_components_swipecarditemexample] @Composable -private fun SwipeCardItemExample() { +private fun SwipeItemWithAnimationExample() { val todoItems = remember { mutableStateListOf( - TodoItem(isItemDone = false, itemDescription = "Pay bills"), - TodoItem(isItemDone = false, itemDescription = "Buy groceries"), - TodoItem(isItemDone = false, itemDescription = "Go to gym"), - TodoItem(isItemDone = false, itemDescription = "Get dinner") + TodoItem("Pay bills"), TodoItem("Buy groceries"), + TodoItem("Go to gym"), TodoItem("Get dinner") ) } @@ -307,22 +241,16 @@ private fun SwipeCardItemExample() { items = todoItems, key = { it.itemDescription } ) { todoItem -> - SwipeCardItem( + TodoListItemWithAnimation( todoItem = todoItem, - startToEndAction = { + onToggleDone = { todoItem -> todoItem.isItemDone = !todoItem.isItemDone }, - endToStartAction = { + onRemove = { todoItem -> todoItems -= todoItem - } - ) { - OutlinedCard(shape = RectangleShape) { - ListItem( - headlineContent = { Text(todoItem.itemDescription) }, - supportingContent = { Text("swipe me to update or remove.") } - ) - } - } + }, + modifier = Modifier.animateItem() + ) } } } From da3ebb8bc1d8782ad17c287f2a66aa1010266f60 Mon Sep 17 00:00:00 2001 From: Simona <35065668+simona-anomis@users.noreply.github.com> Date: Thu, 1 May 2025 09:34:51 +0100 Subject: [PATCH 002/120] Update accessibility snippets (#508) * Add new and updated accessibility DAC snippets * Import hideFromAccessibility * Apply Spotless * Revert removal of some existing a11y snippets --- compose/snippets/build.gradle.kts | 1 + .../accessibility/AccessibilitySnippets.kt | 94 ++++- .../snippets/semantics/SemanticsSnippets.kt | 1 - .../accessibility/AccessibilitySnippets.kt | 391 +++++++++++++++++- gradle/libs.versions.toml | 6 +- 5 files changed, 484 insertions(+), 9 deletions(-) diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts index 8b644b694..c8d728da0 100644 --- a/compose/snippets/build.gradle.kts +++ b/compose/snippets/build.gradle.kts @@ -160,6 +160,7 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.compose.ui.test.junit4.accessibility) debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt index 724876a87..ee6b51325 100644 --- a/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt @@ -16,25 +16,109 @@ package com.example.compose.snippets.accessibility +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert +import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks import androidx.compose.ui.test.junit4.createAndroidComposeRule -import com.example.compose.snippets.MyActivity +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.compose.ui.unit.dp +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator import org.junit.Ignore import org.junit.Rule import org.junit.Test -class AccessibilitySnippetsTest { +class AccessibilityTest { + +// [START android_compose_accessibility_testing_label] @Rule @JvmField - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule() + + @Test + fun noAccessibilityLabel() { + composeTestRule.setContent { + Box( + modifier = Modifier + .size(50.dp, 50.dp) + .background(color = Color.Gray) + .clickable { } + .semantics { + contentDescription = "" + } + ) + } - private val nodeMatcher = SemanticsMatcher("DUMMY") { it.isRoot } + composeTestRule.enableAccessibilityChecks() - @Ignore("Dummy test") + // Any action (such as performClick) will perform accessibility checks too: + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_label] +// [START android_compose_accessibility_testing_click] + @Test + fun smallClickTarget() { + composeTestRule.setContent { + Box( + modifier = Modifier + .size(20.dp, 20.dp) + .background(color = Color(0xFFFAFBFC)) + .clickable { } + ) + } + + composeTestRule.enableAccessibilityChecks() + + // Any action (such as performClick) will perform accessibility checks too: + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_click] + +// [START android_compose_accessibility_testing_validator] + @Test + fun lowContrastScreen() { + composeTestRule.setContent { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0xFFFAFBFC)), + contentAlignment = Alignment.Center + ) { + Text(text = "Hello", color = Color(0xFFB0B1B2)) + } + } + + // Optionally, set AccessibilityValidator manually + val accessibilityValidator = AccessibilityValidator() + .setThrowExceptionFor( + AccessibilityCheckResult.AccessibilityCheckResultType.WARNING + ) + + composeTestRule.enableAccessibilityChecks(accessibilityValidator) + + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_validator] + + private val nodeMatcher = SemanticsMatcher(description = "DUMMY") { it.isRoot } + + @Ignore("Dummy test") // [START android_compose_accessibility_testing] @Test fun test() { diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt index 89b577ea7..1b867edfa 100644 --- a/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt @@ -16,7 +16,6 @@ package com.example.compose.snippets.semantics -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsProperties diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt index 3c6be8afc..a83111f71 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt @@ -19,9 +19,11 @@ package com.example.compose.snippets.accessibility import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -34,6 +36,7 @@ import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Share import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Checkbox @@ -44,10 +47,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -58,16 +64,29 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.compose.snippets.R @@ -155,7 +174,7 @@ private fun LargeBox() { // [START android_compose_accessibility_click_label] @Composable -private fun ArticleListItem(openArticle: () -> Unit) { +private fun ArticleListItem(openArticle: () -> Unit = {}) { Row( Modifier.clickable( // R.string.action_read_article = "read article" @@ -418,6 +437,376 @@ fun FloatingBox() { } // [END android_compose_accessibility_traversal_fab] +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun InteractiveElements( + openArticle: () -> Unit = {}, + addToBookmarks: () -> Unit = {}, +) { +// [START android_compose_accessibility_interactive_clickable] + Row( + // Uses `mergeDescendants = true` under the hood + modifier = Modifier.clickable { openArticle() } + ) { + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Open", + ) + Text("Accessibility in Compose") + } +// [END android_compose_accessibility_interactive_clickable] + +// [START android_compose_accessibility_interactive_click_label] + Row( + modifier = Modifier + .clickable(onClickLabel = "Open this article") { + openArticle() + } + ) { + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Open" + ) + Text("Accessibility in Compose") + } +// [END android_compose_accessibility_interactive_click_label] + +// [START android_compose_accessibility_interactive_long_click] + Row( + modifier = Modifier + .combinedClickable( + onLongClickLabel = "Bookmark this article", + onLongClick = { addToBookmarks() }, + onClickLabel = "Open this article", + onClick = { openArticle() }, + ) + ) {} +// [END android_compose_accessibility_interactive_long_click] +} + +// [START android_compose_accessibility_interactive_nested_click] +@Composable +private fun ArticleList(openArticle: () -> Unit) { + NestedArticleListItem( + // Clickable is set separately, in a nested layer: + onClickAction = openArticle, + // Semantics are set here: + modifier = Modifier.semantics { + onClick( + label = "Open this article", + action = { + // Not needed here: openArticle() + true + } + ) + } + ) +} +// [END android_compose_accessibility_interactive_nested_click] + +@Composable +private fun NestedArticleListItem( + onClickAction: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun Semantics( + removeArticle: () -> Unit, + openArticle: () -> Unit, + addToBookmarks: () -> Unit, +) { + +// [START android_compose_accessibility_semantics_alert_polite] + PopupAlert( + message = "You have a new message", + modifier = Modifier.semantics { + liveRegion = LiveRegionMode.Polite + } + ) +// [END android_compose_accessibility_semantics_alert_polite] + +// [START android_compose_accessibility_semantics_alert_assertive] + PopupAlert( + message = "Emergency alert incoming", + modifier = Modifier.semantics { + liveRegion = LiveRegionMode.Assertive + } + ) +// [END android_compose_accessibility_semantics_alert_assertive] + + Box() { +// [START android_compose_accessibility_semantics_window] + ShareSheet( + message = "Choose how to share this photo", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter) + .semantics { paneTitle = "New bottom sheet" } + ) +// [END android_compose_accessibility_semantics_window] + } + +// [START android_compose_accessibility_semantics_error] + Error( + errorText = "Fields cannot be empty", + modifier = Modifier + .semantics { + error("Please add both email and password") + } + ) +// [END android_compose_accessibility_semantics_error] + + val progress by remember { mutableFloatStateOf(0F) } +// [START android_compose_accessibility_semantics_progress] + ProgressInfoBar( + modifier = Modifier + .semantics { + progressBarRangeInfo = + ProgressBarRangeInfo( + current = progress, + range = 0F..1F + ) + } + ) +// [END android_compose_accessibility_semantics_progress] + + val milkyWay = List(10) { it.toString() } +// [START android_compose_accessibility_semantics_long_list] + MilkyWayList( + modifier = Modifier + .semantics { + collectionInfo = CollectionInfo( + rowCount = milkyWay.count(), + columnCount = 1 + ) + } + ) { + milkyWay.forEachIndexed { index, text -> + Text( + text = text, + modifier = Modifier.semantics { + collectionItemInfo = + CollectionItemInfo(index, 0, 0, 0) + } + ) + } + } +// [END android_compose_accessibility_semantics_long_list] + +// [START android_compose_accessibility_semantics_custom_action_swipe] + SwipeToDismissBox( + modifier = Modifier.semantics { + // Represents the swipe to dismiss for accessibility + customActions = listOf( + CustomAccessibilityAction( + label = "Remove article from list", + action = { + removeArticle() + true + } + ) + ) + }, + state = rememberSwipeToDismissBoxState(), + backgroundContent = {} + ) { + ArticleListItem() + } +// [END android_compose_accessibility_semantics_custom_action_swipe] + +// [START android_compose_accessibility_semantics_custom_action_long_list] + ArticleListItemRow( + modifier = Modifier + .semantics { + customActions = listOf( + CustomAccessibilityAction( + label = "Open article", + action = { + openArticle() + true + } + ), + CustomAccessibilityAction( + label = "Add to bookmarks", + action = { + addToBookmarks() + true + } + ), + ) + } + ) { + Article( + modifier = Modifier.clearAndSetSemantics { }, + onClick = openArticle, + ) + BookmarkButton( + modifier = Modifier.clearAndSetSemantics { }, + onClick = addToBookmarks, + ) + } +// [END android_compose_accessibility_semantics_custom_action_long_list] +} + +@Composable +private fun PopupAlert( + message: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +fun ShareSheet( + message: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun Error( + errorText: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun ProgressInfoBar( + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun MilkyWayList( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + content() +} + +@Composable +private fun ArticleListItemRow( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + content() +} + +@Composable +fun Article( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +@Composable +fun BookmarkButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +// [START android_compose_accessibility_merging] +@Composable +private fun ArticleListItem( + openArticle: () -> Unit, + addToBookmarks: () -> Unit, +) { + + Row(modifier = Modifier.clickable { openArticle() }) { + // Merges with parent clickable: + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Article thumbnail" + ) + ArticleDetails() + + // Defies the merge due to its own clickable: + BookmarkButton(onClick = addToBookmarks) + } +} +// [END android_compose_accessibility_merging] + +@Composable +fun ArticleDetails( + modifier: Modifier = Modifier, +) { +} + +// [START android_compose_accessibility_clearing] +// Developer might intend this to be a toggleable. +// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied, +// a custom description is set, and a Role is applied. + +@Composable +fun FavoriteToggle() { + val checked = remember { mutableStateOf(true) } + Row( + modifier = Modifier + .toggleable( + value = checked.value, + onValueChange = { checked.value = it } + ) + .clearAndSetSemantics { + stateDescription = if (checked.value) "Favorited" else "Not favorited" + toggleableState = ToggleableState(checked.value) + role = Role.Switch + }, + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null // not needed here + + ) + Text("Favorite?") + } +} +// [END android_compose_accessibility_clearing] + +// [START android_compose_accessibility_hiding] +@Composable +fun WatermarkExample( + watermarkText: String, + content: @Composable () -> Unit, +) { + Box { + WatermarkedContent() + // Mark the watermark as hidden to accessibility services. + WatermarkText( + text = watermarkText, + color = Color.Gray.copy(alpha = 0.5f), + modifier = Modifier + .align(Alignment.BottomEnd) + .semantics { hideFromAccessibility() } + ) + } +} + +@Composable +fun DecorativeExample() { + Text( + modifier = + Modifier.semantics { + hideFromAccessibility() + }, + text = "A dot character that is used to decoratively separate information, like •" + ) +} +// [END android_compose_accessibility_hiding] + +@Composable +private fun WatermarkedContent() { +} + +@Composable +private fun WatermarkText( + text: String, + color: Color, + modifier: Modifier = Modifier, +) { +} + private object ColumnWithFab { // [START android_compose_accessibility_traversal_fab_scaffold] @OptIn(ExperimentalMaterial3Api::class) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 733b3cd7e..0a2589feb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" androidx-compose-bom = "2025.04.01" androidx-compose-ui-test = "1.7.0-alpha08" +androidx-compose-ui-test-junit4-accessibility = "1.8.0-rc02" androidx-constraintlayout = "2.2.1" androidx-constraintlayout-compose = "1.1.1" androidx-coordinator-layout = "1.3.0" @@ -97,8 +98,9 @@ androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-latest" } androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } -androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test" } -androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose-latest" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-latest" } +androidx-compose-ui-test-junit4-accessibility = { group = "androidx.compose.ui", name = "ui-test-junit4-accessibility", version.ref = "androidx-compose-ui-test-junit4-accessibility" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } From 432ff04642aa5da2b8da7304a79c93356d6bb3e4 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 8 May 2025 14:52:30 +0200 Subject: [PATCH 003/120] Migrate XR snippets to alpha04 (#509) * Migrate existing snippets to alpha04 * Add snippet for Session resume * Add SpatialExternalSurface snippet for xr-alpha04 and update SNAPSHOT build version. * Fix up Subspaces and Environments for alpha04 * Update SpatialExternalSurface example for xr-alpha04 * Add comments for SpatialExternalSurface in xr-alpha04 * Fix Anchor configuration snippet * Migrate to alpha04 (from snapshot build) --------- Co-authored-by: devbridie <442644+devbridie@users.noreply.github.com> Co-authored-by: Jan Kleinert --- gradle/libs.versions.toml | 10 ++- .../java/com/example/xr/arcore/Anchors.kt | 25 +++++- .../main/java/com/example/xr/arcore/Hands.kt | 80 +++++++------------ .../main/java/com/example/xr/arcore/Planes.kt | 24 +++++- .../xr/arcore/SessionLifecycleHelper.kt | 30 ------- .../xr/compose/SpatialExternalSurface.kt | 71 ++++++++++++++++ .../java/com/example/xr/compose/SpatialRow.kt | 2 +- .../java/com/example/xr/compose/Subspace.kt | 3 +- .../main/java/com/example/xr/compose/Views.kt | 10 +-- .../com/example/xr/misc/ModeTransition.kt | 5 +- .../java/com/example/xr/runtime/Session.kt | 61 ++++++++++++++ .../java/com/example/xr/scenecore/Entities.kt | 2 +- .../com/example/xr/scenecore/Environments.kt | 15 ++-- .../com/example/xr/scenecore/GltfEntity.kt | 6 +- .../com/example/xr/scenecore/SpatialAudio.kt | 20 ++--- .../xr/scenecore/SpatialCapabilities.kt | 10 +-- .../com/example/xr/scenecore/SpatialVideo.kt | 31 +++---- 17 files changed, 267 insertions(+), 138 deletions(-) delete mode 100644 xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt create mode 100644 xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt create mode 100644 xr/src/main/java/com/example/xr/runtime/Session.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a2589feb..3b540244c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,10 @@ okHttp = "4.12.0" playServicesWearable = "19.0.0" protolayout = "1.2.1" recyclerview = "1.4.0" +# @keep +androidx-xr-arcore = "1.0.0-alpha04" +androidx-xr-scenecore = "1.0.0-alpha04" +androidx-xr-compose = "1.0.0-alpha04" targetSdk = "34" tiles = "1.4.1" version-catalog-update = "1.0.0" @@ -149,9 +153,9 @@ androidx-window = { module = "androidx.window:window", version.ref = "androidx-w androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" } androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window-java" } androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.1" -androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr" } -androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr" } -androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr" } +androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr-arcore" } +androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr-compose" } +androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr-scenecore" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } diff --git a/xr/src/main/java/com/example/xr/arcore/Anchors.kt b/xr/src/main/java/com/example/xr/arcore/Anchors.kt index 963da2b43..cb4992095 100644 --- a/xr/src/main/java/com/example/xr/arcore/Anchors.kt +++ b/xr/src/main/java/com/example/xr/arcore/Anchors.kt @@ -19,10 +19,31 @@ package com.example.xr.arcore import androidx.xr.arcore.Anchor import androidx.xr.arcore.AnchorCreateSuccess import androidx.xr.arcore.Trackable +import androidx.xr.runtime.Config import androidx.xr.runtime.Session +import androidx.xr.runtime.SessionConfigureConfigurationNotSupported +import androidx.xr.runtime.SessionConfigurePermissionsNotGranted +import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.scenecore.AnchorEntity import androidx.xr.scenecore.Entity +import androidx.xr.scenecore.scene + +@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted +fun configureAnchoring(session: Session) { + // [START androidxr_arcore_anchoring_configure] + val newConfig = session.config.copy( + anchorPersistence = Config.AnchorPersistenceMode.Enabled, + ) + when (val result = session.configure(newConfig)) { + is SessionConfigureConfigurationNotSupported -> + TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) + is SessionConfigurePermissionsNotGranted -> + TODO(/* The required permissions in result.permissions have not been granted. */) + is SessionConfigureSuccess -> TODO(/* Success! */) + } + // [END androidxr_arcore_anchoring_configure] +} private fun createAnchorAtPose(session: Session, pose: Pose) { val pose = Pose() @@ -45,13 +66,13 @@ private fun createAnchorAtTrackable(trackable: Trackable<*>) { } private fun attachEntityToAnchor( - session: androidx.xr.scenecore.Session, + session: Session, entity: Entity, anchor: Anchor ) { // [START androidxr_arcore_entity_tracks_anchor] AnchorEntity.create(session, anchor).apply { - setParent(session.activitySpace) + setParent(session.scene.activitySpace) addChild(entity) } // [END androidxr_arcore_entity_tracks_anchor] diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt index 26cc0ba8e..af3a547b0 100644 --- a/xr/src/main/java/com/example/xr/arcore/Hands.kt +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -16,59 +16,39 @@ package com.example.xr.arcore -import android.annotation.SuppressLint -import android.os.Bundle import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope import androidx.xr.arcore.Hand -import androidx.xr.arcore.HandJointType -import androidx.xr.compose.platform.setSubspaceContent +import androidx.xr.runtime.Config +import androidx.xr.runtime.HandJointType import androidx.xr.runtime.Session +import androidx.xr.runtime.SessionConfigureConfigurationNotSupported +import androidx.xr.runtime.SessionConfigurePermissionsNotGranted +import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Vector3 -import androidx.xr.scenecore.Entity -import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.GltfModelEntity -import kotlinx.coroutines.guava.await +import androidx.xr.scenecore.scene import kotlinx.coroutines.launch -class SampleHandsActivity : ComponentActivity() { - lateinit var session: Session - lateinit var scenecoreSession: androidx.xr.scenecore.Session - lateinit var sessionHelper: SessionLifecycleHelper - - var palmEntity: Entity? = null - var indexFingerEntity: Entity? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setSubspaceContent { } - - scenecoreSession = androidx.xr.scenecore.Session.create(this@SampleHandsActivity) - lifecycleScope.launch { - val model = GltfModel.create(scenecoreSession, "models/saturn_rings.glb").await() - palmEntity = GltfModelEntity.create(scenecoreSession, model).apply { - setScale(0.3f) - setHidden(true) - } - indexFingerEntity = GltfModelEntity.create(scenecoreSession, model).apply { - setScale(0.2f) - setHidden(true) - } - } - - sessionHelper = SessionLifecycleHelper( - onCreateCallback = { session = it }, - onResumeCallback = { - collectHands(session) - } - ) - lifecycle.addObserver(sessionHelper) +@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted +fun ComponentActivity.configureSession(session: Session) { + // [START androidxr_arcore_hand_configure] + val newConfig = session.config.copy( + handTracking = Config.HandTrackingMode.Enabled + ) + when (val result = session.configure(newConfig)) { + is SessionConfigureConfigurationNotSupported -> + TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) + is SessionConfigurePermissionsNotGranted -> + TODO(/* The required permissions in result.permissions have not been granted. */) + is SessionConfigureSuccess -> TODO(/* Success! */) } + // [END androidxr_arcore_hand_configure] } -fun SampleHandsActivity.collectHands(session: Session) { +fun ComponentActivity.collectHands(session: Session) { lifecycleScope.launch { // [START androidxr_arcore_hand_collect] Hand.left(session)?.state?.collect { handState -> // or Hand.right(session) @@ -85,9 +65,9 @@ fun SampleHandsActivity.collectHands(session: Session) { } } -@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504 -fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { - val palmEntity = palmEntity ?: return +fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { + val session: Session = null!! + val palmEntity: GltfModelEntity = null!! // [START androidxr_arcore_hand_entityAtHandPalm] val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return @@ -96,18 +76,18 @@ fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { palmEntity.setHidden(angle > Math.toRadians(40.0)) val transformedPose = - scenecoreSession.perceptionSpace.transformPoseTo( + session.scene.perceptionSpace.transformPoseTo( palmPose, - scenecoreSession.activitySpace, + session.scene.activitySpace, ) val newPosition = transformedPose.translation + transformedPose.down * 0.05f palmEntity.setPose(Pose(newPosition, transformedPose.rotation)) // [END androidxr_arcore_hand_entityAtHandPalm] } -@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504 -fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { - val indexFingerEntity = indexFingerEntity ?: return +fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { + val session: Session = null!! + val indexFingerEntity: GltfModelEntity = null!! // [START androidxr_arcore_hand_entityAtIndexFingerTip] val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return @@ -117,9 +97,9 @@ fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { indexFingerEntity.setHidden(angle > Math.toRadians(40.0)) val transformedPose = - scenecoreSession.perceptionSpace.transformPoseTo( + session.scene.perceptionSpace.transformPoseTo( tipPose, - scenecoreSession.activitySpace, + session.scene.activitySpace, ) val position = transformedPose.translation + transformedPose.forward * 0.03f val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up) diff --git a/xr/src/main/java/com/example/xr/arcore/Planes.kt b/xr/src/main/java/com/example/xr/arcore/Planes.kt index 975523ae8..fd5e02c11 100644 --- a/xr/src/main/java/com/example/xr/arcore/Planes.kt +++ b/xr/src/main/java/com/example/xr/arcore/Planes.kt @@ -17,9 +17,30 @@ package com.example.xr.arcore import androidx.xr.arcore.Plane +import androidx.xr.runtime.Config import androidx.xr.runtime.Session +import androidx.xr.runtime.SessionConfigureConfigurationNotSupported +import androidx.xr.runtime.SessionConfigurePermissionsNotGranted +import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Ray +import androidx.xr.scenecore.scene + +@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted +fun configurePlaneTracking(session: Session) { + // [START androidxr_arcore_planetracking_configure] + val newConfig = session.config.copy( + planeTracking = Config.PlaneTrackingMode.HorizontalAndVertical, + ) + when (val result = session.configure(newConfig)) { + is SessionConfigureConfigurationNotSupported -> + TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) + is SessionConfigurePermissionsNotGranted -> + TODO(/* The required permissions in result.permissions have not been granted. */) + is SessionConfigureSuccess -> TODO(/* Success! */) + } + // [END androidxr_arcore_planetracking_configure] +} private suspend fun subscribePlanes(session: Session) { // [START androidxr_arcore_planes_subscribe] @@ -30,8 +51,7 @@ private suspend fun subscribePlanes(session: Session) { } private fun hitTestTable(session: Session) { - val scenecoreSession: androidx.xr.scenecore.Session = null!! - val pose = scenecoreSession.spatialUser.head?.transformPoseTo(Pose(), scenecoreSession.perceptionSpace) ?: return + val pose = session.scene.spatialUser.head?.transformPoseTo(Pose(), session.scene.perceptionSpace) ?: return val ray = Ray(pose.translation, pose.forward) // [START androidxr_arcore_hitTest] val results = androidx.xr.arcore.hitTest(session, ray) diff --git a/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt b/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt deleted file mode 100644 index 77462257f..000000000 --- a/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.xr.arcore - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.xr.runtime.Session - -/** - * This is a dummy version of [SessionLifecycleHelper](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/SessionLifecycleHelper.kt). - * This will be removed when Session becomes a LifecycleOwner in cl/726643897. - */ -class SessionLifecycleHelper( - val onCreateCallback: (Session) -> Unit, - - val onResumeCallback: (() -> Unit)? = null, -) : DefaultLifecycleObserver diff --git a/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt b/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt new file mode 100644 index 000000000..1736bc909 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt @@ -0,0 +1,71 @@ +/* + * 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.xr.compose + +import android.content.ContentResolver +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialExternalSurface +import androidx.xr.compose.subspace.StereoMode +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.height +import androidx.xr.compose.subspace.layout.width + +// [START androidxr_compose_SpatialExternalSurfaceStereo] +@Composable +fun SpatialExternalSurfaceContent() { + val context = LocalContext.current + Subspace { + SpatialExternalSurface( + modifier = SubspaceModifier + .width(1200.dp) // Default width is 400.dp if no width modifier is specified + .height(676.dp), // Default height is 400.dp if no height modifier is specified + // Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending + // upon which type of content you are rendering: monoscopic content, side-by-side stereo + // content, or top-bottom stereo content + stereoMode = StereoMode.SideBySide, + ) { + val exoPlayer = remember { ExoPlayer.Builder(context).build() } + val videoUri = Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + // Represents a side-by-side stereo video, where each frame contains a pair of + // video frames arranged side-by-side. The frame on the left represents the left + // eye view, and the frame on the right represents the right eye view. + .path("sbs_video.mp4") + .build() + val mediaItem = MediaItem.fromUri(videoUri) + + // onSurfaceCreated is invoked only one time, when the Surface is created + onSurfaceCreated { surface -> + exoPlayer.setVideoSurface(surface) + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.play() + } + // onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its + // associated Surface are destroyed + onSurfaceDestroyed { exoPlayer.release() } + } + } +} +// [END androidxr_compose_SpatialExternalSurfaceStereo] diff --git a/xr/src/main/java/com/example/xr/compose/SpatialRow.kt b/xr/src/main/java/com/example/xr/compose/SpatialRow.kt index 2c0ccb955..d138411ac 100644 --- a/xr/src/main/java/com/example/xr/compose/SpatialRow.kt +++ b/xr/src/main/java/com/example/xr/compose/SpatialRow.kt @@ -27,7 +27,7 @@ import androidx.xr.compose.subspace.layout.width @Composable private fun SpatialRowExample() { // [START androidxr_compose_SpatialRowExample] - SpatialRow(curveRadius = 825.dp) { + SpatialRow { SpatialPanel( SubspaceModifier .width(384.dp) diff --git a/xr/src/main/java/com/example/xr/compose/Subspace.kt b/xr/src/main/java/com/example/xr/compose/Subspace.kt index 75579968e..2cbbe1021 100644 --- a/xr/src/main/java/com/example/xr/compose/Subspace.kt +++ b/xr/src/main/java/com/example/xr/compose/Subspace.kt @@ -22,6 +22,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable +import androidx.xr.compose.spatial.ApplicationSubspace import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.SpatialPanel @@ -32,7 +33,7 @@ private class SubspaceActivity : ComponentActivity() { // [START androidxr_compose_SubspaceSetContent] setContent { // This is a top-level subspace - Subspace { + ApplicationSubspace { SpatialPanel { MyComposable() } diff --git a/xr/src/main/java/com/example/xr/compose/Views.kt b/xr/src/main/java/com/example/xr/compose/Views.kt index 20a8b4d74..3f91ef2ad 100644 --- a/xr/src/main/java/com/example/xr/compose/Views.kt +++ b/xr/src/main/java/com/example/xr/compose/Views.kt @@ -33,9 +33,9 @@ import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.depth import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.width -import androidx.xr.scenecore.Dimensions +import androidx.xr.runtime.Session import androidx.xr.scenecore.PanelEntity -import androidx.xr.scenecore.Session +import androidx.xr.scenecore.PixelDimensions import com.example.xr.R private class MyCustomView(context: Context) : View(context) @@ -46,9 +46,8 @@ private class ActivityWithSubspaceContent : ComponentActivity() { // [START androidxr_compose_ActivityWithSubspaceContent] setSubspaceContent { SpatialPanel( - view = MyCustomView(this), modifier = SubspaceModifier.height(500.dp).width(500.dp).depth(25.dp) - ) + ) { MyCustomView(this) } } // [END androidxr_compose_ActivityWithSubspaceContent] } @@ -84,8 +83,7 @@ fun ComponentActivity.PanelEntityWithView(xrSession: Session) { val panelEntity = PanelEntity.create( session = xrSession, view = panelContent, - surfaceDimensionsPx = Dimensions(500f, 500f), - dimensions = Dimensions(1f, 1f, 1f), + pixelDimensions = PixelDimensions(500, 500), name = "panel entity" ) // [END androidxr_compose_PanelEntityWithView] diff --git a/xr/src/main/java/com/example/xr/misc/ModeTransition.kt b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt index c34d549ae..dca0ddbfb 100644 --- a/xr/src/main/java/com/example/xr/misc/ModeTransition.kt +++ b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt @@ -18,7 +18,8 @@ package com.example.xr.misc import androidx.compose.runtime.Composable import androidx.xr.compose.platform.LocalSpatialConfiguration -import androidx.xr.scenecore.Session +import androidx.xr.runtime.Session +import androidx.xr.scenecore.scene @Composable fun modeTransitionCompose() { @@ -31,6 +32,6 @@ fun modeTransitionCompose() { fun modeTransitionScenecore(xrSession: Session) { // [START androidxr_misc_modeTransitionScenecore] - xrSession.spatialEnvironment.requestHomeSpaceMode() + xrSession.scene.spatialEnvironment.requestHomeSpaceMode() // [END androidxr_misc_modeTransitionScenecore] } diff --git a/xr/src/main/java/com/example/xr/runtime/Session.kt b/xr/src/main/java/com/example/xr/runtime/Session.kt new file mode 100644 index 000000000..f2fd85a2a --- /dev/null +++ b/xr/src/main/java/com/example/xr/runtime/Session.kt @@ -0,0 +1,61 @@ +/* + * 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.xr.runtime + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.xr.compose.platform.LocalSession +import androidx.xr.runtime.Session +import androidx.xr.runtime.SessionCreatePermissionsNotGranted +import androidx.xr.runtime.SessionCreateSuccess +import androidx.xr.runtime.SessionResumePermissionsNotGranted +import androidx.xr.runtime.SessionResumeSuccess + +// [START androidxr_localsession] +@Composable +fun ComposableUsingSession() { + val session = LocalSession.current +} +// [END androidxr_localsession] + +fun Activity.createSession() { + // [START androidxr_session_create] + when (val result = Session.create(this)) { + is SessionCreateSuccess -> { + val xrSession = result.session + // ... + } + is SessionCreatePermissionsNotGranted -> + TODO(/* The required permissions in result.permissions have not been granted. */) + } + // [END androidxr_session_create] +} + +fun sessionResume(session: Session) { + // [START androidxr_session_resume] + when (val result = session.resume()) { + is SessionResumeSuccess -> { + // Session has been created successfully. + // Attach any successful handlers here. + } + + is SessionResumePermissionsNotGranted -> { + // Request permissions in `result.permissions`. + } + } + // [END androidxr_session_resume] +} diff --git a/xr/src/main/java/com/example/xr/scenecore/Entities.kt b/xr/src/main/java/com/example/xr/scenecore/Entities.kt index f2bb48f6e..7c8046664 100644 --- a/xr/src/main/java/com/example/xr/scenecore/Entities.kt +++ b/xr/src/main/java/com/example/xr/scenecore/Entities.kt @@ -16,6 +16,7 @@ package com.example.xr.scenecore +import androidx.xr.runtime.Session import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Vector3 @@ -28,7 +29,6 @@ import androidx.xr.scenecore.MovableComponent import androidx.xr.scenecore.PlaneSemantic import androidx.xr.scenecore.PlaneType import androidx.xr.scenecore.ResizableComponent -import androidx.xr.scenecore.Session import java.util.concurrent.Executors private fun setPoseExample(entity: Entity) { diff --git a/xr/src/main/java/com/example/xr/scenecore/Environments.kt b/xr/src/main/java/com/example/xr/scenecore/Environments.kt index a366f9d12..35f753569 100644 --- a/xr/src/main/java/com/example/xr/scenecore/Environments.kt +++ b/xr/src/main/java/com/example/xr/scenecore/Environments.kt @@ -16,10 +16,11 @@ package com.example.xr.scenecore +import androidx.xr.runtime.Session import androidx.xr.scenecore.ExrImage import androidx.xr.scenecore.GltfModel -import androidx.xr.scenecore.Session import androidx.xr.scenecore.SpatialEnvironment +import androidx.xr.scenecore.scene import kotlinx.coroutines.guava.await private class Environments(val session: Session) { @@ -32,16 +33,16 @@ private class Environments(val session: Session) { fun loadEnvironmentSkybox() { // [START androidxr_scenecore_environment_loadEnvironmentSkybox] - val skybox = ExrImage.create(session, "BlueSkybox.exr") + val lightingForSkybox = ExrImage.create(session, "BlueSkyboxLighting.zip") // [END androidxr_scenecore_environment_loadEnvironmentSkybox] } - fun setEnvironmentPreference(environmentGeometry: GltfModel, skybox: ExrImage) { + fun setEnvironmentPreference(environmentGeometry: GltfModel, lightingForSkybox: ExrImage) { // [START androidxr_scenecore_environment_setEnvironmentPreference] val spatialEnvironmentPreference = - SpatialEnvironment.SpatialEnvironmentPreference(skybox, environmentGeometry) + SpatialEnvironment.SpatialEnvironmentPreference(lightingForSkybox, environmentGeometry) val preferenceResult = - session.spatialEnvironment.setSpatialEnvironmentPreference(spatialEnvironmentPreference) + session.scene.spatialEnvironment.setSpatialEnvironmentPreference(spatialEnvironmentPreference) if (preferenceResult == SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied()) { // The environment was successfully updated and is now visible, and any listeners // specified using addOnSpatialEnvironmentChangedListener will be notified. @@ -54,7 +55,7 @@ private class Environments(val session: Session) { fun setPassthroughOpacityPreference() { // [START androidxr_scenecore_environment_setPassthroughOpacityPreference] - val preferenceResult = session.spatialEnvironment.setPassthroughOpacityPreference(1.0f) + val preferenceResult = session.scene.spatialEnvironment.setPassthroughOpacityPreference(1.0f) if (preferenceResult == SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied()) { // The passthrough opacity request succeeded and should be visible now, and any listeners // specified using addOnPassthroughOpacityChangedListener will be notified @@ -71,7 +72,7 @@ private class Environments(val session: Session) { fun getCurrentPassthroughOpacity() { // [START androidxr_scenecore_environment_getCurrentPassthroughOpacity] - val currentPassthroughOpacity = session.spatialEnvironment.getCurrentPassthroughOpacity() + val currentPassthroughOpacity = session.scene.spatialEnvironment.getCurrentPassthroughOpacity() // [END androidxr_scenecore_environment_getCurrentPassthroughOpacity] } } diff --git a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt index 6be959836..586f53fb3 100644 --- a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt +++ b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt @@ -19,11 +19,11 @@ package com.example.xr.scenecore import android.content.Intent import android.net.Uri import androidx.activity.ComponentActivity +import androidx.xr.runtime.Session import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.GltfModelEntity -import androidx.xr.scenecore.Session import androidx.xr.scenecore.SpatialCapabilities -import androidx.xr.scenecore.getSpatialCapabilities +import androidx.xr.scenecore.scene import kotlinx.coroutines.guava.await private suspend fun loadGltfFile(session: Session) { @@ -34,7 +34,7 @@ private suspend fun loadGltfFile(session: Session) { private fun createModelEntity(session: Session, gltfModel: GltfModel) { // [START androidxr_scenecore_gltfmodelentity_create] - if (session.getSpatialCapabilities() + if (session.scene.spatialCapabilities .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT) ) { val gltfEntity = GltfModelEntity.create(session, gltfModel) diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt index 8bc9e99ee..1d1eac1ae 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt @@ -22,20 +22,20 @@ import android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION import android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION import android.media.MediaPlayer import android.media.SoundPool +import androidx.xr.runtime.Session import androidx.xr.scenecore.Entity -import androidx.xr.scenecore.PointSourceAttributes -import androidx.xr.scenecore.Session +import androidx.xr.scenecore.PointSourceParams import androidx.xr.scenecore.SoundFieldAttributes import androidx.xr.scenecore.SpatialCapabilities import androidx.xr.scenecore.SpatialMediaPlayer import androidx.xr.scenecore.SpatialSoundPool import androidx.xr.scenecore.SpatializerConstants -import androidx.xr.scenecore.getSpatialCapabilities +import androidx.xr.scenecore.scene private fun playSpatialAudioAtEntity(session: Session, appContext: Context, entity: Entity) { // [START androidxr_scenecore_playSpatialAudio] // Check spatial capabilities before using spatial audio - if (session.getSpatialCapabilities() + if (session.scene.spatialCapabilities .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO) ) { // The session has spatial audio capabilities val maxVolume = 1F @@ -52,7 +52,7 @@ private fun playSpatialAudioAtEntity(session: Session, appContext: Context, enti ) .build() - val pointSource = PointSourceAttributes(entity) + val pointSource = PointSourceParams(entity) val soundEffect = appContext.assets.openFd("sounds/tiger_16db.mp3") val pointSoundId = soundPool.load(soundEffect, lowPriority) @@ -64,7 +64,7 @@ private fun playSpatialAudioAtEntity(session: Session, appContext: Context, enti session = session, soundPool = soundPool, soundID = pointSoundId, - attributes = pointSource, + params = pointSource, volume = maxVolume, priority = lowPriority, loop = infiniteLoop, @@ -81,10 +81,10 @@ private fun playSpatialAudioAtEntity(session: Session, appContext: Context, enti private fun playSpatialAudioAtEntitySurround(session: Session, appContext: Context) { // [START androidxr_scenecore_playSpatialAudioSurround] // Check spatial capabilities before using spatial audio - if (session.getSpatialCapabilities().hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) { + if (session.scene.spatialCapabilities.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) { // The session has spatial audio capabilities - val pointSourceAttributes = PointSourceAttributes(session.mainPanelEntity) + val pointSourceAttributes = PointSourceParams(session.scene.mainPanelEntity) val mediaPlayer = MediaPlayer() @@ -98,7 +98,7 @@ private fun playSpatialAudioAtEntitySurround(session: Session, appContext: Conte .setUsage(AudioAttributes.USAGE_MEDIA) .build() - SpatialMediaPlayer.setPointSourceAttributes( + SpatialMediaPlayer.setPointSourceParams( session, mediaPlayer, pointSourceAttributes @@ -116,7 +116,7 @@ private fun playSpatialAudioAtEntitySurround(session: Session, appContext: Conte private fun playSpatialAudioAtEntityAmbionics(session: Session, appContext: Context) { // [START androidxr_scenecore_playSpatialAudioAmbionics] // Check spatial capabilities before using spatial audio - if (session.getSpatialCapabilities().hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) { + if (session.scene.spatialCapabilities.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) { // The session has spatial audio capabilities val soundFieldAttributes = diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt index 9d041eeac..fcfcdf5a8 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt @@ -16,21 +16,21 @@ package com.example.xr.scenecore -import androidx.xr.scenecore.Session +import androidx.xr.runtime.Session import androidx.xr.scenecore.SpatialCapabilities -import androidx.xr.scenecore.getSpatialCapabilities +import androidx.xr.scenecore.scene fun checkMultipleCapabilities(xrSession: Session) { // [START androidxr_compose_checkMultipleCapabilities] // Example 1: check if enabling passthrough mode is allowed - if (xrSession.getSpatialCapabilities().hasCapability( + if (xrSession.scene.spatialCapabilities.hasCapability( SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL ) ) { - xrSession.spatialEnvironment.setPassthroughOpacityPreference(0f) + xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(0f) } // Example 2: multiple capability flags can be checked simultaneously: - if (xrSession.getSpatialCapabilities().hasCapability( + if (xrSession.scene.spatialCapabilities.hasCapability( SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL and SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT ) diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index df4a20d23..272c21a8a 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -21,19 +21,20 @@ import android.net.Uri import androidx.activity.ComponentActivity import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer +import androidx.xr.runtime.Session import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Vector3 -import androidx.xr.scenecore.Session -import androidx.xr.scenecore.StereoSurfaceEntity +import androidx.xr.scenecore.SurfaceEntity +import androidx.xr.scenecore.scene private fun ComponentActivity.surfaceEntityCreate(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreate] - val stereoSurfaceEntity = StereoSurfaceEntity.create( + val stereoSurfaceEntity = SurfaceEntity.create( xrSession, - StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE, + SurfaceEntity.StereoMode.SIDE_BY_SIDE, // Position 1.5 meters in front of user Pose(Vector3(0.0f, 0.0f, -1.5f)), - StereoSurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) + SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) ) val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) @@ -53,14 +54,14 @@ private fun ComponentActivity.surfaceEntityCreateSbs(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreateSbs] // Set up the surface for playing a 180° video on a hemisphere. val hemisphereStereoSurfaceEntity = - StereoSurfaceEntity.create( + SurfaceEntity.create( xrSession, - StereoSurfaceEntity.StereoMode.SIDE_BY_SIDE, - xrSession.spatialUser.head?.transformPoseTo( + SurfaceEntity.StereoMode.SIDE_BY_SIDE, + xrSession.scene.spatialUser.head?.transformPoseTo( Pose.Identity, - xrSession.activitySpace + xrSession.scene.activitySpace )!!, - StereoSurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f), + SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f), ) // ... and use the surface for playing the media. // [END androidxr_scenecore_surfaceEntityCreateSbs] @@ -70,14 +71,14 @@ private fun ComponentActivity.surfaceEntityCreateTb(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreateTb] // Set up the surface for playing a 360° video on a sphere. val sphereStereoSurfaceEntity = - StereoSurfaceEntity.create( + SurfaceEntity.create( xrSession, - StereoSurfaceEntity.StereoMode.TOP_BOTTOM, - xrSession.spatialUser.head?.transformPoseTo( + SurfaceEntity.StereoMode.TOP_BOTTOM, + xrSession.scene.spatialUser.head?.transformPoseTo( Pose.Identity, - xrSession.activitySpace + xrSession.scene.activitySpace )!!, - StereoSurfaceEntity.CanvasShape.Vr360Sphere(1.0f), + SurfaceEntity.CanvasShape.Vr360Sphere(1.0f), ) // ... and use the surface for playing the media. // [END androidxr_scenecore_surfaceEntityCreateTb] From 222fd4f9422987a3ea721273cb118f9c88913821 Mon Sep 17 00:00:00 2001 From: compose-devrel-github-bot <118755852+compose-devrel-github-bot@users.noreply.github.com> Date: Thu, 8 May 2025 17:10:05 +0100 Subject: [PATCH 004/120] =?UTF-8?q?=F0=9F=A4=96=20Update=20Dependencies=20?= =?UTF-8?q?(#511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🤖 Update Dependencies * revert the window dep update * Apply Spotless * Revert the revert * Suppress requiresSdk warnings * Suppress requiresSdk warnings * Revert xr deps --------- Co-authored-by: Simona Milanovic Co-authored-by: simona-anomis <35065668+simona-anomis@users.noreply.github.com> --- gradle/libs.versions.toml | 32 +++++++++---------- .../ActivityEmbeddingJavaSnippets.java | 2 ++ .../ActivityEmbeddingKotlinSnippets.kt | 4 +++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b540244c..543a9545f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,12 @@ accompanist = "0.36.0" activityKtx = "1.10.1" android-googleid = "1.1.1" -androidGradlePlugin = "8.9.2" +androidGradlePlugin = "8.10.0" androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2025.04.01" +androidx-compose-bom = "2025.05.00" androidx-compose-ui-test = "1.7.0-alpha08" -androidx-compose-ui-test-junit4-accessibility = "1.8.0-rc02" +androidx-compose-ui-test-junit4-accessibility = "1.9.0-alpha02" androidx-constraintlayout = "2.2.1" androidx-constraintlayout-compose = "1.1.1" androidx-coordinator-layout = "1.3.0" @@ -17,17 +17,17 @@ androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" androidx-fragment-ktx = "1.8.6" androidx-glance-appwidget = "1.1.1" -androidx-lifecycle-compose = "2.8.7" -androidx-lifecycle-runtime-compose = "2.8.7" -androidx-navigation = "2.8.9" +androidx-lifecycle-compose = "2.9.0" +androidx-lifecycle-runtime-compose = "2.9.0" +androidx-navigation = "2.9.0" androidx-paging = "3.3.6" androidx-startup-runtime = "1.2.0" androidx-test = "1.6.1" androidx-test-espresso = "3.6.1" androidx-test-junit = "1.2.1" -androidx-window = "1.5.0-alpha01" -androidx-window-core = "1.5.0-alpha01" -androidx-window-java = "1.5.0-alpha01" +androidx-window = "1.5.0-alpha02" +androidx-window-core = "1.5.0-alpha02" +androidx-window-java = "1.5.0-alpha02" # @keep androidx-xr = "1.0.0-alpha03" androidxHiltNavigationCompose = "1.2.0" @@ -35,7 +35,7 @@ appcompat = "1.7.0" coil = "2.7.0" # @keep compileSdk = "35" -compose-latest = "1.8.0" +compose-latest = "1.8.1" composeUiTooling = "1.4.1" coreSplashscreen = "1.0.1" coroutines = "1.10.2" @@ -50,7 +50,7 @@ kotlin = "2.1.20" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" kotlinxSerializationJson = "1.8.1" -ksp = "2.1.20-2.0.0" +ksp = "2.1.20-2.0.1" maps-compose = "6.6.0" material = "1.13.0-alpha13" material3-adaptive = "1.1.0" @@ -76,8 +76,8 @@ wearToolingPreview = "1.0.0" webkit = "1.13.0" [libraries] -accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } -accompanist-permissions = "com.google.accompanist:accompanist-permissions:0.37.2" +accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3" +accompanist-permissions = "com.google.accompanist:accompanist-permissions:0.37.3" accompanist-theme-adapter-appcompat = { module = "com.google.accompanist:accompanist-themeadapter-appcompat", version.ref = "accompanist" } accompanist-theme-adapter-material = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanist" } accompanist-theme-adapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist" } @@ -102,9 +102,9 @@ androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-latest" } androidx-compose-ui-googlefonts = { module = "androidx.compose.ui:ui-text-google-fonts" } androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } -androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose-latest" } -androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-latest" } -androidx-compose-ui-test-junit4-accessibility = { group = "androidx.compose.ui", name = "ui-test-junit4-accessibility", version.ref = "androidx-compose-ui-test-junit4-accessibility" } +androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose-latest" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-latest" } +androidx-compose-ui-test-junit4-accessibility = { module = "androidx.compose.ui:ui-test-junit4-accessibility", version.ref = "androidx-compose-ui-test-junit4-accessibility" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } diff --git a/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java b/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java index 03d3fc812..e3765998a 100644 --- a/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java +++ b/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java @@ -1,5 +1,6 @@ package com.example.snippets; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -348,6 +349,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { static class ActivityPinningSnippetsActivity extends Activity { + @SuppressLint("RequiresWindowSdk") @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt index 909672387..d00308286 100644 --- a/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt +++ b/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt @@ -16,6 +16,7 @@ package com.example.snippets +import android.annotation.SuppressLint import android.app.Activity import android.content.ComponentName import android.content.Context @@ -53,6 +54,7 @@ class ActivityEmbeddingKotlinSnippets { class SplitAttributesCalculatorSnippetsActivity : AppCompatActivity() { + @SuppressLint("RequiresWindowSdk") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -224,6 +226,7 @@ class ActivityEmbeddingKotlinSnippets { class SplitAttributesBuilderSnippetsActivity : AppCompatActivity() { + @SuppressLint("RequiresWindowSdk") @RequiresApi(VERSION_CODES.M) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -327,6 +330,7 @@ class ActivityEmbeddingKotlinSnippets { } class ActivityPinningSnippetsActivity : AppCompatActivity() { + @SuppressLint("RequiresWindowSdk") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) From 187d70f980fbe0f0b300c7fb24c8e092d29328fa Mon Sep 17 00:00:00 2001 From: Vinny Date: Thu, 8 May 2025 15:10:41 -0400 Subject: [PATCH 005/120] Adding snippet for MV-HEVC video playback (#513) --- .../com/example/xr/scenecore/SpatialVideo.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index 272c21a8a..f60d9ecfe 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -32,7 +32,6 @@ private fun ComponentActivity.surfaceEntityCreate(xrSession: Session) { val stereoSurfaceEntity = SurfaceEntity.create( xrSession, SurfaceEntity.StereoMode.SIDE_BY_SIDE, - // Position 1.5 meters in front of user Pose(Vector3(0.0f, 0.0f, -1.5f)), SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) ) @@ -83,3 +82,26 @@ private fun ComponentActivity.surfaceEntityCreateTb(xrSession: Session) { // ... and use the surface for playing the media. // [END androidxr_scenecore_surfaceEntityCreateTb] } + +private fun ComponentActivity.surfaceEntityCreateMVHEVC(xrSession: Session) { + // [START androidxr_scenecore_surfaceEntityCreateMVHEVC] + // Create the SurfaceEntity with the StereoMode corresponding to the MV-HEVC content + val stereoSurfaceEntity = SurfaceEntity.create( + xrSession, + SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY, + Pose(Vector3(0.0f, 0.0f, -1.5f)), + SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) + ) + val videoUri = Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .path("mvhevc_video.mp4") + .build() + val mediaItem = MediaItem.fromUri(videoUri) + + val exoPlayer = ExoPlayer.Builder(this).build() + exoPlayer.setVideoSurface(stereoSurfaceEntity.getSurface()) + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.play() + // [END androidxr_scenecore_surfaceEntityCreateMVHEVC] +} \ No newline at end of file From 3f4d4add42dd3dd39028f70bb0fe8ae69c140cd4 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Mon, 12 May 2025 09:47:46 +0100 Subject: [PATCH 006/120] Update to 1.5.0-beta01 (#514) * Update to 1.5.0-beta01 Change-Id: I5acad074c6dc067b10d37ff1416c671e7c9c91b9 * Apply Spotless --------- Co-authored-by: kul3r4 <820891+kul3r4@users.noreply.github.com> --- gradle/libs.versions.toml | 15 +- wear/build.gradle.kts | 4 +- wear/src/main/AndroidManifest.xml | 19 +- .../MainActivity.kt | 39 +++ .../com.example.wear.snippets.m3/list/List.kt | 175 +++++++++++++ .../navigation/Navigation.kt | 142 +++++++++++ .../rotary/Rotary.kt | 233 ++++++++++++++++++ .../com.example.wear.snippets.m3/tile/Tile.kt | 60 +++++ .../voiceinput/VoiceInputScreen.kt | 109 ++++++++ .../wear/snippets/navigation/Navigation.kt | 12 +- .../snippets/voiceinput/VoiceInputScreen.kt | 12 +- .../com/example/xr/scenecore/SpatialVideo.kt | 2 +- 12 files changed, 807 insertions(+), 15 deletions(-) create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/list/List.kt create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt create mode 100644 wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 543a9545f..babb910f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ google-maps = "19.2.0" gradle-versions = "0.52.0" guava = "33.4.8-jre" hilt = "2.56.2" -horologist = "0.6.23" +horologist = "0.7.10-alpha" junit = "4.13.2" kotlin = "2.1.20" kotlinCoroutinesOkhttp = "1.0" @@ -60,18 +60,19 @@ media3 = "1.6.1" minSdk = "21" okHttp = "4.12.0" playServicesWearable = "19.0.0" -protolayout = "1.2.1" +protolayout = "1.3.0-beta02" recyclerview = "1.4.0" # @keep androidx-xr-arcore = "1.0.0-alpha04" androidx-xr-scenecore = "1.0.0-alpha04" androidx-xr-compose = "1.0.0-alpha04" targetSdk = "34" -tiles = "1.4.1" +tiles = "1.5.0-beta01" version-catalog-update = "1.0.0" wear = "1.3.0" -wearComposeFoundation = "1.4.1" -wearComposeMaterial = "1.4.1" +wearComposeFoundation = "1.5.0-beta01" +wearComposeMaterial = "1.5.0-beta01" +wearComposeMaterial3 = "1.5.0-beta01" wearToolingPreview = "1.0.0" webkit = "1.13.0" @@ -135,6 +136,7 @@ androidx-paging-compose = { module = "androidx.paging:paging-compose", version.r androidx-protolayout = { module = "androidx.wear.protolayout:protolayout", version.ref = "protolayout" } androidx-protolayout-expression = { module = "androidx.wear.protolayout:protolayout-expression", version.ref = "protolayout" } androidx-protolayout-material = { module = "androidx.wear.protolayout:protolayout-material", version.ref = "protolayout" } +androidx-protolayout-material3 = { module = "androidx.wear.protolayout:protolayout-material3", version.ref = "protolayout" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup-runtime" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } @@ -159,7 +161,7 @@ androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.re appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } -compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } +wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "composeUiTooling" } glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } google-android-material = { module = "com.google.android.material:material", version.ref = "material" } @@ -179,6 +181,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } +wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 154e7d37e..888f3d434 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.androidx.wear) implementation(libs.androidx.protolayout) implementation(libs.androidx.protolayout.material) + implementation(libs.androidx.protolayout.material3) implementation(libs.androidx.protolayout.expression) debugImplementation(libs.androidx.tiles.renderer) testImplementation(libs.androidx.tiles.testing) @@ -69,7 +70,8 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.compose.material) + implementation(libs.wear.compose.material) + implementation(libs.wear.compose.material3) implementation(libs.compose.foundation) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 84c4785a2..c2740b2dc 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ android:value="true" /> @@ -52,6 +52,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt b/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt new file mode 100644 index 000000000..52e9c2eb7 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt @@ -0,0 +1,39 @@ +/* + * 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 + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import com.example.wear.snippets.m3.list.ComposeList + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + WearApp() + } + } +} + +@Composable +fun WearApp() { + // insert here the snippet you want to test + ComposeList() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt b/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt new file mode 100644 index 000000000..29b0f8fd5 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt @@ -0,0 +1,175 @@ +/* + * 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.snippets.m3.list + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.layout.ColumnItemType +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults +import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState +import com.google.android.horologist.compose.material.Button +import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding +import com.google.android.horologist.compose.material.ResponsiveListHeader + +@Composable +fun ComposeList() { + // [START android_wear_list] + val columnState = rememberTransformingLazyColumnState() + val contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader, + last = ColumnItemType.Button, + ) + val transformationSpec = rememberTransformationSpec() + ScreenScaffold( + scrollState = columnState, + contentPadding = contentPadding + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + // [END android_wear_list] +} + +@OptIn(ExperimentalHorologistApi::class) +@Composable +fun SnapAndFlingComposeList() { + // [START android_wear_snap] + val columnState = rememberResponsiveColumnState( + // ... + // [START_EXCLUDE] + contentPadding = ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.SingleButton + ), + // [END_EXCLUDE] + rotaryMode = ScalingLazyColumnState.RotaryMode.Snap + ) + ScreenScaffold(scrollState = columnState) { + ScalingLazyColumn( + columnState = columnState + ) { + // ... + // [START_EXCLUDE] + item { + ResponsiveListHeader(contentPadding = firstItemPadding()) { + androidx.wear.compose.material.Text(text = "Header") + } + } + // ... other items + item { + Button( + imageVector = Icons.Default.Build, + contentDescription = "Example Button", + onClick = { } + ) + } + // [END_EXCLUDE] + } + } + // [END android_wear_snap] +} + +// [START android_wear_list_breakpoint] +const val LARGE_DISPLAY_BREAKPOINT = 225 + +@Composable +fun isLargeDisplay() = + LocalConfiguration.current.screenWidthDp >= LARGE_DISPLAY_BREAKPOINT + +// [START_EXCLUDE] +@Composable +fun breakpointDemo() { + // [END_EXCLUDE] +// ... use in your Composables: + if (isLargeDisplay()) { + // Show additional content. + } else { + // Show content only for smaller displays. + } + // [START_EXCLUDE] +} +// [END_EXCLUDE] +// [END android_wear_list_breakpoint] + +// [START android_wear_list_preview] +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun ComposeListPreview() { + ComposeList() +} +// [END android_wear_list_preview] + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun SnapAndFlingComposeListPreview() { + SnapAndFlingComposeList() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt b/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt new file mode 100644 index 000000000..283fa08ca --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt @@ -0,0 +1,142 @@ +/* + * 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.snippets.m3.navigation + +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.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 + +@Composable +fun navigation() { + // [START android_wear_navigation] + AppScaffold { + val navController = rememberSwipeDismissableNavController() + SwipeDismissableNavHost( + navController = navController, + startDestination = "message_list" + ) { + composable("message_list") { + MessageList(onMessageClick = { id -> + navController.navigate("message_detail/$id") + }) + } + composable("message_detail/{id}") { + MessageDetail(id = it.arguments?.getString("id")!!) + } + } + } + // [START_EXCLUDE] +} + +@Composable +fun MessageDetail(id: String) { + // [END_EXCLUDE] + // .. Screen level content goes here + val scrollState = rememberTransformingLazyColumnState() + + val padding = rememberResponsiveColumnPadding( + first = ColumnItemType.BodyText + ) + + ScreenScaffold( + scrollState = scrollState, + contentPadding = padding + ) { + // Screen content goes here + // [END android_wear_navigation] + TransformingLazyColumn(state = scrollState) { + item { + Text( + text = id, + 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() { + MessageDetail("test") +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun MessageListPreview() { + MessageList(onMessageClick = {}) +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt b/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt new file mode 100644 index 000000000..ac352c73b --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2024 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.rotary + +import android.view.MotionEvent +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.input.rotary.onRotaryScrollEvent +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.Picker +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.ScrollIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.rememberPickerState +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import kotlinx.coroutines.launch + +@Composable +fun TimePicker() { + val textStyle = MaterialTheme.typography.displayMedium + + // [START android_wear_rotary_input_picker] + var selectedColumn by remember { mutableIntStateOf(0) } + + val hoursFocusRequester = remember { FocusRequester() } + val minutesRequester = remember { FocusRequester() } + // [START_EXCLUDE] + val coroutineScope = rememberCoroutineScope() + + @Composable + fun Option(column: Int, text: String) = Box(modifier = Modifier.fillMaxSize()) { + Text( + text = text, style = textStyle, + color = if (selectedColumn == column) MaterialTheme.colorScheme.secondary + else MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .pointerInteropFilter { + if (it.action == MotionEvent.ACTION_DOWN) selectedColumn = column + true + } + ) + } + // [END_EXCLUDE] + ScreenScaffold(modifier = Modifier.fillMaxSize()) { + Row( + // [START_EXCLUDE] + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + // [END_EXCLUDE] + // ... + ) { + // [START_EXCLUDE] + val hourState = rememberPickerState( + initialNumberOfOptions = 12, + initiallySelectedIndex = 5 + ) + val hourContentDescription by remember { + derivedStateOf { "${hourState.selectedOptionIndex + 1 } hours" } + } + // [END_EXCLUDE] + Picker( + readOnly = selectedColumn != 0, + modifier = Modifier.size(64.dp, 100.dp) + .onRotaryScrollEvent { + coroutineScope.launch { + hourState.scrollBy(it.verticalScrollPixels) + } + true + } + .focusRequester(hoursFocusRequester) + .focusable(), + onSelected = { selectedColumn = 0 }, + // ... + // [START_EXCLUDE] + state = hourState, + contentDescription = { hourContentDescription }, + option = { hour: Int -> Option(0, "%2d".format(hour + 1)) } + // [END_EXCLUDE] + ) + // [START_EXCLUDE] + Spacer(Modifier.width(8.dp)) + Text(text = ":", style = textStyle, color = MaterialTheme.colorScheme.onBackground) + Spacer(Modifier.width(8.dp)) + val minuteState = + rememberPickerState(initialNumberOfOptions = 60, initiallySelectedIndex = 0) + val minuteContentDescription by remember { + derivedStateOf { "${minuteState.selectedOptionIndex} minutes" } + } + // [END_EXCLUDE] + Picker( + readOnly = selectedColumn != 1, + modifier = Modifier.size(64.dp, 100.dp) + .onRotaryScrollEvent { + coroutineScope.launch { + minuteState.scrollBy(it.verticalScrollPixels) + } + true + } + .focusRequester(minutesRequester) + .focusable(), + onSelected = { selectedColumn = 1 }, + // ... + // [START_EXCLUDE] + state = minuteState, + contentDescription = { minuteContentDescription }, + option = { minute: Int -> Option(1, "%02d".format(minute)) } + // [END_EXCLUDE] + ) + LaunchedEffect(selectedColumn) { + listOf( + hoursFocusRequester, + minutesRequester + )[selectedColumn] + .requestFocus() + } + } + } + // [END android_wear_rotary_input_picker] +} + +@Composable +fun SnapScrollableScreen() { + // This sample doesn't add a Time Text at the top of the screen. + // If using Time Text, add padding to ensure content does not overlap with Time Text. + // [START android_wear_rotary_input_snap_fling] + val listState = rememberScalingLazyListState() + ScreenScaffold( + scrollIndicator = { + ScrollIndicator(state = listState) + } + ) { + + val state = rememberScalingLazyListState() + ScalingLazyColumn( + modifier = Modifier.fillMaxWidth(), + state = state, + flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state) + ) { + // Content goes here + // [START_EXCLUDE] + item { ListHeader { Text(text = "List Header") } } + items(20) { + Button( + onClick = {}, + label = { Text("List item $it") }, + colors = ButtonDefaults.filledTonalButtonColors() + ) + } + // [END_EXCLUDE] + } + } + // [END android_wear_rotary_input_snap_fling] +} + +@Composable +fun PositionScrollIndicator() { + // [START android_wear_rotary_position_indicator] + val listState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollIndicator = { + ScrollIndicator(state = listState) + } + ) { + // ... + } + // [END android_wear_rotary_position_indicator] +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun TimePickerPreview() { + TimePicker() +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun SnapScrollableScreenPreview() { + SnapScrollableScreen() +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun PositionScrollIndicatorPreview() { + PositionScrollIndicator() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt b/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt new file mode 100644 index 000000000..2ad9812e2 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt @@ -0,0 +1,60 @@ +/* + * 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 androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders.Timeline +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.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 + +private const val RESOURCES_VERSION = "1" + +// [START android_wear_m3_tile_mytileservice] +class MyTileService : TileService() { + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + materialScope(this, requestParams.deviceConfiguration) { + primaryLayout( + mainSlot = { + text("Hello, World!".layoutString, typography = BODY_LARGE) + } + ) + } + ) + ) + .build() + ) + + override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture( + Resources.Builder().setVersion(RESOURCES_VERSION).build() + ) +} +// [END android_wear_m3_tile_mytileservice] diff --git a/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt b/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt new file mode 100644 index 000000000..d926487c5 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2024 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.voiceinput + +import android.content.Intent +import android.speech.RecognizerIntent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.ScreenScaffold +import androidx.wear.compose.material3.Text +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 + +/** + * Shows voice input option + */ +@Composable +fun VoiceInputScreen() { + AppScaffold { + // [START android_wear_voice_input] + var textForVoiceInput by remember { mutableStateOf("") } + + val voiceLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { activityResult -> + // This is where you process the intent and extract the speech text from the intent. + activityResult.data?.let { data -> + val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + textForVoiceInput = results?.get(0) ?: "None" + } + } + + val scrollState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollState = scrollState, + contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.Button + ) + ) { contentPadding -> + TransformingLazyColumn( + contentPadding = contentPadding, + state = scrollState, + ) { + item { + // Create an intent that can start the Speech Recognizer activity + val voiceIntent: Intent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + + putExtra( + RecognizerIntent.EXTRA_PROMPT, + stringResource(R.string.voice_text_entry_label) + ) + } + // Invoke the process from a Button + Button( + onClick = { + voiceLauncher.launch(voiceIntent) + }, + label = { Text(stringResource(R.string.voice_input_label)) }, + secondaryLabel = { Text(textForVoiceInput) }, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + // [END android_wear_voice_input] + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun VoiceInputScreenPreview() { + VoiceInputScreen() +} diff --git a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt index 42507078c..ed75220ab 100644 --- a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt +++ b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt @@ -26,6 +26,9 @@ 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.wear.compose.foundation.rememberActiveFocusRequester +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults.behavior +import androidx.wear.compose.foundation.rotary.rotaryScrollable import androidx.wear.compose.material.Text import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable @@ -43,7 +46,6 @@ import com.google.android.horologist.compose.layout.rememberResponsiveColumnStat import com.google.android.horologist.compose.material.Chip import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding import com.google.android.horologist.compose.material.ResponsiveListHeader -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll @Composable fun navigation() { @@ -81,12 +83,16 @@ fun MessageDetail(id: String) { first = ItemType.Text, last = ItemType.Text )() + val focusRequester = rememberActiveFocusRequester() Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) - .rotaryWithScroll(scrollState) - .padding(padding), + .padding(padding) + .rotaryScrollable( + behavior = behavior(scrollableState = scrollState), + focusRequester = focusRequester, + ), verticalArrangement = Arrangement.Center ) { Text( diff --git a/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt b/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt index fa80ab800..31bbde0e9 100644 --- a/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt +++ b/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt @@ -49,6 +49,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.wear.compose.foundation.rememberActiveFocusRequester +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults.behavior +import androidx.wear.compose.foundation.rotary.rotaryScrollable import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales import com.example.wear.R @@ -58,7 +61,6 @@ import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.ItemType import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.material.Chip -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll /** * Shows voice input option @@ -90,6 +92,7 @@ fun VoiceInputScreen() { first = ItemType.Text, last = ItemType.Chip )() + val focusRequester = rememberActiveFocusRequester() // [END_EXCLUDE] Column( // rest of implementation here @@ -97,8 +100,11 @@ fun VoiceInputScreen() { modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) - .rotaryWithScroll(scrollState) - .padding(padding), + .padding(padding) + .rotaryScrollable( + behavior = behavior(scrollableState = scrollState), + focusRequester = focusRequester, + ), verticalArrangement = Arrangement.Center ) { // [END_EXCLUDE] diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index f60d9ecfe..460d35db2 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -104,4 +104,4 @@ private fun ComponentActivity.surfaceEntityCreateMVHEVC(xrSession: Session) { exoPlayer.prepare() exoPlayer.play() // [END androidxr_scenecore_surfaceEntityCreateMVHEVC] -} \ No newline at end of file +} From 21896a6fd761ea6494b71332bf1d99f12858b1b9 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Mon, 12 May 2025 11:11:54 +0100 Subject: [PATCH 007/120] Fix package name (#515) --- .../example/wear/snippets/m3}/MainActivity.kt | 0 .../example/wear/snippets/m3}/list/List.kt | 0 .../example/wear/snippets/m3}/navigation/Navigation.kt | 0 .../example/wear/snippets/m3}/rotary/Rotary.kt | 0 .../example/wear/snippets/m3}/tile/Tile.kt | 0 .../example/wear/snippets/m3}/voiceinput/VoiceInputScreen.kt | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/MainActivity.kt (100%) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/list/List.kt (100%) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/navigation/Navigation.kt (100%) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/rotary/Rotary.kt (100%) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/tile/Tile.kt (100%) rename wear/src/main/java/{com.example.wear.snippets.m3 => com/example/wear/snippets/m3}/voiceinput/VoiceInputScreen.kt (100%) diff --git a/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt b/wear/src/main/java/com/example/wear/snippets/m3/MainActivity.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt rename to wear/src/main/java/com/example/wear/snippets/m3/MainActivity.kt diff --git a/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt b/wear/src/main/java/com/example/wear/snippets/m3/list/List.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/list/List.kt rename to wear/src/main/java/com/example/wear/snippets/m3/list/List.kt diff --git a/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt rename to wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt diff --git a/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt b/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt rename to wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt diff --git a/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt rename to wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt diff --git a/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt b/wear/src/main/java/com/example/wear/snippets/m3/voiceinput/VoiceInputScreen.kt similarity index 100% rename from wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt rename to wear/src/main/java/com/example/wear/snippets/m3/voiceinput/VoiceInputScreen.kt From 70285acd83dcff90fe1b9b2b1fa33e41240b087c Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Tue, 13 May 2025 15:50:21 +0100 Subject: [PATCH 008/120] Fix scrollState for List and expand the snippet to show more code for completeness (#517) * Fix scrollState for List and expand the snippet to show kore code for completeness * Apply Spotless --------- Co-authored-by: kul3r4 <820891+kul3r4@users.noreply.github.com> --- .../java/com/example/wear/snippets/m3/navigation/Navigation.kt | 3 +-- .../main/java/com/example/wear/snippets/m3/rotary/Rotary.kt | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt index 283fa08ca..c595c54ef 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt @@ -57,12 +57,11 @@ fun navigation() { } } } - // [START_EXCLUDE] } +// Implementation of one of the screens in the navigation @Composable fun MessageDetail(id: String) { - // [END_EXCLUDE] // .. Screen level content goes here val scrollState = rememberTransformingLazyColumnState() diff --git a/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt b/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt index ac352c73b..4cff56480 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt @@ -170,6 +170,7 @@ fun SnapScrollableScreen() { // [START android_wear_rotary_input_snap_fling] val listState = rememberScalingLazyListState() ScreenScaffold( + scrollState = listState, scrollIndicator = { ScrollIndicator(state = listState) } @@ -202,6 +203,7 @@ fun PositionScrollIndicator() { // [START android_wear_rotary_position_indicator] val listState = rememberTransformingLazyColumnState() ScreenScaffold( + scrollState = listState, scrollIndicator = { ScrollIndicator(state = listState) } From 903fcbc9fa5a91e7287125e8fe63eb5eda1c0288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Wed, 14 May 2025 20:55:26 +0200 Subject: [PATCH 009/120] Properly handle recycling of RecomposeHighlighterModifier (#510) * Properly handle recycling of RecomposeHighlighterModifier After migrating to be Node based, it's possible the Modifier node can be reused. For example, when scrolling in a LazyColumn the nodes of items scrolled off the viewport may be recycled and used with items scrolling into the viewport. When this occurs, the composition count needs to be reset. Otherwise, it may look like the item scrolling into the viewport has recomposed more than it actually has. * Fix code style --- .../compose/recomposehighlighter/RecomposeHighlighter.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt index fdcbbda30..cc92daed6 100644 --- a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt +++ b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt @@ -89,6 +89,11 @@ private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode { override val shouldAutoInvalidate: Boolean = false + override fun onReset() { + totalCompositions = 0 + timerJob?.cancel() + } + override fun onDetach() { timerJob?.cancel() } From f6e899c0f8e1659b98e80054960468da9d877166 Mon Sep 17 00:00:00 2001 From: MagicalMeghan <46006059+MagicalMeghan@users.noreply.github.com> Date: Thu, 15 May 2025 18:34:51 -0400 Subject: [PATCH 010/120] State based TF snippets (#520) * State based TF snippets * Apply Spotless * updates * Apply Spotless * Update StateBasedText.kt * Update StateBasedText.kt * Update StateBasedText.kt * Update StateBasedText.kt * Apply Spotless --- .../compose/snippets/text/StateBasedText.kt | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt new file mode 100644 index 000000000..50375279c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt @@ -0,0 +1,274 @@ +/* + * 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.compose.snippets.text + +import android.text.TextUtils +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldBuffer +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.insert +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.selectAll +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.text.input.then +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly +import androidx.lifecycle.ViewModel + +@Composable +fun StateBasedTextSnippets() { + // [START android_compose_state_text_1] + BasicTextField(state = rememberTextFieldState()) + + TextField(state = rememberTextFieldState()) + // [END android_compose_state_text_1] +} + +@Composable +fun StyleTextField() { + // [START android_compose_state_text_2] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 2), + placeholder = { Text("") }, + textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold), + modifier = Modifier.padding(20.dp) + ) + // [END android_compose_state_text_2] +} + +@Composable +fun ConfigureLineLimits() { + // [START android_compose_state_text_3] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine + ) + // [END android_compose_state_text_3] + + // [START android_compose_state_text_4] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.MultiLine(1, 4) + ) + // [END android_compose_state_text_4] +} + +@Composable +fun StyleWithBrush() { + // [START android_compose_state_text_5] + val brush = remember { + Brush.linearGradient( + colors = listOf(Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Magenta) + ) + } + TextField( + state = rememberTextFieldState(), textStyle = TextStyle(brush = brush) + ) + // [END android_compose_state_text_5] +} + +@Composable +fun StateHoisting() { + // [START android_compose_state_text_6] + val usernameState = rememberTextFieldState() + TextField( + state = usernameState, + lineLimits = TextFieldLineLimits.SingleLine, + placeholder = { Text("Enter Username") } + ) + // [END android_compose_state_text_6] +} + +@Composable +fun TextFieldInitialState() { + // [START android_compose_state_text_7] + TextField( + state = rememberTextFieldState(initialText = "Username"), + lineLimits = TextFieldLineLimits.SingleLine, + ) + // [END android_compose_state_text_7] +} + +@Composable +fun TextFieldBuffer() { + // [START android_compose_state_text_8] + val phoneNumberState = rememberTextFieldState() + + LaunchedEffect(phoneNumberState) { + phoneNumberState.edit { // TextFieldBuffer scope + append("123456789") + } + } + + TextField( + state = phoneNumberState, + inputTransformation = InputTransformation { // TextFieldBuffer scope + if (asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, + outputTransformation = OutputTransformation { + if (length > 0) insert(0, "(") + if (length > 4) insert(4, ")") + if (length > 8) insert(8, "-") + } + ) + // [END android_compose_state_text_8] +} + +@Preview +@Composable +fun EditTextFieldState() { + // [START android_compose_state_text_9] + val usernameState = rememberTextFieldState("I love Android") + // textFieldState.text : I love Android + // textFieldState.selection: TextRange(14, 14) + usernameState.edit { insert(14, "!") } + // textFieldState.text : I love Android! + // textFieldState.selection: TextRange(15, 15) + usernameState.edit { replace(7, 14, "Compose") } + // textFieldState.text : I love Compose! + // textFieldState.selection: TextRange(15, 15) + usernameState.edit { append("!!!") } + // textFieldState.text : I love Compose!!!! + // textFieldState.selection: TextRange(18, 18) + usernameState.edit { selectAll() } + // textFieldState.text : I love Compose!!!! + // textFieldState.selection: TextRange(0, 18) + // [END android_compose_state_text_9] + + // [START android_compose_state_text_10] + usernameState.setTextAndPlaceCursorAtEnd("I really love Android") + // textFieldState.text : I really love Android + // textFieldState.selection : TextRange(21, 21) + // [END android_compose_state_text_10] + + // [START android_compose_state_text_11] + usernameState.clearText() + // textFieldState.text : + // textFieldState.selection : TextRange(0, 0) + // [END android_compose_state_text_11] +} + +class TextFieldViewModel : ViewModel() { + val usernameState = TextFieldState() + fun validateUsername() { + } +} +val textFieldViewModel = TextFieldViewModel() + +@Composable +fun TextFieldKeyboardOptions() { + // [START android_compose_state_text_13] + TextField( + state = textFieldViewModel.usernameState, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + onKeyboardAction = { performDefaultAction -> + textFieldViewModel.validateUsername() + performDefaultAction() + } + ) + // [END android_compose_state_text_13] +} + +@Composable +fun TextFieldInputTransformation() { + // [START android_compose_state_text_14] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine, + inputTransformation = InputTransformation.maxLength(10) + ) + // [END android_compose_state_text_14] +} + +// [START android_compose_state_text_15] +class CustomInputTransformation : InputTransformation { + override fun TextFieldBuffer.transformInput() { + } +} +// [END android_compose_state_text_15] + +// [START android_compose_state_text_16] +class DigitOnlyInputTransformation : InputTransformation { + override fun TextFieldBuffer.transformInput() { + if (!TextUtils.isDigitsOnly(asCharSequence())) { + revertAllChanges() + } + } +} +// [END android_compose_state_text_16] + +@Composable +fun ChainInputTransformation() { + // [START android_compose_state_text_17] + TextField( + state = rememberTextFieldState(), + inputTransformation = InputTransformation.maxLength(6) + .then(CustomInputTransformation()), + ) + // [END android_compose_state_text_17] +} + +// [START android_compose_state_text_18] +class CustomOutputTransformation : OutputTransformation { + override fun TextFieldBuffer.transformOutput() { + } +} +// [END android_compose_state_text_18] + +// [START android_compose_state_text_19] +class PhoneNumberOutputTransformation : OutputTransformation { + override fun TextFieldBuffer.transformOutput() { + if (length > 0) insert(0, "(") + if (length > 4) insert(4, ")") + if (length > 8) insert(8, "-") + } +} +// [END android_compose_state_text_19] + +@Composable +fun TextFieldOutputTransformation() { + // [START android_compose_state_text_20] + TextField( + state = rememberTextFieldState(), + outputTransformation = PhoneNumberOutputTransformation() + ) + // [END android_compose_state_text_20] +} From 9d6278951bdcc7b269f9d6d3131fc947319a4c87 Mon Sep 17 00:00:00 2001 From: Lauren Ward Date: Thu, 15 May 2025 17:11:47 -0600 Subject: [PATCH 011/120] Icon button snippets (#512) * Adding icon button snippets * Apply Spotless * Changing M3 extended icons to hardcoded drawables in IconButton code * Apply Spotless * Adding region tags * Remove unused import * Updating to hardcoded drawables * Apply Spotless --------- Co-authored-by: wardlauren <203715894+wardlauren@users.noreply.github.com> --- .../compose/snippets/components/IconButton.kt | 124 ++++++++++++++++++ .../src/main/res/drawable/fast_forward.xml | 9 ++ .../main/res/drawable/fast_forward_filled.xml | 9 ++ .../src/main/res/drawable/fast_rewind.xml | 9 ++ .../main/res/drawable/fast_rewind_filled.xml | 9 ++ .../src/main/res/drawable/favorite.xml | 9 ++ .../src/main/res/drawable/favorite_filled.xml | 9 ++ 7 files changed, 178 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt create mode 100644 compose/snippets/src/main/res/drawable/fast_forward.xml create mode 100644 compose/snippets/src/main/res/drawable/fast_forward_filled.xml create mode 100644 compose/snippets/src/main/res/drawable/fast_rewind.xml create mode 100644 compose/snippets/src/main/res/drawable/fast_rewind_filled.xml create mode 100644 compose/snippets/src/main/res/drawable/favorite.xml create mode 100644 compose/snippets/src/main/res/drawable/favorite_filled.xml diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt new file mode 100644 index 000000000..59727c71b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt @@ -0,0 +1,124 @@ +/* + * 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.compose.snippets.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.example.compose.snippets.R +import kotlinx.coroutines.delay + +// [START android_compose_components_togglebuttonexample] +@Preview +@Composable +fun ToggleIconButtonExample() { + // isToggled initial value should be read from a view model or persistent storage. + var isToggled by rememberSaveable { mutableStateOf(false) } + + IconButton( + onClick = { isToggled = !isToggled } + ) { + Icon( + painter = if (isToggled) painterResource(R.drawable.favorite_filled) else painterResource(R.drawable.favorite), + contentDescription = if (isToggled) "Selected icon button" else "Unselected icon button." + ) + } +} +// [END android_compose_components_togglebuttonexample] + +// [START android_compose_components_iconbutton] +@Composable +fun MomentaryIconButton( + unselectedImage: Int, + selectedImage: Int, + contentDescription: String, + modifier: Modifier = Modifier, + stepDelay: Long = 100L, // Minimum value is 1L milliseconds. + onClick: () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val pressedListener by rememberUpdatedState(onClick) + + LaunchedEffect(isPressed) { + while (isPressed) { + delay(stepDelay.coerceIn(1L, Long.MAX_VALUE)) + pressedListener() + } + } + + IconButton( + modifier = modifier, + onClick = onClick, + interactionSource = interactionSource + ) { + Icon( + painter = if (isPressed) painterResource(id = selectedImage) else painterResource(id = unselectedImage), + contentDescription = contentDescription, + ) + } +} +// [END android_compose_components_iconbutton] + +// [START android_compose_components_momentaryiconbuttons] +@Preview() +@Composable +fun MomentaryIconButtonExample() { + var pressedCount by remember { mutableIntStateOf(0) } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + MomentaryIconButton( + unselectedImage = R.drawable.fast_rewind, + selectedImage = R.drawable.fast_rewind_filled, + stepDelay = 100L, + onClick = { pressedCount -= 1 }, + contentDescription = "Decrease count button" + ) + Spacer(modifier = Modifier) + Text("advanced by $pressedCount frames") + Spacer(modifier = Modifier) + MomentaryIconButton( + unselectedImage = R.drawable.fast_forward, + selectedImage = R.drawable.fast_forward_filled, + contentDescription = "Increase count button", + stepDelay = 100L, + onClick = { pressedCount += 1 } + ) + } +} +// [END android_compose_components_momentaryiconbuttons] diff --git a/compose/snippets/src/main/res/drawable/fast_forward.xml b/compose/snippets/src/main/res/drawable/fast_forward.xml new file mode 100644 index 000000000..d49dffbf3 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/compose/snippets/src/main/res/drawable/fast_forward_filled.xml b/compose/snippets/src/main/res/drawable/fast_forward_filled.xml new file mode 100644 index 000000000..2986028f5 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_forward_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/compose/snippets/src/main/res/drawable/fast_rewind.xml b/compose/snippets/src/main/res/drawable/fast_rewind.xml new file mode 100644 index 000000000..aec6e80d9 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_rewind.xml @@ -0,0 +1,9 @@ + + + diff --git a/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml b/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml new file mode 100644 index 000000000..e9426630e --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/compose/snippets/src/main/res/drawable/favorite.xml b/compose/snippets/src/main/res/drawable/favorite.xml new file mode 100644 index 000000000..f9256d68d --- /dev/null +++ b/compose/snippets/src/main/res/drawable/favorite.xml @@ -0,0 +1,9 @@ + + + diff --git a/compose/snippets/src/main/res/drawable/favorite_filled.xml b/compose/snippets/src/main/res/drawable/favorite_filled.xml new file mode 100644 index 000000000..1e1136d7b --- /dev/null +++ b/compose/snippets/src/main/res/drawable/favorite_filled.xml @@ -0,0 +1,9 @@ + + + From cd2e0c1d974a70d936ce2baffe4ed3ccee4f9746 Mon Sep 17 00:00:00 2001 From: Vinny Date: Fri, 16 May 2025 13:19:53 -0400 Subject: [PATCH 012/120] Updates to ResizableComponent (#516) * Adding snippet for MV-HEVC video playback * updating the resizable snippet to include ResizeListener --- .../java/com/example/xr/scenecore/Entities.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/xr/src/main/java/com/example/xr/scenecore/Entities.kt b/xr/src/main/java/com/example/xr/scenecore/Entities.kt index 7c8046664..d4c723604 100644 --- a/xr/src/main/java/com/example/xr/scenecore/Entities.kt +++ b/xr/src/main/java/com/example/xr/scenecore/Entities.kt @@ -29,6 +29,9 @@ import androidx.xr.scenecore.MovableComponent import androidx.xr.scenecore.PlaneSemantic import androidx.xr.scenecore.PlaneType import androidx.xr.scenecore.ResizableComponent +import androidx.xr.scenecore.ResizeListener +import androidx.xr.scenecore.SurfaceEntity +import java.util.concurrent.Executor import java.util.concurrent.Executors private fun setPoseExample(entity: Entity) { @@ -73,11 +76,26 @@ private fun moveableComponentExample(session: Session, entity: Entity) { // [END androidxr_scenecore_moveableComponentExample] } -private fun resizableComponentExample(session: Session, entity: Entity) { +private fun resizableComponentExample(session: Session, entity: Entity, executor: Executor) { // [START androidxr_scenecore_resizableComponentExample] val resizableComponent = ResizableComponent.create(session) resizableComponent.minimumSize = Dimensions(177f, 100f, 1f) resizableComponent.fixedAspectRatio = 16f / 9f // Specify a 16:9 aspect ratio + + resizableComponent.addResizeListener( + executor, + object : ResizeListener { + override fun onResizeEnd(entity: Entity, finalSize: Dimensions) { + + // update the size in the component + resizableComponent.size = finalSize + + // update the Entity to reflect the new size + (entity as SurfaceEntity).canvasShape = SurfaceEntity.CanvasShape.Quad(finalSize.width, finalSize.height) + } + }, + ) + entity.addComponent(resizableComponent) // [END androidxr_scenecore_resizableComponentExample] } From 4c55c0d54bec68e11538433a6ed1cad36fbc9d44 Mon Sep 17 00:00:00 2001 From: MagicalMeghan <46006059+MagicalMeghan@users.noreply.github.com> Date: Fri, 16 May 2025 18:19:58 -0400 Subject: [PATCH 013/120] Update migration snippet (#524) * Adding TextField migration snippets This is for the new version of https://developer.android.com/develop/ui/compose/text/user-input * Update TextFieldMigrationSnippets.kt optimize import * Apply Spotless --------- Co-authored-by: Halil Ozercan --- .../text/TextFieldMigrationSnippets.kt | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt new file mode 100644 index 000000000..a9e970266 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt @@ -0,0 +1,345 @@ +/* + * 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.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.delete +import androidx.compose.foundation.text.input.insert +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.SecureTextField +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.Text +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.substring +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.compose.snippets.touchinput.Button +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update + +// [START android_compose_text_textfield_migration_old_simple] +@Composable +fun OldSimpleTextField() { + var state by rememberSaveable { mutableStateOf("") } + TextField( + value = state, + onValueChange = { state = it }, + singleLine = true, + ) +} +// [END android_compose_text_textfield_migration_old_simple] + +// [START android_compose_text_textfield_migration_new_simple] +@Composable +fun NewSimpleTextField() { + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine + ) +} +// [END android_compose_text_textfield_migration_new_simple] + +// [START android_compose_text_textfield_migration_old_filtering] +@Composable +fun OldNoLeadingZeroes() { + var input by rememberSaveable { mutableStateOf("") } + TextField( + value = input, + onValueChange = { newText -> + input = newText.trimStart { it == '0' } + } + ) +} +// [END android_compose_text_textfield_migration_old_filtering] + +// [START android_compose_text_textfield_migration_new_filtering] + +@Preview +@Composable +fun NewNoLeadingZeros() { + TextField( + state = rememberTextFieldState(), + inputTransformation = InputTransformation { + while (length > 0 && charAt(0) == '0') delete(0, 1) + } + ) +} +// [END android_compose_text_textfield_migration_new_filtering] + +// [START android_compose_text_textfield_migration_old_credit_card_formatter] +@Composable +fun OldTextFieldCreditCardFormatter() { + var state by remember { mutableStateOf("") } + TextField( + value = state, + onValueChange = { if (it.length <= 16) state = it }, + visualTransformation = VisualTransformation { text -> + // Making XXXX-XXXX-XXXX-XXXX string. + var out = "" + for (i in text.indices) { + out += text[i] + if (i % 4 == 3 && i != 15) out += "-" + } + + TransformedText( + text = AnnotatedString(out), + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset <= 3) return offset + if (offset <= 7) return offset + 1 + if (offset <= 11) return offset + 2 + if (offset <= 16) return offset + 3 + return 19 + } + + override fun transformedToOriginal(offset: Int): Int { + if (offset <= 4) return offset + if (offset <= 9) return offset - 1 + if (offset <= 14) return offset - 2 + if (offset <= 19) return offset - 3 + return 16 + } + } + ) + } + ) +} +// [END android_compose_text_textfield_migration_old_credit_card_formatter] + +// [START android_compose_text_textfield_migration_new_credit_card_formatter] +@Composable +fun NewTextFieldCreditCardFormatter() { + val state = rememberTextFieldState() + TextField( + state = state, + inputTransformation = InputTransformation.maxLength(16), + outputTransformation = OutputTransformation { + if (length > 4) insert(4, "-") + if (length > 9) insert(9, "-") + if (length > 14) insert(14, "-") + }, + ) +} +// [END android_compose_text_textfield_migration_new_credit_card_formatter] + +private object StateUpdateSimpleSnippet { + object UserRepository { + suspend fun fetchUsername(): String = TODO() + } + // [START android_compose_text_textfield_migration_old_update_state_simple] + @Composable + fun OldTextFieldStateUpdate(userRepository: UserRepository) { + var username by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + username = userRepository.fetchUsername() + } + TextField( + value = username, + onValueChange = { username = it } + ) + } + // [END android_compose_text_textfield_migration_old_update_state_simple] + + // [START android_compose_text_textfield_migration_new_update_state_simple] + @Composable + fun NewTextFieldStateUpdate(userRepository: UserRepository) { + val usernameState = rememberTextFieldState() + LaunchedEffect(Unit) { + usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) + } + TextField(state = usernameState) + } + // [END android_compose_text_textfield_migration_new_update_state_simple] +} + +// [START android_compose_text_textfield_migration_old_state_update_complex] +@Composable +fun OldTextFieldAddMarkdownEmphasis() { + var markdownState by remember { mutableStateOf(TextFieldValue()) } + Button(onClick = { + // add ** decorations around the current selection, also preserve the selection + markdownState = with(markdownState) { + copy( + text = buildString { + append(text.take(selection.min)) + append("**") + append(text.substring(selection)) + append("**") + append(text.drop(selection.max)) + }, + selection = TextRange(selection.min + 2, selection.max + 2) + ) + } + }) { + Text("Bold") + } + TextField( + value = markdownState, + onValueChange = { markdownState = it }, + maxLines = 10 + ) +} +// [END android_compose_text_textfield_migration_old_state_update_complex] + +// [START android_compose_text_textfield_migration_new_state_update_complex] +@Composable +fun NewTextFieldAddMarkdownEmphasis() { + val markdownState = rememberTextFieldState() + LaunchedEffect(Unit) { + // add ** decorations around the current selection + markdownState.edit { + insert(originalSelection.max, "**") + insert(originalSelection.min, "**") + selection = TextRange(originalSelection.min + 2, originalSelection.max + 2) + } + } + TextField( + state = markdownState, + lineLimits = TextFieldLineLimits.MultiLine(1, 10) + ) +} +// [END android_compose_text_textfield_migration_new_state_update_complex] + +private object ViewModelMigrationOldSnippet { + // [START android_compose_text_textfield_migration_old_viewmodel] + class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } + + fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } + } + + data class UiState( + val username: String = "", + val password: String = "" + ) + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + val uiState by loginViewModel.uiState.collectAsStateWithLifecycle() + Column(modifier) { + TextField( + value = uiState.username, + onValueChange = { loginViewModel.updateUsername(it) } + ) + TextField( + value = uiState.password, + onValueChange = { loginViewModel.updatePassword(it) }, + visualTransformation = PasswordVisualTransformation() + ) + } + } + // [END android_compose_text_textfield_migration_old_viewmodel] +} + +private object ViewModelMigrationNewSimpleSnippet { + // [START android_compose_text_textfield_migration_new_viewmodel_simple] + class LoginViewModel : ViewModel() { + val usernameState = TextFieldState() + val passwordState = TextFieldState() + } + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + Column(modifier) { + TextField(state = loginViewModel.usernameState,) + SecureTextField(state = loginViewModel.passwordState) + } + } + // [END android_compose_text_textfield_migration_new_viewmodel_simple] +} + +private object ViewModelMigrationNewConformingSnippet { + // [START android_compose_text_textfield_migration_new_viewmodel_conforming] + class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } + + fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } + } + + data class UiState( + val username: String = "", + val password: String = "" + ) + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value } + Column(modifier) { + val usernameState = rememberTextFieldState(initialUiState.username) + LaunchedEffect(usernameState) { + snapshotFlow { usernameState.text.toString() }.collectLatest { + loginViewModel.updateUsername(it) + } + } + TextField(usernameState) + + val passwordState = rememberTextFieldState(initialUiState.password) + LaunchedEffect(usernameState) { + snapshotFlow { usernameState.text.toString() }.collectLatest { + loginViewModel.updatePassword(it) + } + } + SecureTextField(passwordState) + } + } + // [END android_compose_text_textfield_migration_new_viewmodel_conforming] +} From a9a6a55f7e35738873eee76178d06cf39871834c Mon Sep 17 00:00:00 2001 From: MagicalMeghan <46006059+MagicalMeghan@users.noreply.github.com> Date: Fri, 16 May 2025 19:20:07 -0400 Subject: [PATCH 014/120] Add state based Autofill snippets (#525) * Add state based Autofill snippets * Update AutofillSnippets.kt * Apply Spotless --- .../compose/snippets/text/AutofillSnippets.kt | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt new file mode 100644 index 000000000..6e30ba93b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt @@ -0,0 +1,130 @@ +/* + * 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.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.LocalAutofillHighlightColor +import androidx.compose.foundation.text.input.rememberTextFieldState +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalAutofillManager +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.touchinput.Button + +@Composable +fun AddAutofill() { + // [START android_compose_autofill_1] + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.Username } + ) + // [END android_compose_autofill_1] +} + +@Composable +fun AddMultipleTypesOfAutofill() { + // [START android_compose_autofill_2] + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { + contentType = ContentType.Username + ContentType.EmailAddress + } + ) + // [END android_compose_autofill_2] +} + +@Composable +fun AutofillManager() { + // [START android_compose_autofill_3] + val autofillManager = LocalAutofillManager.current + // [END android_compose_autofill_3] +} + +@Composable +fun SaveDataWithAutofill() { + var textFieldValue = remember { + mutableStateOf(TextFieldValue("")) + } + // [START android_compose_autofill_4] + val autofillManager = LocalAutofillManager.current + + Column { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewUsername } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewPassword } + ) + } + // [END android_compose_autofill_4] +} + +@Composable +fun SaveDataWithAutofillOnClick() { + // [START android_compose_autofill_5] + val autofillManager = LocalAutofillManager.current + + Column { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewUsername }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewPassword }, + ) + + // Submit button + Button(onClick = { autofillManager?.commit() }) { Text("Reset credentials") } + } + // [END android_compose_autofill_5] +} + +@Composable +fun CustomAutofillHighlight(customHighlightColor: Color = Color.Red) { + // [START android_compose_autofill_6] + val customHighlightColor = Color.Red + + CompositionLocalProvider(LocalAutofillHighlightColor provides customHighlightColor) { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.Username } + ) + } + // [END android_compose_autofill_6] +} From ce81ef65312fad40c0ca5f921f9aad4892ce0268 Mon Sep 17 00:00:00 2001 From: MagicalMeghan <46006059+MagicalMeghan@users.noreply.github.com> Date: Mon, 19 May 2025 14:53:05 -0400 Subject: [PATCH 015/120] Update StateBasedText snippets (#526) * Update StateBasedText.kt Update state based TF snippets * Apply Spotless * Update StateBasedText.kt * Apply Spotless --- .../compose/snippets/text/StateBasedText.kt | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt index 50375279c..b43aef25f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt @@ -17,8 +17,10 @@ package com.example.compose.snippets.text import android.text.TextUtils +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.OutputTransformation @@ -32,6 +34,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.selectAll import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.text.input.then +import androidx.compose.material.OutlinedTextField //noinspection UsingMaterialAndMaterial3Libraries import androidx.compose.material.TextField //noinspection UsingMaterialAndMaterial3Libraries @@ -50,23 +53,36 @@ import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly import androidx.lifecycle.ViewModel +@Preview @Composable fun StateBasedTextSnippets() { - // [START android_compose_state_text_1] - BasicTextField(state = rememberTextFieldState()) + Column() { + // [START android_compose_state_text_0] + TextField( + state = rememberTextFieldState(initialText = "Hello"), + label = { Text("Label") } + ) + // [END android_compose_state_text_0] - TextField(state = rememberTextFieldState()) - // [END android_compose_state_text_1] + // [START android_compose_state_text_1] + OutlinedTextField( + state = rememberTextFieldState(), + label = { Text("Label") } + ) + // [END android_compose_state_text_1] + } } +@Preview @Composable fun StyleTextField() { // [START android_compose_state_text_2] TextField( - state = rememberTextFieldState(), + state = rememberTextFieldState("Hello\nWorld\nInvisible"), lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 2), placeholder = { Text("") }, textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold), + label = { Text("Enter text") }, modifier = Modifier.padding(20.dp) ) // [END android_compose_state_text_2] @@ -80,10 +96,15 @@ fun ConfigureLineLimits() { lineLimits = TextFieldLineLimits.SingleLine ) // [END android_compose_state_text_3] +} +@Preview +@Composable +fun Multiline() { + Spacer(modifier = Modifier.height(15.dp)) // [START android_compose_state_text_4] TextField( - state = rememberTextFieldState(), + state = rememberTextFieldState("Hello\nWorld\nHello\nWorld"), lineLimits = TextFieldLineLimits.MultiLine(1, 4) ) // [END android_compose_state_text_4] From 8f703ccc77de1c70a1dcf18166855336119c3486 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 22 May 2025 19:46:42 +0200 Subject: [PATCH 016/120] Add snippets for XR alpha04 hands (#530) * Add snippet that demonstrates how to detect a secondary hand * Add snippet to demonstrate how to detect a basic stop gesture --- .../main/java/com/example/xr/arcore/Hands.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt index af3a547b0..13346b202 100644 --- a/xr/src/main/java/com/example/xr/arcore/Hands.kt +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -16,6 +16,7 @@ package com.example.xr.arcore +import android.app.Activity import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope import androidx.xr.arcore.Hand @@ -28,8 +29,10 @@ import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Vector3 +import androidx.xr.runtime.math.toRadians import androidx.xr.scenecore.GltfModelEntity import androidx.xr.scenecore.scene +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted @@ -65,6 +68,16 @@ fun ComponentActivity.collectHands(session: Session) { } } +fun secondaryHandDetection(activity: Activity, session: Session) { + fun detectGesture(handState: Flow) {} + // [START androidxr_arcore_hand_handedness] + val handedness = Hand.getHandedness(activity.contentResolver) + val secondaryHand = if (handedness == Hand.Handedness.LEFT) Hand.right(session) else Hand.left(session) + val handState = secondaryHand?.state ?: return + detectGesture(handState) + // [END androidxr_arcore_hand_handedness] +} + fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { val session: Session = null!! val palmEntity: GltfModelEntity = null!! @@ -106,3 +119,27 @@ fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { indexFingerEntity.setPose(Pose(position, rotation)) // [END androidxr_arcore_hand_entityAtIndexFingerTip] } + +private fun detectPinch(session: Session, handState: Hand.State): Boolean { + // [START androidxr_arcore_hand_pinch_gesture] + val thumbTip = handState.handJoints[HandJointType.THUMB_TIP] ?: return false + val thumbTipPose = session.scene.perceptionSpace.transformPoseTo(thumbTip, session.scene.activitySpace) + val indexTip = handState.handJoints[HandJointType.INDEX_TIP] ?: return false + val indexTipPose = session.scene.perceptionSpace.transformPoseTo(indexTip, session.scene.activitySpace) + return Vector3.distance(thumbTipPose.translation, indexTipPose.translation) < 0.05 + // [END androidxr_arcore_hand_pinch_gesture] +} + +private fun detectStop(session: Session, handState: Hand.State): Boolean { + // [START androidxr_arcore_hand_stop_gesture] + val threshold = toRadians(angleInDegrees = 30f) + fun pointingInSameDirection(joint1: HandJointType, joint2: HandJointType): Boolean { + val forward1 = handState.handJoints[joint1]?.forward ?: return false + val forward2 = handState.handJoints[joint2]?.forward ?: return false + return Vector3.angleBetween(forward1, forward2) < threshold + } + return pointingInSameDirection(HandJointType.INDEX_PROXIMAL, HandJointType.INDEX_TIP) && + pointingInSameDirection(HandJointType.MIDDLE_PROXIMAL, HandJointType.MIDDLE_TIP) && + pointingInSameDirection(HandJointType.RING_PROXIMAL, HandJointType.RING_TIP) + // [END androidxr_arcore_hand_stop_gesture] +} From 7f4c676bb2c3b93491f25e01247115790ae1ea2f Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 23 May 2025 13:31:59 -0700 Subject: [PATCH 017/120] Change XR SceneViewer intent setup (#531) * Change SceneViewer intent setup --- .../main/java/com/example/xr/scenecore/GltfEntity.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt index 586f53fb3..c7181e2f3 100644 --- a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt +++ b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt @@ -16,6 +16,7 @@ package com.example.xr.scenecore +import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import androidx.activity.ComponentActivity @@ -51,14 +52,18 @@ private fun animateEntity(gltfEntity: GltfModelEntity) { private fun ComponentActivity.startSceneViewer() { // [START androidxr_scenecore_sceneviewer] val url = - "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/FlightHelmet/glTF/FlightHelmet.gltf" + "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Avocado/glTF/Avocado.gltf" val sceneViewerIntent = Intent(Intent.ACTION_VIEW) val intentUri = Uri.parse("https://arvr.google.com/scene-viewer/1.2") .buildUpon() .appendQueryParameter("file", url) .build() - sceneViewerIntent.setDataAndType(intentUri, "model/gltf-binary") - startActivity(sceneViewerIntent) + sceneViewerIntent.setData(intentUri) + try { + startActivity(sceneViewerIntent) + } catch (e: ActivityNotFoundException) { + // There is no activity that could handle the intent. + } // [END androidxr_scenecore_sceneviewer] } From 7230f05989e37fceae197c38f7aecef08bf5d2f6 Mon Sep 17 00:00:00 2001 From: "Ian G. Clifton" <1033551+IanGClifton@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:37:01 -0700 Subject: [PATCH 018/120] Fix for #533 animated CircleNode modifier (#534) This addresses https://github.com/android/snippets/issues/533 by recreating the Animatable when the node is attached. --- .../compose/snippets/modifiers/CustomModifierSnippets.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt index 7b778ef25..650c4beb8 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt @@ -18,6 +18,7 @@ package com.example.compose.snippets.modifiers import android.annotation.SuppressLint import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloatAsState @@ -259,7 +260,7 @@ class ScrollableNode : object CustomModifierSnippets14 { // [START android_compose_custom_modifiers_14] class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { - private val alpha = Animatable(1f) + private lateinit var alpha: Animatable override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) @@ -267,6 +268,7 @@ object CustomModifierSnippets14 { } override fun onAttach() { + alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, From f34038dfdd0d371a48427c2e38dbbbd8733f137d Mon Sep 17 00:00:00 2001 From: amyZepp <134542280+amyZepp@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:47:58 -0700 Subject: [PATCH 019/120] updating setSubspaceContent snippet (#543) * updating setSubspaceContent snippet * Update Activity --------- Co-authored-by: Dereck Bridie --- xr/src/main/java/com/example/xr/compose/Views.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/xr/src/main/java/com/example/xr/compose/Views.kt b/xr/src/main/java/com/example/xr/compose/Views.kt index 3f91ef2ad..4fc693828 100644 --- a/xr/src/main/java/com/example/xr/compose/Views.kt +++ b/xr/src/main/java/com/example/xr/compose/Views.kt @@ -22,12 +22,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment -import androidx.xr.compose.platform.setSubspaceContent +import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.depth @@ -44,10 +45,12 @@ private class ActivityWithSubspaceContent : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // [START androidxr_compose_ActivityWithSubspaceContent] - setSubspaceContent { - SpatialPanel( - modifier = SubspaceModifier.height(500.dp).width(500.dp).depth(25.dp) - ) { MyCustomView(this) } + setContent { + Subspace { + SpatialPanel( + modifier = SubspaceModifier.height(500.dp).width(500.dp).depth(25.dp) + ) { MyCustomView(this@ActivityWithSubspaceContent) } + } } // [END androidxr_compose_ActivityWithSubspaceContent] } From b198bf04418ebd635937c0147eecd290350d98d1 Mon Sep 17 00:00:00 2001 From: Ash <83780687+ashnohe@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:09:19 -0700 Subject: [PATCH 020/120] Update android_system_bar_protection_kotlin to change alpha (#544) * Update android_system_bar_protection_kotlin to change alpha * show less code in DAC * use Color.rgb instead --- views/src/main/java/insets/SystemBarProtectionSnippet.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views/src/main/java/insets/SystemBarProtectionSnippet.kt b/views/src/main/java/insets/SystemBarProtectionSnippet.kt index d4774fd2f..bd44cd4c9 100644 --- a/views/src/main/java/insets/SystemBarProtectionSnippet.kt +++ b/views/src/main/java/insets/SystemBarProtectionSnippet.kt @@ -51,18 +51,18 @@ class SystemBarProtectionSnippet : AppCompatActivity() { insets } - // [START android_system_bar_protection_kotlin] val red = 52 val green = 168 val blue = 83 + val paneBackgroundColor = Color.rgb(red, green, blue) + // [START android_system_bar_protection_kotlin] findViewById(R.id.list_protection) .setProtections( listOf( GradientProtection( WindowInsetsCompat.Side.TOP, // Ideally, this is the pane's background color - // alpha = 204 for an 80% gradient - Color.argb(204, red, green, blue) + paneBackgroundColor ) ) ) From d0ef8d01ea9c6d41a965a13b4bfa49cf2bcd5b55 Mon Sep 17 00:00:00 2001 From: Roberto Orgiu Date: Wed, 18 Jun 2025 10:33:02 +0200 Subject: [PATCH 021/120] Fix Drag&Drop code to also support external apps (#546) * Fix code to also support external apps * Sort chronologically the snippets * Move the Activity to be used without the nullability check --------- Co-authored-by: Rob Orgiu --- .../draganddrop/DragAndDropSnippets.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt index 6314eca3a..cc2bbdf2d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt @@ -20,6 +20,7 @@ import android.content.ClipData import android.content.ClipDescription import android.os.Build import android.view.View +import androidx.activity.compose.LocalActivity import androidx.annotation.RequiresApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.draganddrop.dragAndDropSource @@ -31,6 +32,7 @@ import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.draganddrop.mimeTypes +import androidx.compose.ui.draganddrop.toAndroidDragEvent @RequiresApi(Build.VERSION_CODES.N) @OptIn(ExperimentalFoundationApi::class) @@ -71,6 +73,22 @@ private fun DragAndDropSnippet() { } // [END android_compose_drag_and_drop_4] + LocalActivity.current?.let { activity -> + // [START android_compose_drag_and_drop_7] + val externalAppCallback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + val permission = + activity.requestDragAndDropPermissions(event.toAndroidDragEvent()) + // Parse received data + permission?.release() + return true + } + } + } + // [END android_compose_drag_and_drop_7] + } + // [START android_compose_drag_and_drop_5] Modifier.dragAndDropTarget( shouldStartDragAndDrop = { event -> From 3fbcdcb7ecea2165c5e1905557f34e4752d6320a Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 18 Jun 2025 13:54:55 +0100 Subject: [PATCH 022/120] Add tile interaction snippets (#547) For use on https://developer.android.com/training/wearables/tiles/interactions --- wear/src/main/AndroidManifest.xml | 94 ++++++ .../wear/snippets/m3/tile/Interaction.kt | 272 ++++++++++++++++++ .../wear/snippets/m3/tile/TileActivity.kt | 184 ++++++++++++ wear/src/main/res/values/strings.xml | 1 + 4 files changed, 551 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt create mode 100644 wear/src/main/java/com/example/wear/snippets/m3/tile/TileActivity.kt 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 From a37e65129e087db5aeea808411ed1d2653a22b69 Mon Sep 17 00:00:00 2001 From: Roberto Orgiu Date: Wed, 18 Jun 2025 18:09:33 +0200 Subject: [PATCH 023/120] Add comment to clarify that both callbacks can be used (#548) * Add comment to clarify that both callbacks can be used * Apply Spotless --------- Co-authored-by: Rob Orgiu Co-authored-by: tiwiz <2534841+tiwiz@users.noreply.github.com> --- .../example/compose/snippets/draganddrop/DragAndDropSnippets.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt index cc2bbdf2d..12542079e 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt @@ -93,7 +93,7 @@ private fun DragAndDropSnippet() { Modifier.dragAndDropTarget( shouldStartDragAndDrop = { event -> event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN) - }, target = callback + }, target = callback // or externalAppCallback ) // [END android_compose_drag_and_drop_5] From 88d1bd65fe90dec3eb1722bab22e38d473179409 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 13:49:01 +0100 Subject: [PATCH 024/120] Add snippets for Android Wear's Always On doc (#549) --- gradle/libs.versions.toml | 6 +- wear/build.gradle.kts | 12 +- wear/src/main/AndroidManifest.xml | 23 + .../snippets/alwayson/AlwaysOnActivity.kt | 186 +++++ .../wear/snippets/alwayson/AlwaysOnService.kt | 145 ++++ wear/src/main/res/drawable/animated_walk.xml | 682 ++++++++++++++++++ wear/src/main/res/drawable/ic_walk.xml | 16 + 7 files changed, 1067 insertions(+), 3 deletions(-) create mode 100644 wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt create mode 100644 wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt create mode 100644 wear/src/main/res/drawable/animated_walk.xml create mode 100644 wear/src/main/res/drawable/ic_walk.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index babb910f0..f705a6920 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" kotlinxSerializationJson = "1.8.1" ksp = "2.1.20-2.0.1" +lifecycleService = "2.9.1" maps-compose = "6.6.0" material = "1.13.0-alpha13" material3-adaptive = "1.1.0" @@ -73,6 +74,7 @@ wear = "1.3.0" wearComposeFoundation = "1.5.0-beta01" wearComposeMaterial = "1.5.0-beta01" wearComposeMaterial3 = "1.5.0-beta01" +wearOngoing = "1.0.0" wearToolingPreview = "1.0.0" webkit = "1.13.0" @@ -126,6 +128,7 @@ androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.1" androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } +androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleService" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } @@ -149,6 +152,7 @@ androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } +androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } @@ -189,7 +193,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.21" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 888f3d434..eb7c66445 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -6,12 +6,12 @@ plugins { android { namespace = "com.example.wear" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.example.wear" minSdk = 26 - targetSdk = 33 + targetSdk = 36 versionCode = 1 versionName = "1.0" vectorDrawables { @@ -46,9 +46,13 @@ android { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + kotlinOptions { + jvmTarget = "17" + } } dependencies { + implementation(libs.androidx.core.ktx) val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) androidTestImplementation(composeBom) @@ -57,6 +61,8 @@ dependencies { implementation(libs.play.services.wearable) implementation(libs.androidx.tiles) implementation(libs.androidx.wear) + implementation(libs.androidx.wear.ongoing) + implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.protolayout) implementation(libs.androidx.protolayout.material) implementation(libs.androidx.protolayout.material3) @@ -70,6 +76,7 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.fragment.ktx) implementation(libs.wear.compose.material) implementation(libs.wear.compose.material3) implementation(libs.compose.foundation) @@ -80,6 +87,7 @@ dependencies { implementation(libs.androidx.material.icons.core) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.junit) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index c7209c3a7..770b6d956 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + @@ -163,6 +166,26 @@ android:resource="@drawable/tile_preview" /> + + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt new file mode 100644 index 000000000..17b18d8af --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt @@ -0,0 +1,186 @@ +/* + * 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.alwayson + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.SystemClock +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.SwitchButton +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.dynamicColorScheme +import androidx.wear.tooling.preview.devices.WearDevices +import com.google.android.horologist.compose.ambient.AmbientAware +import com.google.android.horologist.compose.ambient.AmbientState +import kotlinx.coroutines.delay + +private const val TAG = "AlwaysOnActivity" + +class AlwaysOnActivity : ComponentActivity() { + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + Log.d(TAG, "POST_NOTIFICATIONS permission granted") + } else { + Log.w(TAG, "POST_NOTIFICATIONS permission denied") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate: Activity created") + + setTheme(android.R.style.Theme_DeviceDefault) + + // Check and request notification permission + checkAndRequestNotificationPermission() + + setContent { WearApp() } + } + + private fun checkAndRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED -> { + Log.d(TAG, "POST_NOTIFICATIONS permission already granted") + } + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { + Log.d(TAG, "Should show permission rationale") + // You could show a dialog here explaining why the permission is needed + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + else -> { + Log.d(TAG, "Requesting POST_NOTIFICATIONS permission") + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + } +} + +@Composable +// [START android_wear_ongoing_activity_elapsedtime] +fun ElapsedTime(ambientState: AmbientState) { + // [START_EXCLUDE] + val startTimeMs = rememberSaveable { SystemClock.elapsedRealtime() } + + val elapsedMs by + produceState(initialValue = 0L, key1 = startTimeMs) { + while (true) { // time doesn't stop! + value = SystemClock.elapsedRealtime() - startTimeMs + // In ambient mode, update every minute instead of every second + val updateInterval = if (ambientState.isAmbient) 60_000L else 1_000L + delay(updateInterval - (value % updateInterval)) + } + } + + val totalSeconds = elapsedMs / 1_000L + val minutes = totalSeconds / 60 + val seconds = totalSeconds % 60 + + // [END_EXCLUDE] + val timeText = + if (ambientState.isAmbient) { + // Show "mm:--" format in ambient mode + "%02d:--".format(minutes) + } else { + // Show full "mm:ss" format in interactive mode + "%02d:%02d".format(minutes, seconds) + } + + Text(text = timeText, style = MaterialTheme.typography.numeralMedium) +} +// [END android_wear_ongoing_activity_elapsedtime] + +@Preview( + device = WearDevices.LARGE_ROUND, + backgroundColor = 0xff000000, + showBackground = true, + group = "Devices - Large Round", + showSystemUi = true, +) +@Composable +fun WearApp() { + val context = LocalContext.current + var isOngoingActivity by rememberSaveable { mutableStateOf(AlwaysOnService.isRunning) } + MaterialTheme( + colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme + ) { + // [START android_wear_ongoing_activity_ambientaware] + AmbientAware { ambientState -> + // [START_EXCLUDE] + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + // [END_EXCLUDE] + ElapsedTime(ambientState = ambientState) + // [START_EXCLUDE] + Spacer(modifier = Modifier.height(8.dp)) + SwitchButton( + checked = isOngoingActivity, + onCheckedChange = { newState -> + Log.d(TAG, "Switch button changed: $newState") + isOngoingActivity = newState + + if (newState) { + Log.d(TAG, "Starting AlwaysOnService") + AlwaysOnService.startService(context) + } else { + Log.d(TAG, "Stopping AlwaysOnService") + AlwaysOnService.stopService(context) + } + }, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + text = "Ongoing Activity", + style = MaterialTheme.typography.bodyExtraSmall, + ) + } + } + } + // [END_EXCLUDE] + } + // [END android_wear_ongoing_activity_ambientaware] + } +} diff --git a/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt new file mode 100644 index 000000000..59ed0f8af --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt @@ -0,0 +1,145 @@ +/* + * 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.alwayson + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.content.getSystemService +import androidx.lifecycle.LifecycleService +import androidx.wear.ongoing.OngoingActivity +import androidx.wear.ongoing.Status +import com.example.wear.R + +class AlwaysOnService : LifecycleService() { + + private val notificationManager by lazy { getSystemService() } + + companion object { + private const val TAG = "AlwaysOnService" + private const val NOTIFICATION_ID = 1001 + private const val CHANNEL_ID = "always_on_service_channel" + private const val CHANNEL_NAME = "Always On Service" + @Volatile + var isRunning = false + private set + + fun startService(context: Context) { + Log.d(TAG, "Starting AlwaysOnService") + val intent = Intent(context, AlwaysOnService::class.java) + context.startForegroundService(intent) + } + + fun stopService(context: Context) { + Log.d(TAG, "Stopping AlwaysOnService") + context.stopService(Intent(context, AlwaysOnService::class.java)) + } + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "onCreate: Service created") + isRunning = true + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + Log.d(TAG, "onStartCommand: Service started with startId: $startId") + + // Create and start foreground notification + val notification = createNotification() + startForeground(NOTIFICATION_ID, notification) + + Log.d(TAG, "onStartCommand: Service is now running as foreground service") + + return START_STICKY + } + + override fun onDestroy() { + Log.d(TAG, "onDestroy: Service destroyed") + isRunning = false + super.onDestroy() + } + + private fun createNotificationChannel() { + val channel = + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + .apply { + description = "Always On Service notification channel" + setShowBadge(false) + } + + notificationManager?.createNotificationChannel(channel) + Log.d(TAG, "createNotificationChannel: Notification channel created") + } + + // [START android_wear_ongoing_activity_create_notification] + private fun createNotification(): Notification { + val activityIntent = + Intent(this, AlwaysOnActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val pendingIntent = + PendingIntent.getActivity( + this, + 0, + activityIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val notificationBuilder = + NotificationCompat.Builder(this, CHANNEL_ID) + // ... + // [START_EXCLUDE] + .setContentTitle("Always On Service") + .setContentText("Service is running in background") + .setSmallIcon(R.drawable.animated_walk) + .setContentIntent(pendingIntent) + .setCategory(NotificationCompat.CATEGORY_STOPWATCH) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + // [END_EXCLUDE] + .setOngoing(true) + + // [START_EXCLUDE] + // Create an Ongoing Activity + val ongoingActivityStatus = Status.Builder().addTemplate("Stopwatch running").build() + // [END_EXCLUDE] + + val ongoingActivity = + OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder) + // ... + // [START_EXCLUDE] + .setStaticIcon(R.drawable.ic_walk) + .setAnimatedIcon(R.drawable.animated_walk) + .setStatus(ongoingActivityStatus) + // [END_EXCLUDE] + .setTouchIntent(pendingIntent) + .build() + + ongoingActivity.apply(applicationContext) + + return notificationBuilder.build() + } + // [END android_wear_ongoing_activity_create_notification] +} diff --git a/wear/src/main/res/drawable/animated_walk.xml b/wear/src/main/res/drawable/animated_walk.xml new file mode 100644 index 000000000..e94991e07 --- /dev/null +++ b/wear/src/main/res/drawable/animated_walk.xml @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wear/src/main/res/drawable/ic_walk.xml b/wear/src/main/res/drawable/ic_walk.xml new file mode 100644 index 000000000..6c226e943 --- /dev/null +++ b/wear/src/main/res/drawable/ic_walk.xml @@ -0,0 +1,16 @@ + + + + + From 592ac880e3ec729415a21fa54dfde99689de663b Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 20 Jun 2025 16:43:34 +0100 Subject: [PATCH 025/120] Add android_wear_m3_interaction_loadaction_request tag (#551) --- .../com/example/wear/snippets/m3/tile/Interaction.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 index 7ef43dc6f..29d63d219 100644 --- 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 @@ -174,20 +174,19 @@ class InteractionDeepLink : TileService() { class InteractionLoadAction : BaseTileService() { + // [START android_wear_m3_interaction_loadaction_request] 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")] + val name = this[stringAppDataKey("name")] + val age = this[intAppDataKey("age")] } + // [START_EXCLUDE] return Futures.immediateFuture( Tile.Builder() .setResourcesVersion(RESOURCES_VERSION) @@ -200,7 +199,9 @@ class InteractionLoadAction : BaseTileService() { ) .build() ) + // [END_EXCLUDE] } + // [END android_wear_m3_interaction_loadaction_request] override fun MaterialScope.tileLayout( requestParams: RequestBuilders.TileRequest From 5723c5a85f379c8eb43f4c77d1654cec7d834084 Mon Sep 17 00:00:00 2001 From: compose-devrel-github-bot <118755852+compose-devrel-github-bot@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:53:59 +0100 Subject: [PATCH 026/120] =?UTF-8?q?=F0=9F=A4=96=20Update=20Dependencies=20?= =?UTF-8?q?(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 52 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f705a6920..207fbcbf6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,12 @@ accompanist = "0.36.0" activityKtx = "1.10.1" android-googleid = "1.1.1" -androidGradlePlugin = "8.10.0" +androidGradlePlugin = "8.10.1" androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2025.05.00" +androidx-compose-bom = "2025.06.01" androidx-compose-ui-test = "1.7.0-alpha08" -androidx-compose-ui-test-junit4-accessibility = "1.9.0-alpha02" +androidx-compose-ui-test-junit4-accessibility = "1.9.0-beta01" androidx-constraintlayout = "2.2.1" androidx-constraintlayout-compose = "1.1.1" androidx-coordinator-layout = "1.3.0" @@ -15,10 +15,10 @@ androidx-corektx = "1.16.0" androidx-credentials = "1.5.0" androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" -androidx-fragment-ktx = "1.8.6" +androidx-fragment-ktx = "1.8.8" androidx-glance-appwidget = "1.1.1" -androidx-lifecycle-compose = "2.9.0" -androidx-lifecycle-runtime-compose = "2.9.0" +androidx-lifecycle-compose = "2.9.1" +androidx-lifecycle-runtime-compose = "2.9.1" androidx-navigation = "2.9.0" androidx-paging = "3.3.6" androidx-startup-runtime = "1.2.0" @@ -30,12 +30,16 @@ androidx-window-core = "1.5.0-alpha02" androidx-window-java = "1.5.0-alpha02" # @keep androidx-xr = "1.0.0-alpha03" +# @keep +androidx-xr-arcore = "1.0.0-alpha04" +androidx-xr-compose = "1.0.0-alpha04" +androidx-xr-scenecore = "1.0.0-alpha04" androidxHiltNavigationCompose = "1.2.0" -appcompat = "1.7.0" +appcompat = "1.7.1" coil = "2.7.0" # @keep compileSdk = "35" -compose-latest = "1.8.1" +compose-latest = "1.8.3" composeUiTooling = "1.4.1" coreSplashscreen = "1.0.1" coroutines = "1.10.2" @@ -44,39 +48,35 @@ google-maps = "19.2.0" gradle-versions = "0.52.0" guava = "33.4.8-jre" hilt = "2.56.2" -horologist = "0.7.10-alpha" +horologist = "0.7.14-beta" junit = "4.13.2" -kotlin = "2.1.20" +kotlin = "2.2.0" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" kotlinxSerializationJson = "1.8.1" -ksp = "2.1.20-2.0.1" +ksp = "2.1.21-2.0.2" lifecycleService = "2.9.1" maps-compose = "6.6.0" -material = "1.13.0-alpha13" +material = "1.14.0-alpha02" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" -media3 = "1.6.1" +media3 = "1.7.1" # @keep minSdk = "21" okHttp = "4.12.0" playServicesWearable = "19.0.0" -protolayout = "1.3.0-beta02" +protolayout = "1.3.0" recyclerview = "1.4.0" -# @keep -androidx-xr-arcore = "1.0.0-alpha04" -androidx-xr-scenecore = "1.0.0-alpha04" -androidx-xr-compose = "1.0.0-alpha04" targetSdk = "34" -tiles = "1.5.0-beta01" +tiles = "1.5.0" version-catalog-update = "1.0.0" wear = "1.3.0" -wearComposeFoundation = "1.5.0-beta01" -wearComposeMaterial = "1.5.0-beta01" -wearComposeMaterial3 = "1.5.0-beta01" +wearComposeFoundation = "1.5.0-beta04" +wearComposeMaterial = "1.5.0-beta04" +wearComposeMaterial3 = "1.5.0-beta04" wearOngoing = "1.0.0" wearToolingPreview = "1.0.0" -webkit = "1.13.0" +webkit = "1.14.0" [libraries] accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3" @@ -158,14 +158,13 @@ androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" } androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window-java" } -androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.1" +androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.2" androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr-arcore" } androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr-compose" } androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr-scenecore" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } -wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "composeUiTooling" } glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } google-android-material = { module = "com.google.android.material:material", version.ref = "material" } @@ -185,6 +184,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } +wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } [plugins] @@ -193,7 +193,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.21" } +kotlin-android = "org.jetbrains.kotlin.android:2.2.0" kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } From 81b5d862e9b9d4db096cb640f5ac529770c85ac8 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 24 Jun 2025 19:07:30 +0100 Subject: [PATCH 027/120] Move END tag to correct location (#553) --- .../java/com/example/wear/snippets/m3/tile/TileActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 314898c39..e9d4771f0 100644 --- 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 @@ -56,10 +56,9 @@ class TileActivity : ComponentActivity() { setContent { MainContent() } // [END_EXCLUDE] } + // [END android_wear_m3_interactions_launchaction_activity] } -// [END android_wear_m3_interactions_launchaction_activity] - @Composable fun MainContent() { // [START android_wear_m3_interaction_deeplink_activity] From cafc3dd188ba79542224e1209ee29ebbef5728da Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 25 Jun 2025 16:11:17 +0100 Subject: [PATCH 028/120] Snippets for some Wear OS Tile pages (#554) --- .../example/wear/snippets/tile/Animations.kt | 312 ++++++++++++++++++ .../com/example/wear/snippets/tile/Tile.kt | 151 ++++++++- 2 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 wear/src/main/java/com/example/wear/snippets/tile/Animations.kt diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt new file mode 100644 index 000000000..b941cb53d --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt @@ -0,0 +1,312 @@ +/* + * 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.tile + +import android.annotation.SuppressLint +import androidx.annotation.OptIn +import androidx.wear.protolayout.DeviceParametersBuilders +import androidx.wear.protolayout.DimensionBuilders.degrees +import androidx.wear.protolayout.DimensionBuilders.dp +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.LayoutElementBuilders.Arc +import androidx.wear.protolayout.LayoutElementBuilders.ArcLine +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.ModifiersBuilders.AnimatedVisibility +import androidx.wear.protolayout.ModifiersBuilders.DefaultContentTransitions +import androidx.wear.protolayout.ModifiersBuilders.Modifiers +import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.TypeBuilders.FloatProp +import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters +import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec +import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat +import androidx.wear.protolayout.expression.ProtoLayoutExperimental +import androidx.wear.protolayout.material.CircularProgressIndicator +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.layouts.EdgeContentLayout +import androidx.wear.tiles.RequestBuilders +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 + +private const val RESOURCES_VERSION = "1" +private const val someTileText = "Hello" +private val deviceParameters = DeviceParametersBuilders.DeviceParameters.Builder().build() + +private fun getTileTextToShow(): String { + return "Some text" +} + +/** Demonstrates a sweep transition animation on a [CircularProgressIndicator]. */ +class AnimationSweepTransition : TileService() { + // [START android_wear_tile_animations_sweep_transition] + private var startValue = 15f + private var endValue = 105f + private val animationDurationInMillis = 2000L // 2 seconds + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + val circularProgressIndicator = + CircularProgressIndicator.Builder() + .setProgress( + FloatProp.Builder(/* static value */ 0.25f) + .setDynamicValue( + // Or you can use some other dynamic object, for example + // from the platform and then at the end of expression + // add animate(). + DynamicFloat.animate( + startValue, + endValue, + AnimationSpec.Builder() + .setAnimationParameters( + AnimationParameters.Builder() + .setDurationMillis(animationDurationInMillis) + .build() + ) + .build(), + ) + ) + .build() + ) + .build() + + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(Timeline.fromLayoutElement(circularProgressIndicator)) + .build() + ) + } + // [END android_wear_tile_animations_sweep_transition] +} + +/** Demonstrates setting the growth direction of an [Arc] and [ArcLine]. */ +@SuppressLint("RestrictedApi") +class AnimationArcDirection : TileService() { + // [START android_wear_tile_animations_set_arc_direction] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + EdgeContentLayout.Builder(deviceParameters) + .setResponsiveContentInsetEnabled(true) + .setEdgeContent( + Arc.Builder() + // Arc should always grow clockwise. + .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE) + .addContent( + ArcLine.Builder() + // Set color, length, thickness, and more. + // Arc should always grow clockwise. + .setArcDirection( + LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_set_arc_direction] +} + +/** Demonstrates smooth fade-in and fade-out transitions. */ +class AnimationFadeTransition : TileService() { + + @OptIn(ProtoLayoutExperimental::class) + // [START android_wear_tile_animations_fade] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + // Assumes that you've defined a custom helper method called + // getTileTextToShow(). + val tileText = getTileTextToShow() + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, tileText) + .setModifiers( + Modifiers.Builder() + .setContentUpdateAnimation( + AnimatedVisibility.Builder() + .setEnterTransition(DefaultContentTransitions.fadeIn()) + .setExitTransition(DefaultContentTransitions.fadeOut()) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_fade] +} + +/** Demonstrates smooth slide-in and slide-out transitions. */ +class AnimationSlideTransition : TileService() { + @OptIn(ProtoLayoutExperimental::class) + // [START android_wear_tile_animations_slide] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + // Assumes that you've defined a custom helper method called + // getTileTextToShow(). + val tileText = getTileTextToShow() + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, tileText) + .setModifiers( + Modifiers.Builder() + .setContentUpdateAnimation( + AnimatedVisibility.Builder() + .setEnterTransition( + DefaultContentTransitions.slideIn( + ModifiersBuilders.SLIDE_DIRECTION_LEFT_TO_RIGHT + ) + ) + .setExitTransition( + DefaultContentTransitions.slideOut( + ModifiersBuilders.SLIDE_DIRECTION_LEFT_TO_RIGHT + ) + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_slide] +} + +/** Demonstrates a rotation transformation. */ +class AnimationRotation : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_rotation] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Set the pivot point 50 dp from the left edge + // and 100 dp from the top edge of the screen. + .setPivotX(dp(50f)) + .setPivotY(dp(100f)) + // Rotate the element 45 degrees clockwise. + .setRotation(degrees(45f)) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_rotation] + } +} + +/** Demonstrates a scaling transformation. */ +class AnimationScaling : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_scaling] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Set the pivot point 50 dp from the left edge + // and 100 dp from the top edge of the screen. + .setPivotX(dp(50f)) + .setPivotY(dp(100f)) + // Shrink the element by a scale factor + // of 0.5 horizontally and 0.75 vertically. + .setScaleX(FloatProp.Builder(0.5f).build()) + .setScaleY( + FloatProp.Builder(0.75f).build() + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_scaling] + } +} + +/** Demonstrates a geometric translation. */ +class AnimationGeometricTranslation : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_geometric_translation] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Translate (move) the element 60 dp to the right + // and 80 dp down. + .setTranslationX(dp(60f)) + .setTranslationY(dp(80f)) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_geometric_translation] + } +} diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt index 58cbaa758..9e2a9508e 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -16,9 +16,18 @@ package com.example.wear.snippets.tile +import android.Manifest +import android.content.Context +import androidx.annotation.RequiresPermission import androidx.wear.protolayout.ColorBuilders.argb +import androidx.wear.protolayout.DimensionBuilders +import androidx.wear.protolayout.LayoutElementBuilders import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.TypeBuilders +import androidx.wear.protolayout.expression.DynamicBuilders +import androidx.wear.protolayout.expression.PlatformHealthSources import androidx.wear.protolayout.material.Text import androidx.wear.protolayout.material.Typography import androidx.wear.tiles.RequestBuilders @@ -26,6 +35,7 @@ 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 private const val RESOURCES_VERSION = "1" @@ -48,10 +58,145 @@ class MyTileService : TileService() { ) override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture(Resources.Builder().setVersion(RESOURCES_VERSION).build()) +} + +// [END android_wear_tile_mytileservice] + +fun simpleLayout(context: Context) = + Text.Builder(context, "Hello World!") + .setTypography(Typography.TYPOGRAPHY_BODY1) + .setColor(argb(0xFFFFFFFF.toInt())) + .build() + +class PeriodicUpdatesSingleEntry : TileService() { + // [START android_wear_tile_periodic_single_entry] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + val tile = + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + // We add a single timeline entry when our layout is fixed, and + // we don't know in advance when its contents might change. + .setTileTimeline(Timeline.fromLayoutElement(simpleLayout(this))) + .build() + return Futures.immediateFuture(tile) + } + // [END android_wear_tile_periodic_single_entry] +} + +fun emptySpacer(): LayoutElementBuilders.LayoutElement { + return LayoutElementBuilders.Spacer.Builder() + .setWidth(DimensionBuilders.dp(0f)) + .setHeight(DimensionBuilders.dp(0f)) + .build() +} + +fun getNoMeetingsLayout(): LayoutElementBuilders.Layout { + return LayoutElementBuilders.Layout.Builder().setRoot(emptySpacer()).build() +} + +fun getMeetingLayout(meeting: Meeting): LayoutElementBuilders.Layout { + return LayoutElementBuilders.Layout.Builder().setRoot(emptySpacer()).build() +} + +data class Meeting(val name: String, val dateTimeMillis: Long) + +object MeetingsRepo { + fun getMeetings(): List { + val now = System.currentTimeMillis() + return listOf( + Meeting("Meeting 1", now + 1 * 60 * 60 * 1000), // 1 hour from now + Meeting("Meeting 2", now + 3 * 60 * 60 * 1000), // 3 hours from now + ) + } +} + +class PeriodicUpdatesTimebound : TileService() { + // [START android_wear_tile_periodic_timebound] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + val timeline = Timeline.Builder() + + // Add fallback "no meetings" entry + // Use the version of TimelineEntry that's in androidx.wear.protolayout. + timeline.addTimelineEntry( + TimelineBuilders.TimelineEntry.Builder().setLayout(getNoMeetingsLayout()).build() + ) + + // Retrieve a list of scheduled meetings + val meetings = MeetingsRepo.getMeetings() + // Add a timeline entry for each meeting + meetings.forEach { meeting -> + timeline.addTimelineEntry( + TimelineBuilders.TimelineEntry.Builder() + .setLayout(getMeetingLayout(meeting)) + .setValidity( + // The tile should disappear when the meeting begins + // Use the version of TimeInterval that's in + // androidx.wear.protolayout. + TimelineBuilders.TimeInterval.Builder() + .setEndMillis(meeting.dateTimeMillis) + .build() + ) + .build() + ) + } + + val tile = + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(timeline.build()) + .build() + return Futures.immediateFuture(tile) + } + // [END android_wear_tile_periodic_timebound] +} + +fun getWeatherLayout() = emptySpacer() + +class PeriodicUpdatesRefresh : TileService() { + // [START android_wear_tile_periodic_refresh] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture = Futures.immediateFuture( - Resources.Builder() - .setVersion(RESOURCES_VERSION) + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes + .setTileTimeline(Timeline.fromLayoutElement(getWeatherLayout())) .build() ) + // [END android_wear_tile_periodic_refresh] +} + +class DynamicHeartRate : TileService() { + @RequiresPermission(Manifest.permission.BODY_SENSORS) + // [START android_wear_tile_dynamic_heart_rate] + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder( + this, + TypeBuilders.StringProp.Builder("--") + .setDynamicValue( + PlatformHealthSources.heartRateBpm() + .format() + .concat(DynamicBuilders.DynamicString.constant(" bpm")) + ) + .build(), + TypeBuilders.StringLayoutConstraint.Builder("000").build(), + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_dynamic_heart_rate] } -// [END android_wear_tile_mytileservice] From c9e37f82b5a35c7c0944ba534a1b24fe2ce96cec Mon Sep 17 00:00:00 2001 From: garanj Date: Fri, 27 Jun 2025 12:55:50 +0100 Subject: [PATCH 029/120] Adds Watch Face Push validation library snippet (#555) * Adds Watch Face Push validation library snippet * Removed jar * Removes unnecessary statement --- gradle/libs.versions.toml | 2 + gradlew | 2 +- settings.gradle.kts | 8 ++- watchfacepush/validator/.gitignore | 1 + watchfacepush/validator/README.md | 1 + watchfacepush/validator/build.gradle.kts | 31 +++++++++ watchfacepush/validator/consumer-rules.pro | 0 watchfacepush/validator/proguard-rules.pro | 21 +++++++ .../main/kotlin/com/example/validator/Main.kt | 59 ++++++++++++++++++ .../src/main/resources/watchface.apk | Bin 0 -> 48096 bytes 10 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 watchfacepush/validator/.gitignore create mode 100644 watchfacepush/validator/README.md create mode 100644 watchfacepush/validator/build.gradle.kts create mode 100644 watchfacepush/validator/consumer-rules.pro create mode 100644 watchfacepush/validator/proguard-rules.pro create mode 100644 watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt create mode 100644 watchfacepush/validator/src/main/resources/watchface.apk diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 207fbcbf6..a9abd1b83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ protolayout = "1.3.0" recyclerview = "1.4.0" targetSdk = "34" tiles = "1.5.0" +validatorPush = "1.0.0-alpha03" version-catalog-update = "1.0.0" wear = "1.3.0" wearComposeFoundation = "1.5.0-beta04" @@ -184,6 +185,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } +validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" } wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } diff --git a/gradlew b/gradlew index 1aa94a426..983fb2e97 100755 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# * For validator: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d2212b63..eb485975e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,12 @@ dependencyResolutionManagement { println("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } } - + maven { + url = uri("https://jitpack.io") + content { + includeGroup("com.github.xgouchet") + } + } google() mavenCentral() } @@ -30,4 +35,5 @@ include( ":misc", ":identity:credentialmanager", ":xr", + ":watchfacepush:validator" ) diff --git a/watchfacepush/validator/.gitignore b/watchfacepush/validator/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/watchfacepush/validator/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/watchfacepush/validator/README.md b/watchfacepush/validator/README.md new file mode 100644 index 000000000..e11373aa7 --- /dev/null +++ b/watchfacepush/validator/README.md @@ -0,0 +1 @@ +This is a sample project that contains the code snippets seen on https://developer.android.com/training/wearables/watch-face-push diff --git a/watchfacepush/validator/build.gradle.kts b/watchfacepush/validator/build.gradle.kts new file mode 100644 index 000000000..66cb693bf --- /dev/null +++ b/watchfacepush/validator/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * 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 + * + * http://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. + */ + +group = "com.example.validator" +version = "1.0" + +plugins { + kotlin("jvm") + application +} + +application { + mainClass.set("com.example.validator.Main") +} + +dependencies { + implementation(libs.validator.push) +} \ No newline at end of file diff --git a/watchfacepush/validator/consumer-rules.pro b/watchfacepush/validator/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/watchfacepush/validator/proguard-rules.pro b/watchfacepush/validator/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/watchfacepush/validator/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt new file mode 100644 index 000000000..f046ebe09 --- /dev/null +++ b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt @@ -0,0 +1,59 @@ +package com.example.validator + +import com.google.android.wearable.watchface.validator.client.DwfValidatorFactory +import java.io.File +import java.io.FileOutputStream +import kotlin.system.exitProcess + +class Main { + companion object { + @JvmStatic + fun main(args: Array) { + println("Watch Face Push validator test program") + performValidation() + } + } +} + +private fun performValidation() { + val watchFaceFile = obtainTempWatchFaceFile() + val appPackageName = "com.example.validator" + + // [START android_examples_wfp_validation] + val validator = DwfValidatorFactory.create() + val result = validator.validate(watchFaceFile, appPackageName) + + if (result.failures().isEmpty()) { + val token = result.validationToken() + println("Validation token: $token") + + // Validation success - continue with the token + // ... + } else { + // There were failures, handle them accordingly - validation has failed. + result.failures().forEach { failure -> + println("FAILURE: ${failure.name()}: ${failure.failureMessage()}") + // ... + } + } + // [END android_examples_wfp_validation] +} + +private fun obtainTempWatchFaceFile(): File { + val resourceName = "watchface.apk" + + val inputStream = object {}.javaClass.classLoader.getResourceAsStream(resourceName) + + if (inputStream == null) { + println("Error: Cannot find resource '$resourceName'") + exitProcess(1) + } + + val tempFile = File.createTempFile("validator-", ".apk") + tempFile.deleteOnExit() + + FileOutputStream(tempFile).use { fos -> + inputStream.copyTo(fos) + } + return tempFile +} \ No newline at end of file diff --git a/watchfacepush/validator/src/main/resources/watchface.apk b/watchfacepush/validator/src/main/resources/watchface.apk new file mode 100644 index 0000000000000000000000000000000000000000..d9bd51c3fcfb16d283a38817df6ab07edb574505 GIT binary patch literal 48096 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 literal 0 HcmV?d00001 From a9464f2833c3e0cf71bce17138d1680bd071d804 Mon Sep 17 00:00:00 2001 From: garanj Date: Fri, 27 Jun 2025 13:33:10 +0100 Subject: [PATCH 030/120] Update build.yml to add Watch Face Push validation (#556) --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5124ef05..bca9bc586 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,3 +49,5 @@ jobs: run: ./gradlew :misc:build - name: Build XR snippets run: ./gradlew :xr:build + - name: Build Watch Face Push validation snippets + run: ./gradlew :watchfacepush:validator:run From 327c0acb5a22cc5ebbd54c47706655f12a938f8c Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:36:15 -0700 Subject: [PATCH 031/120] Typo fix - one import per line (#557) Per Java style guide, each statement should be on its own line. Mainly just doing this commit to test out my coding setup, though. --- .../com/example/snippets/ActivityEmbeddingJavaSnippets.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java b/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java index e3765998a..a0ed0595b 100644 --- a/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java +++ b/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java @@ -13,7 +13,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; -import androidx.annotation.RequiresApi;import androidx.appcompat.app.AppCompatActivity; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.startup.Initializer; import androidx.window.WindowSdkExtensions; From ef79bd25ed1b705546a47a611ba04201f28e65f2 Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Thu, 3 Jul 2025 01:33:00 +0900 Subject: [PATCH 032/120] Update webview url (#559) * Add WebView snippets * Updated glitch.me sites on Webview --- .../example/identity/credentialmanager/WebViewMainActivity.kt | 2 +- identity/credentialmanager/src/main/jsonSnippets.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt index a336754aa..f0f2a516b 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt @@ -26,7 +26,7 @@ class WebViewMainActivity : ComponentActivity() { settings.javaScriptEnabled = true // Test URL: - val url = "https://credman-web-test.glitch.me/" + val url = "https://passkeys-codelab.glitch.me/" val listenerSupported = WebViewFeature.isFeatureSupported( WebViewFeature.WEB_MESSAGE_LISTENER ) diff --git a/identity/credentialmanager/src/main/jsonSnippets.json b/identity/credentialmanager/src/main/jsonSnippets.json index 085fbc39b..9cbccd4e1 100644 --- a/identity/credentialmanager/src/main/jsonSnippets.json +++ b/identity/credentialmanager/src/main/jsonSnippets.json @@ -47,7 +47,7 @@ "allowCredentials": [], "timeout": 1800000, "userVerification": "required", - "rpId": "credential-manager-app-test.glitch.me" + "rpId": "https://passkeys-codelab.glitch.me/" } // [END android_identity_format_json_request_passkey] }, From 0c2331244604949866fb4735848645ec055e6ed7 Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Tue, 8 Jul 2025 20:55:25 +0900 Subject: [PATCH 033/120] Add additional credman webview snippet and autofill snippets (#561) --- .../PasskeyAndPasswordFunctions.kt | 36 +++++++++++++++++++ .../credentialmanager/WebViewMainActivity.kt | 9 +++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt index d8f3b8525..e2df067f9 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt @@ -130,6 +130,40 @@ class PasskeyAndPasswordFunctions ( } } + fun autofillImplementation( + requestJson: String + ) { + // [START android_identity_autofill_construct_request] + // Retrieves the user's saved password for your app. + val getPasswordOption = GetPasswordOption() + + // Get a passkey from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson + ) + + val getCredRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption) + ) + // [END android_identity_autofill_construct_request] + + runBlocking { + // [START android_identity_autofill_get_credential_api] + coroutineScope { + try { + val result = credentialManager.getCredential( + context = activityContext, // Use an activity-based context. + request = getCredRequest + ) + handleSignIn(result); + } catch (e: GetCredentialException) { + handleFailure(e); + } + } + // [END android_identity_autofill_get_credential_api] + } + } + // [START android_identity_launch_sign_in_flow_2] fun handleSignIn(result: GetCredentialResponse) { // Handle the successfully returned credential. @@ -239,6 +273,8 @@ class PasskeyAndPasswordFunctions ( } // [END android_identity_handle_create_passkey_failure] + fun handleFailure(e: GetCredentialException) { } + // [START android_identity_register_password] suspend fun registerPassword(username: String, password: String) { // Initialize a CreatePasswordRequest object. diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt index f0f2a516b..725ad7ff8 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt @@ -58,7 +58,9 @@ class WebViewMainActivity : ComponentActivity() { coroutineScope: CoroutineScope, credentialManagerHandler: CredentialManagerHandler ) { + // [START android_identity_create_webview_object] val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler) + val webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) @@ -66,12 +68,15 @@ class WebViewMainActivity : ComponentActivity() { } } + webView.webViewClient = webViewClient + // [END android_identity_create_webview_object] + + // [START android_identity_set_web] val rules = setOf("*") if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener) } - - webView.webViewClient = webViewClient + // [END android_identity_set_web] } } \ No newline at end of file From 80802cb8311a80fdaa4fa1f2097c8239f94a34fc Mon Sep 17 00:00:00 2001 From: Chiping Yeh Date: Thu, 10 Jul 2025 05:51:26 +0900 Subject: [PATCH 034/120] Add remaining autofill snippet with edittext (#562) --- identity/credentialmanager/build.gradle.kts | 1 + .../PasskeyAndPasswordFunctions.kt | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/identity/credentialmanager/build.gradle.kts b/identity/credentialmanager/build.gradle.kts index 0d1291dbd..b0a93839c 100644 --- a/identity/credentialmanager/build.gradle.kts +++ b/identity/credentialmanager/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.okhttp) implementation(libs.kotlin.coroutines.okhttp) implementation(libs.androidx.webkit) + implementation(libs.appcompat) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt index e2df067f9..f6dfe4485 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt @@ -22,6 +22,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatEditText import androidx.credentials.CreatePasswordRequest import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CredentialManager @@ -31,15 +32,16 @@ import androidx.credentials.GetCredentialResponse import androidx.credentials.GetPasswordOption import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PasswordCredential +import androidx.credentials.PendingGetCredentialRequest import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialCancellationException import androidx.credentials.exceptions.CreateCredentialCustomException import androidx.credentials.exceptions.CreateCredentialException import androidx.credentials.exceptions.CreateCredentialInterruptedException import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException -import androidx.credentials.exceptions.CreateCredentialUnknownException import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException +import androidx.credentials.pendingGetCredentialRequest import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import org.json.JSONObject @@ -162,6 +164,19 @@ class PasskeyAndPasswordFunctions ( } // [END android_identity_autofill_get_credential_api] } + + val usernameEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + val passwordEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + + // [START android_identity_autofill_enable_edit_text] + usernameEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest) { response -> handleSignIn(response) + } + + passwordEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest) { response -> handleSignIn(response) + } + // [END android_identity_autofill_enable_edit_text] } // [START android_identity_launch_sign_in_flow_2] From 33c4bc24591e6d4ada77b52e58b0ac9d3b26622f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Fri, 11 Jul 2025 10:18:38 +0200 Subject: [PATCH 035/120] Fix rethrowing cancellation exceptions (#563) * Fix missing import * Rethrow cancellation-exceptions --- .../compose/snippets/semantics/SemanticsSnippets.kt | 1 + .../sharedelement/CustomizeSharedElementsSnippets.kt | 3 ++- .../snippets/predictiveback/PredictiveBackSnippets.kt | 11 +++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt index 1b867edfa..89b577ea7 100644 --- a/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt @@ -16,6 +16,7 @@ package com.example.compose.snippets.semantics +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsProperties diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index d599e1586..079c68347 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -659,7 +659,7 @@ fun CustomPredictiveBackHandle() { // For each backEvent that comes in, we manually seekTo the reported back progress try { seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) - } catch (e: CancellationException) { + } catch (_: CancellationException) { // seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls // before the current seekTo finishes, in this case, we ignore the cancellation. } @@ -671,6 +671,7 @@ fun CustomPredictiveBackHandle() { // When the predictive back gesture is cancelled, we snap to the end state to ensure // it completes its seeking animation back to the currentState seekableTransitionState.snapTo(seekableTransitionState.currentState) + throw e } } val coroutineScope = rememberCoroutineScope() diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt index fb94e1186..f9999f67c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -110,7 +111,10 @@ private fun PredictiveBackHandlerBasicExample() { Box( modifier = Modifier - .fillMaxSize(boxScale) + .graphicsLayer { + scaleX = boxScale + scaleY = scaleX + } .background(Color.Blue) ) @@ -127,6 +131,7 @@ private fun PredictiveBackHandlerBasicExample() { } catch (e: CancellationException) { // code for cancellation boxScale = 1F + throw e } } // [END android_compose_predictivebackhandler_basic] @@ -180,8 +185,10 @@ private fun PredictiveBackHandlerManualProgress() { closeDrawer(velocityTracker.calculateVelocity().x) } catch (e: CancellationException) { openDrawer(velocityTracker.calculateVelocity().x) + throw e + } finally { + velocityTracker.resetTracking() } - velocityTracker.resetTracking() } // [END android_compose_predictivebackhandler_manualprogress] } From 341dbd4f9ecb1816da0f777385829b023893b6bf Mon Sep 17 00:00:00 2001 From: Edgar Arriaga Date: Wed, 16 Jul 2025 20:20:21 +0000 Subject: [PATCH 036/120] Add profiling manager snippets for how-to-capture DAC docs (#568) *Adding code snippets for how-to-capture section of ProfilingManager docs Co-authored-by: edgararriagag <211581399+edgararriagag@users.noreply.github.com> --- gradle/libs.versions.toml | 6 +- misc/build.gradle.kts | 1 + .../ProfilingManagerJavaSnippets.java | 73 ++++++++++++++++ .../ProfilingManagerKotlinSnippets.kt | 85 +++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java create mode 100644 misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9abd1b83..f7e2b5470 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,13 +62,14 @@ material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" media3 = "1.7.1" # @keep -minSdk = "21" +minSdk = "35" okHttp = "4.12.0" playServicesWearable = "19.0.0" protolayout = "1.3.0" recyclerview = "1.4.0" -targetSdk = "34" +targetSdk = "35" tiles = "1.5.0" +tracing = "1.3.0" validatorPush = "1.0.0-alpha03" version-catalog-update = "1.0.0" wear = "1.3.0" @@ -152,6 +153,7 @@ androidx-tiles-renderer = { module = "androidx.wear.tiles:tiles-renderer", versi androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version.ref = "tiles" } androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } +androidx-tracing = { group = "androidx.tracing", name = "tracing", version.ref = "tracing" } androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts index e5cc3cc1d..af5893d75 100644 --- a/misc/build.gradle.kts +++ b/misc/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.tracing) implementation(libs.hilt.android) implementation(libs.androidx.hilt.navigation.compose) diff --git a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java new file mode 100644 index 000000000..4dae4d353 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java @@ -0,0 +1,73 @@ +package com.example.snippets.profiling; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import java.util.function.Consumer; +import java.util.concurrent.Executor; +import android.os.ProfilingResult; +import java.util.concurrent.Executors; +import android.os.CancellationSignal; +import androidx.tracing.Trace; +import androidx.core.os.Profiling; +import androidx.core.os.SystemTraceRequestBuilder; +import androidx.core.os.BufferFillPolicy; + +public class ProfilingManagerJavaSnippets { + public class MainActivityJava extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sampleRecordSystemTrace(); + } + + // [START android_profiling_manager_record_system_trace_java] + void heavyOperation() { + // Computations you want to profile + } + + void sampleRecordSystemTrace() { + Executor mainExecutor = Executors.newSingleThreadExecutor(); + Consumer resultCallback = + new Consumer() { + @Override + public void accept(ProfilingResult profilingResult) { + if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) { + Log.d( + "ProfileTest", + "Received profiling result file=" + profilingResult.getResultFilePath()); + } else { + Log.e( + "ProfileTest", + "Profiling failed errorcode=" + + + profilingResult.getErrorCode() + + " errormsg=" + + profilingResult.getErrorMessage()); + } + } + }; + CancellationSignal stopSignal = new CancellationSignal(); + + SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder(); + requestBuilder.setCancellationSignal(stopSignal); + requestBuilder.setTag("FOO"); + requestBuilder.setDurationMs(60000); + requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER); + requestBuilder.setBufferSizeKb(20971520); + Profiling.requestProfiling(getApplicationContext(), requestBuilder.build(), mainExecutor, + resultCallback); + + // Wait some time for profiling to start. + + Trace.beginSection("MyApp:HeavyOperation"); + heavyOperation(); + Trace.endSection(); + + // Once the interesting code section is profiled, stop profile + stopSignal.cancel(); + } + // [END android_profiling_manager_record_system_trace_java] + } +} diff --git a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt new file mode 100644 index 000000000..f3f0ad99c --- /dev/null +++ b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt @@ -0,0 +1,85 @@ +/* + * 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.snippets.profiling + +import android.app.Activity +import android.os.Build +import android.os.Bundle +import android.os.CancellationSignal +import android.os.ProfilingResult +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.os.BufferFillPolicy +import androidx.core.os.SystemTraceRequestBuilder +import androidx.core.os.requestProfiling +import androidx.tracing.Trace +import java.util.concurrent.Executor +import java.util.function.Consumer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor + +class ProfilingManagerKotlinSnippets { + class MainActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sampleRecordSystemTrace() + } + + // [START android_profiling_manager_record_system_trace_kotlin] + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun sampleRecordSystemTrace() { + val mainExecutor: Executor = + Dispatchers.IO.asExecutor() // Your choice of executor for the callback to occur on. + val resultCallback = Consumer { profilingResult -> + if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) { + Log.d( + "ProfileTest", + "Received profiling result file=" + profilingResult.resultFilePath + ) + } else { + Log.e( + "ProfileTest", + "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage + ) + } + } + val stopSignal = CancellationSignal() + + val requestBuilder = SystemTraceRequestBuilder() + requestBuilder.setCancellationSignal(stopSignal) + requestBuilder.setTag("FOO") // Caller supplied tag for identification + requestBuilder.setDurationMs(60000) + requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER) + requestBuilder.setBufferSizeKb(20971520) + requestProfiling(applicationContext, requestBuilder.build(), mainExecutor, resultCallback) + + // Wait some time for profiling to start. + + Trace.beginSection("MyApp:HeavyOperation") + heavyOperation() + Trace.endSection() + + // Once the interesting code section is profiled, stop profile + stopSignal.cancel() + } + + fun heavyOperation() { + // Computations you want to profile + } + // [END android_profiling_manager_record_system_trace_kotlin] + } +} From b78993dc70859d0f9776f9ce4d73d4700c670515 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 31 Jul 2025 10:12:22 +0200 Subject: [PATCH 037/120] Update XR snippets to be compatible with alpha05 versions of the libraries (#571) * Migrate to snapshot 13623196 (alpha05) * Update to snapshot 13663278 * Update to snapshot 13681046 * Update to snapshot 13693757 * Remove guava dependency * Spotless apply * Update to snapshot 13740187 * Add snippets for xr DRM * Update to snapshot 13790637 * Manual unrestrict on 13790637. * Use ExoPlayer to detect compatibility * Fix duplicated libs in toml * Add snippets for HDR and Material improvements * Update to public alpha05 --------- Co-authored-by: azeppenfeld --- gradle/libs.versions.toml | 9 +- xr/build.gradle.kts | 5 +- .../java/com/example/xr/arcore/Anchors.kt | 10 +- .../main/java/com/example/xr/arcore/Hands.kt | 16 +-- .../main/java/com/example/xr/arcore/Planes.kt | 10 +- .../java/com/example/xr/compose/Orbiter.kt | 19 +-- .../example/xr/compose/SpatialElevation.kt | 2 +- .../xr/compose/SpatialExternalSurface.kt | 47 +++++++ .../main/java/com/example/xr/compose/Views.kt | 4 +- .../java/com/example/xr/compose/Volume.kt | 2 + .../com/example/xr/misc/ModeTransition.kt | 2 +- .../java/com/example/xr/runtime/Session.kt | 26 +--- .../java/com/example/xr/scenecore/Entities.kt | 82 +----------- .../com/example/xr/scenecore/Environments.kt | 38 +++--- .../com/example/xr/scenecore/GltfEntity.kt | 4 +- .../xr/scenecore/InteractableComponent.kt | 41 ++++++ .../example/xr/scenecore/MovableComponent.kt | 46 +++++++ .../xr/scenecore/ResizableComponent.kt | 43 +++++++ .../com/example/xr/scenecore/SpatialAudio.kt | 20 +++ .../xr/scenecore/SpatialCapabilities.kt | 2 +- .../com/example/xr/scenecore/SpatialVideo.kt | 120 ++++++++++++++++++ 21 files changed, 386 insertions(+), 162 deletions(-) create mode 100644 xr/src/main/java/com/example/xr/scenecore/InteractableComponent.kt create mode 100644 xr/src/main/java/com/example/xr/scenecore/MovableComponent.kt create mode 100644 xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7e2b5470..351974d06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,12 +28,9 @@ androidx-test-junit = "1.2.1" androidx-window = "1.5.0-alpha02" androidx-window-core = "1.5.0-alpha02" androidx-window-java = "1.5.0-alpha02" -# @keep -androidx-xr = "1.0.0-alpha03" -# @keep -androidx-xr-arcore = "1.0.0-alpha04" -androidx-xr-compose = "1.0.0-alpha04" -androidx-xr-scenecore = "1.0.0-alpha04" +androidx-xr-arcore = "1.0.0-alpha05" +androidx-xr-scenecore = "1.0.0-alpha05" +androidx-xr-compose = "1.0.0-alpha05" androidxHiltNavigationCompose = "1.2.0" appcompat = "1.7.1" coil = "2.7.0" diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index 74ad5bfe9..138383c8d 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -25,6 +25,9 @@ android { buildFeatures { compose = true } + lint { + disable += "RestrictedApi" + } } dependencies { @@ -33,8 +36,6 @@ dependencies { implementation(libs.androidx.xr.compose) implementation(libs.androidx.activity.ktx) - implementation(libs.guava) - implementation(libs.kotlinx.coroutines.guava) implementation(libs.androidx.media3.exoplayer) diff --git a/xr/src/main/java/com/example/xr/arcore/Anchors.kt b/xr/src/main/java/com/example/xr/arcore/Anchors.kt index cb4992095..56c7cb8d7 100644 --- a/xr/src/main/java/com/example/xr/arcore/Anchors.kt +++ b/xr/src/main/java/com/example/xr/arcore/Anchors.kt @@ -22,25 +22,23 @@ import androidx.xr.arcore.Trackable import androidx.xr.runtime.Config import androidx.xr.runtime.Session import androidx.xr.runtime.SessionConfigureConfigurationNotSupported -import androidx.xr.runtime.SessionConfigurePermissionsNotGranted import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.scenecore.AnchorEntity import androidx.xr.scenecore.Entity import androidx.xr.scenecore.scene -@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted fun configureAnchoring(session: Session) { // [START androidxr_arcore_anchoring_configure] val newConfig = session.config.copy( - anchorPersistence = Config.AnchorPersistenceMode.Enabled, + anchorPersistence = Config.AnchorPersistenceMode.LOCAL, ) when (val result = session.configure(newConfig)) { is SessionConfigureConfigurationNotSupported -> TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) - is SessionConfigurePermissionsNotGranted -> - TODO(/* The required permissions in result.permissions have not been granted. */) is SessionConfigureSuccess -> TODO(/* Success! */) + else -> + TODO(/* A different unhandled exception was thrown. */) } // [END androidxr_arcore_anchoring_configure] } @@ -72,7 +70,7 @@ private fun attachEntityToAnchor( ) { // [START androidxr_arcore_entity_tracks_anchor] AnchorEntity.create(session, anchor).apply { - setParent(session.scene.activitySpace) + parent = session.scene.activitySpace addChild(entity) } // [END androidxr_arcore_entity_tracks_anchor] diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt index 13346b202..1507bed7c 100644 --- a/xr/src/main/java/com/example/xr/arcore/Hands.kt +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -24,7 +24,6 @@ import androidx.xr.runtime.Config import androidx.xr.runtime.HandJointType import androidx.xr.runtime.Session import androidx.xr.runtime.SessionConfigureConfigurationNotSupported -import androidx.xr.runtime.SessionConfigurePermissionsNotGranted import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion @@ -35,18 +34,17 @@ import androidx.xr.scenecore.scene import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted fun ComponentActivity.configureSession(session: Session) { // [START androidxr_arcore_hand_configure] val newConfig = session.config.copy( - handTracking = Config.HandTrackingMode.Enabled + handTracking = Config.HandTrackingMode.BOTH ) when (val result = session.configure(newConfig)) { is SessionConfigureConfigurationNotSupported -> TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) - is SessionConfigurePermissionsNotGranted -> - TODO(/* The required permissions in result.permissions have not been granted. */) is SessionConfigureSuccess -> TODO(/* Success! */) + else -> + TODO(/* A different unhandled exception was thrown. */) } // [END androidxr_arcore_hand_configure] } @@ -71,8 +69,8 @@ fun ComponentActivity.collectHands(session: Session) { fun secondaryHandDetection(activity: Activity, session: Session) { fun detectGesture(handState: Flow) {} // [START androidxr_arcore_hand_handedness] - val handedness = Hand.getHandedness(activity.contentResolver) - val secondaryHand = if (handedness == Hand.Handedness.LEFT) Hand.right(session) else Hand.left(session) + val handedness = Hand.getPrimaryHandSide(activity.contentResolver) + val secondaryHand = if (handedness == Hand.HandSide.LEFT) Hand.right(session) else Hand.left(session) val handState = secondaryHand?.state ?: return detectGesture(handState) // [END androidxr_arcore_hand_handedness] @@ -86,7 +84,7 @@ fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { // the down direction points in the same direction as the palm val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up) - palmEntity.setHidden(angle > Math.toRadians(40.0)) + palmEntity.setEnabled(angle > Math.toRadians(40.0)) val transformedPose = session.scene.perceptionSpace.transformPoseTo( @@ -107,7 +105,7 @@ fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { // the forward direction points towards the finger tip. val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up) - indexFingerEntity.setHidden(angle > Math.toRadians(40.0)) + indexFingerEntity.setEnabled(angle > Math.toRadians(40.0)) val transformedPose = session.scene.perceptionSpace.transformPoseTo( diff --git a/xr/src/main/java/com/example/xr/arcore/Planes.kt b/xr/src/main/java/com/example/xr/arcore/Planes.kt index fd5e02c11..b5017dcb7 100644 --- a/xr/src/main/java/com/example/xr/arcore/Planes.kt +++ b/xr/src/main/java/com/example/xr/arcore/Planes.kt @@ -20,24 +20,22 @@ import androidx.xr.arcore.Plane import androidx.xr.runtime.Config import androidx.xr.runtime.Session import androidx.xr.runtime.SessionConfigureConfigurationNotSupported -import androidx.xr.runtime.SessionConfigurePermissionsNotGranted import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Ray import androidx.xr.scenecore.scene -@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted fun configurePlaneTracking(session: Session) { // [START androidxr_arcore_planetracking_configure] val newConfig = session.config.copy( - planeTracking = Config.PlaneTrackingMode.HorizontalAndVertical, + planeTracking = Config.PlaneTrackingMode.HORIZONTAL_AND_VERTICAL, ) when (val result = session.configure(newConfig)) { is SessionConfigureConfigurationNotSupported -> TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) - is SessionConfigurePermissionsNotGranted -> - TODO(/* The required permissions in result.permissions have not been granted. */) is SessionConfigureSuccess -> TODO(/* Success! */) + else -> + TODO(/* A different unhandled exception was thrown. */) } // [END androidxr_arcore_planetracking_configure] } @@ -58,7 +56,7 @@ private fun hitTestTable(session: Session) { // When interested in the first Table hit: val tableHit = results.firstOrNull { val trackable = it.trackable - trackable is Plane && trackable.state.value.label == Plane.Label.Table + trackable is Plane && trackable.state.value.label == Plane.Label.TABLE } // [END androidxr_arcore_hitTest] } diff --git a/xr/src/main/java/com/example/xr/compose/Orbiter.kt b/xr/src/main/java/com/example/xr/compose/Orbiter.kt index 364709c83..f01c4cd1f 100644 --- a/xr/src/main/java/com/example/xr/compose/Orbiter.kt +++ b/xr/src/main/java/com/example/xr/compose/Orbiter.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -36,9 +36,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.xr.compose.spatial.EdgeOffset +import androidx.xr.compose.spatial.ContentEdge import androidx.xr.compose.spatial.Orbiter -import androidx.xr.compose.spatial.OrbiterEdge +import androidx.xr.compose.spatial.OrbiterOffsetType import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.SpatialRow @@ -72,7 +72,7 @@ private fun OrbiterExampleSubspace() { @Composable fun OrbiterExample() { Orbiter( - position = OrbiterEdge.Bottom, + position = ContentEdge.Bottom, offset = 96.dp, alignment = Alignment.CenterHorizontally ) { @@ -102,13 +102,14 @@ fun OrbiterAnchoringExample() { Subspace { SpatialRow { Orbiter( - position = OrbiterEdge.Top, - offset = EdgeOffset.inner(8.dp), + position = ContentEdge.Top, + offset = 8.dp, + offsetType = OrbiterOffsetType.InnerEdge, shape = SpatialRoundedCornerShape(size = CornerSize(50)) ) { Text( "Hello World!", - style = MaterialTheme.typography.h2, + style = MaterialTheme.typography.titleMedium, modifier = Modifier .background(Color.White) .padding(16.dp) @@ -150,7 +151,7 @@ private fun Ui2DToOribiter() { // New XR differentiated approach Orbiter( - position = OrbiterEdge.Start, + position = ContentEdge.Start, offset = dimensionResource(R.dimen.start_orbiter_padding), alignment = Alignment.Top ) { diff --git a/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt b/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt index 3ab8f3f54..68fedfe3f 100644 --- a/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt +++ b/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt @@ -27,7 +27,7 @@ private fun ComposableThatShouldElevateInXr() {} private fun SpatialElevationExample() { // [START androidxr_compose_spatialelevation] // Elevate an otherwise 2D Composable (signified here by ComposableThatShouldElevateInXr). - SpatialElevation(spatialElevationLevel = SpatialElevationLevel.Level4) { + SpatialElevation(elevation = SpatialElevationLevel.Level4) { ComposableThatShouldElevateInXr() } // [END androidxr_compose_spatialelevation] diff --git a/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt b/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt index 1736bc909..c42730758 100644 --- a/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt +++ b/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt @@ -19,19 +19,23 @@ package com.example.xr.compose import android.content.ContentResolver import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.SpatialExternalSurface import androidx.xr.compose.subspace.StereoMode +import androidx.xr.compose.subspace.SurfaceProtection import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.width // [START androidxr_compose_SpatialExternalSurfaceStereo] +@OptIn(ExperimentalComposeApi::class) @Composable fun SpatialExternalSurfaceContent() { val context = LocalContext.current @@ -69,3 +73,46 @@ fun SpatialExternalSurfaceContent() { } } // [END androidxr_compose_SpatialExternalSurfaceStereo] + +// [START androidxr_compose_SpatialExternalSurfaceDRM] +@OptIn(ExperimentalComposeApi::class) +@Composable +fun DrmSpatialVideoPlayer() { + val context = LocalContext.current + Subspace { + SpatialExternalSurface( + modifier = SubspaceModifier + .width(1200.dp) + .height(676.dp), + stereoMode = StereoMode.SideBySide, + surfaceProtection = SurfaceProtection.Protected + ) { + val exoPlayer = remember { ExoPlayer.Builder(context).build() } + + // Define the URI for your DRM-protected content and license server. + val videoUri = "https://your-content-provider.com/video.mpd" + val drmLicenseUrl = "https://your-license-server.com/license" + + // Build a MediaItem with the necessary DRM configuration. + val mediaItem = MediaItem.Builder() + .setUri(videoUri) + .setDrmConfiguration( + MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) + .setLicenseUri(drmLicenseUrl) + .build() + ) + .build() + + onSurfaceCreated { surface -> + // The created surface is secure and can be used by the player. + exoPlayer.setVideoSurface(surface) + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.play() + } + + onSurfaceDestroyed { exoPlayer.release() } + } + } +} +// [END androidxr_compose_SpatialExternalSurfaceDRM] diff --git a/xr/src/main/java/com/example/xr/compose/Views.kt b/xr/src/main/java/com/example/xr/compose/Views.kt index 4fc693828..b567b2f09 100644 --- a/xr/src/main/java/com/example/xr/compose/Views.kt +++ b/xr/src/main/java/com/example/xr/compose/Views.kt @@ -35,8 +35,8 @@ import androidx.xr.compose.subspace.layout.depth import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.width import androidx.xr.runtime.Session +import androidx.xr.runtime.math.IntSize2d import androidx.xr.scenecore.PanelEntity -import androidx.xr.scenecore.PixelDimensions import com.example.xr.R private class MyCustomView(context: Context) : View(context) @@ -86,7 +86,7 @@ fun ComponentActivity.PanelEntityWithView(xrSession: Session) { val panelEntity = PanelEntity.create( session = xrSession, view = panelContent, - pixelDimensions = PixelDimensions(500, 500), + pixelDimensions = IntSize2d(500, 500), name = "panel entity" ) // [END androidxr_compose_PanelEntityWithView] diff --git a/xr/src/main/java/com/example/xr/compose/Volume.kt b/xr/src/main/java/com/example/xr/compose/Volume.kt index 83073c224..cce37ca83 100644 --- a/xr/src/main/java/com/example/xr/compose/Volume.kt +++ b/xr/src/main/java/com/example/xr/compose/Volume.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.xr.compose.platform.LocalSession import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.ExperimentalSubspaceVolumeApi import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.Volume import androidx.xr.compose.subspace.layout.SubspaceModifier @@ -62,6 +63,7 @@ private fun VolumeExample() { } // [START androidxr_compose_ObjectInAVolume] +@OptIn(ExperimentalSubspaceVolumeApi::class) @Composable fun ObjectInAVolume(show3DObject: Boolean) { // [START_EXCLUDE silent] diff --git a/xr/src/main/java/com/example/xr/misc/ModeTransition.kt b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt index dca0ddbfb..ea13d5868 100644 --- a/xr/src/main/java/com/example/xr/misc/ModeTransition.kt +++ b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt @@ -32,6 +32,6 @@ fun modeTransitionCompose() { fun modeTransitionScenecore(xrSession: Session) { // [START androidxr_misc_modeTransitionScenecore] - xrSession.scene.spatialEnvironment.requestHomeSpaceMode() + xrSession.scene.requestHomeSpaceMode() // [END androidxr_misc_modeTransitionScenecore] } diff --git a/xr/src/main/java/com/example/xr/runtime/Session.kt b/xr/src/main/java/com/example/xr/runtime/Session.kt index f2fd85a2a..2a9f84ff1 100644 --- a/xr/src/main/java/com/example/xr/runtime/Session.kt +++ b/xr/src/main/java/com/example/xr/runtime/Session.kt @@ -16,14 +16,11 @@ package com.example.xr.runtime -import android.app.Activity +import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.xr.compose.platform.LocalSession import androidx.xr.runtime.Session -import androidx.xr.runtime.SessionCreatePermissionsNotGranted import androidx.xr.runtime.SessionCreateSuccess -import androidx.xr.runtime.SessionResumePermissionsNotGranted -import androidx.xr.runtime.SessionResumeSuccess // [START androidxr_localsession] @Composable @@ -32,30 +29,15 @@ fun ComposableUsingSession() { } // [END androidxr_localsession] -fun Activity.createSession() { +fun ComponentActivity.createSession() { // [START androidxr_session_create] when (val result = Session.create(this)) { is SessionCreateSuccess -> { val xrSession = result.session // ... } - is SessionCreatePermissionsNotGranted -> - TODO(/* The required permissions in result.permissions have not been granted. */) + else -> + TODO(/* A different unhandled exception was thrown. */) } // [END androidxr_session_create] } - -fun sessionResume(session: Session) { - // [START androidxr_session_resume] - when (val result = session.resume()) { - is SessionResumeSuccess -> { - // Session has been created successfully. - // Attach any successful handlers here. - } - - is SessionResumePermissionsNotGranted -> { - // Request permissions in `result.permissions`. - } - } - // [END androidxr_session_resume] -} diff --git a/xr/src/main/java/com/example/xr/scenecore/Entities.kt b/xr/src/main/java/com/example/xr/scenecore/Entities.kt index d4c723604..cf16f266e 100644 --- a/xr/src/main/java/com/example/xr/scenecore/Entities.kt +++ b/xr/src/main/java/com/example/xr/scenecore/Entities.kt @@ -16,23 +16,10 @@ package com.example.xr.scenecore -import androidx.xr.runtime.Session import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Vector3 -import androidx.xr.scenecore.AnchorPlacement -import androidx.xr.scenecore.Dimensions import androidx.xr.scenecore.Entity -import androidx.xr.scenecore.InputEvent -import androidx.xr.scenecore.InteractableComponent -import androidx.xr.scenecore.MovableComponent -import androidx.xr.scenecore.PlaneSemantic -import androidx.xr.scenecore.PlaneType -import androidx.xr.scenecore.ResizableComponent -import androidx.xr.scenecore.ResizeListener -import androidx.xr.scenecore.SurfaceEntity -import java.util.concurrent.Executor -import java.util.concurrent.Executors private fun setPoseExample(entity: Entity) { // [START androidxr_scenecore_entity_setPoseExample] @@ -45,11 +32,11 @@ private fun setPoseExample(entity: Entity) { // [END androidxr_scenecore_entity_setPoseExample] } -private fun hideEntity(entity: Entity) { - // [START androidxr_scenecore_entity_hideEntity] - // Hide the entity - entity.setHidden(true) - // [END androidxr_scenecore_entity_hideEntity] +private fun disableEntity(entity: Entity) { + // [START androidxr_scenecore_entity_setEnabled] + // Disable the entity. + entity.setEnabled(false) + // [END androidxr_scenecore_entity_setEnabled] } private fun entitySetScale(entity: Entity) { @@ -58,62 +45,3 @@ private fun entitySetScale(entity: Entity) { entity.setScale(2f) // [END androidxr_scenecore_entity_entitySetScale] } - -private fun moveableComponentExample(session: Session, entity: Entity) { - // [START androidxr_scenecore_moveableComponentExample] - val anchorPlacement = AnchorPlacement.createForPlanes( - planeTypeFilter = setOf(PlaneSemantic.FLOOR, PlaneSemantic.TABLE), - planeSemanticFilter = setOf(PlaneType.VERTICAL) - ) - - val movableComponent = MovableComponent.create( - session = session, - systemMovable = false, - scaleInZ = false, - anchorPlacement = setOf(anchorPlacement) - ) - entity.addComponent(movableComponent) - // [END androidxr_scenecore_moveableComponentExample] -} - -private fun resizableComponentExample(session: Session, entity: Entity, executor: Executor) { - // [START androidxr_scenecore_resizableComponentExample] - val resizableComponent = ResizableComponent.create(session) - resizableComponent.minimumSize = Dimensions(177f, 100f, 1f) - resizableComponent.fixedAspectRatio = 16f / 9f // Specify a 16:9 aspect ratio - - resizableComponent.addResizeListener( - executor, - object : ResizeListener { - override fun onResizeEnd(entity: Entity, finalSize: Dimensions) { - - // update the size in the component - resizableComponent.size = finalSize - - // update the Entity to reflect the new size - (entity as SurfaceEntity).canvasShape = SurfaceEntity.CanvasShape.Quad(finalSize.width, finalSize.height) - } - }, - ) - - entity.addComponent(resizableComponent) - // [END androidxr_scenecore_resizableComponentExample] -} - -private fun interactableComponentExample(session: Session, entity: Entity) { - // [START androidxr_scenecore_interactableComponentExample] - val executor = Executors.newSingleThreadExecutor() - val interactableComponent = InteractableComponent.create(session, executor) { - // when the user disengages with the entity with their hands - if (it.source == InputEvent.SOURCE_HANDS && it.action == InputEvent.ACTION_UP) { - // increase size with right hand and decrease with left - if (it.pointerType == InputEvent.POINTER_TYPE_RIGHT) { - entity.setScale(1.5f) - } else if (it.pointerType == InputEvent.POINTER_TYPE_LEFT) { - entity.setScale(0.5f) - } - } - } - entity.addComponent(interactableComponent) - // [END androidxr_scenecore_interactableComponentExample] -} diff --git a/xr/src/main/java/com/example/xr/scenecore/Environments.kt b/xr/src/main/java/com/example/xr/scenecore/Environments.kt index 35f753569..5ed80d36f 100644 --- a/xr/src/main/java/com/example/xr/scenecore/Environments.kt +++ b/xr/src/main/java/com/example/xr/scenecore/Environments.kt @@ -16,24 +16,24 @@ package com.example.xr.scenecore +import android.content.Context import androidx.xr.runtime.Session import androidx.xr.scenecore.ExrImage import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.SpatialEnvironment import androidx.xr.scenecore.scene -import kotlinx.coroutines.guava.await +import java.nio.file.Paths private class Environments(val session: Session) { - suspend fun loadEnvironmentGeometry() { + suspend fun loadEnvironmentGeometry(context: Context) { // [START androidxr_scenecore_environment_loadEnvironmentGeometry] - val environmentGeometryFuture = GltfModel.create(session, "DayGeometry.glb") - val environmentGeometry = environmentGeometryFuture.await() + val environmentGeometry = GltfModel.create(session, Paths.get("DayGeometry.glb")) // [END androidxr_scenecore_environment_loadEnvironmentGeometry] } - fun loadEnvironmentSkybox() { + suspend fun loadEnvironmentSkybox() { // [START androidxr_scenecore_environment_loadEnvironmentSkybox] - val lightingForSkybox = ExrImage.create(session, "BlueSkyboxLighting.zip") + val lightingForSkybox = ExrImage.createFromZip(session, Paths.get("BlueSkyboxLighting.zip")) // [END androidxr_scenecore_environment_loadEnvironmentSkybox] } @@ -41,38 +41,40 @@ private class Environments(val session: Session) { // [START androidxr_scenecore_environment_setEnvironmentPreference] val spatialEnvironmentPreference = SpatialEnvironment.SpatialEnvironmentPreference(lightingForSkybox, environmentGeometry) - val preferenceResult = - session.scene.spatialEnvironment.setSpatialEnvironmentPreference(spatialEnvironmentPreference) - if (preferenceResult == SpatialEnvironment.SetSpatialEnvironmentPreferenceChangeApplied()) { + session.scene.spatialEnvironment.preferredSpatialEnvironment = spatialEnvironmentPreference + if (session.scene.spatialEnvironment.isPreferredSpatialEnvironmentActive) { // The environment was successfully updated and is now visible, and any listeners // specified using addOnSpatialEnvironmentChangedListener will be notified. - } else if (preferenceResult == SpatialEnvironment.SetSpatialEnvironmentPreferenceChangePending()) { - // The environment is in the process of being updated. Once visible, any listeners - // specified using addOnSpatialEnvironmentChangedListener will be notified. + } else { + // The passthrough opacity preference was successfully set, but not + // immediately visible. The passthrough opacity change will be applied + // when the activity has the SPATIAL_CAPABILITY_APP_ENVIRONMENT capability. + // Then, any listeners specified using addOnSpatialEnvironmentChangedListener + // will be notified. } // [END androidxr_scenecore_environment_setEnvironmentPreference] } fun setPassthroughOpacityPreference() { // [START androidxr_scenecore_environment_setPassthroughOpacityPreference] - val preferenceResult = session.scene.spatialEnvironment.setPassthroughOpacityPreference(1.0f) - if (preferenceResult == SpatialEnvironment.SetPassthroughOpacityPreferenceChangeApplied()) { + session.scene.spatialEnvironment.preferredPassthroughOpacity = 1.0f + if (session.scene.spatialEnvironment.currentPassthroughOpacity == 1.0f) { // The passthrough opacity request succeeded and should be visible now, and any listeners - // specified using addOnPassthroughOpacityChangedListener will be notified - } else if (preferenceResult == SpatialEnvironment.SetPassthroughOpacityPreferenceChangePending()) { + // specified using addOnPassthroughOpacityChangedListener will be notified. + } else { // The passthrough opacity preference was successfully set, but not // immediately visible. The passthrough opacity change will be applied // when the activity has the // SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL capability. // Then, any listeners specified using addOnPassthroughOpacityChangedListener - // will be notified + // will be notified. } // [END androidxr_scenecore_environment_setPassthroughOpacityPreference] } fun getCurrentPassthroughOpacity() { // [START androidxr_scenecore_environment_getCurrentPassthroughOpacity] - val currentPassthroughOpacity = session.scene.spatialEnvironment.getCurrentPassthroughOpacity() + val currentPassthroughOpacity = session.scene.spatialEnvironment.currentPassthroughOpacity // [END androidxr_scenecore_environment_getCurrentPassthroughOpacity] } } diff --git a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt index c7181e2f3..997436ddf 100644 --- a/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt +++ b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt @@ -25,11 +25,11 @@ import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.GltfModelEntity import androidx.xr.scenecore.SpatialCapabilities import androidx.xr.scenecore.scene -import kotlinx.coroutines.guava.await +import java.nio.file.Paths private suspend fun loadGltfFile(session: Session) { // [START androidxr_scenecore_gltfmodel_create] - val gltfModel = GltfModel.create(session, "models/saturn_rings.glb").await() + val gltfModel = GltfModel.create(session, Paths.get("models", "saturn_rings.glb")) // [END androidxr_scenecore_gltfmodel_create] } diff --git a/xr/src/main/java/com/example/xr/scenecore/InteractableComponent.kt b/xr/src/main/java/com/example/xr/scenecore/InteractableComponent.kt new file mode 100644 index 000000000..a712d0dfe --- /dev/null +++ b/xr/src/main/java/com/example/xr/scenecore/InteractableComponent.kt @@ -0,0 +1,41 @@ +/* + * 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.xr.scenecore + +import androidx.xr.runtime.Session +import androidx.xr.scenecore.Entity +import androidx.xr.scenecore.InputEvent +import androidx.xr.scenecore.InteractableComponent +import java.util.concurrent.Executors + +private fun interactableComponentExample(session: Session, entity: Entity) { + // [START androidxr_scenecore_interactableComponentExample] + val executor = Executors.newSingleThreadExecutor() + val interactableComponent = InteractableComponent.create(session, executor) { + // when the user disengages with the entity with their hands + if (it.source == InputEvent.Source.SOURCE_HANDS && it.action == InputEvent.Action.ACTION_UP) { + // increase size with right hand and decrease with left + if (it.pointerType == InputEvent.Pointer.POINTER_TYPE_RIGHT) { + entity.setScale(1.5f) + } else if (it.pointerType == InputEvent.Pointer.POINTER_TYPE_LEFT) { + entity.setScale(0.5f) + } + } + } + entity.addComponent(interactableComponent) + // [END androidxr_scenecore_interactableComponentExample] +} diff --git a/xr/src/main/java/com/example/xr/scenecore/MovableComponent.kt b/xr/src/main/java/com/example/xr/scenecore/MovableComponent.kt new file mode 100644 index 000000000..3c254e433 --- /dev/null +++ b/xr/src/main/java/com/example/xr/scenecore/MovableComponent.kt @@ -0,0 +1,46 @@ +/* + * 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.xr.scenecore + +import androidx.xr.runtime.Session +import androidx.xr.scenecore.AnchorPlacement +import androidx.xr.scenecore.Entity +import androidx.xr.scenecore.MovableComponent +import androidx.xr.scenecore.PlaneOrientation +import androidx.xr.scenecore.PlaneSemanticType + +private fun createSystemMovable(session: Session, entity: Entity) { + // [START androidxr_scenecore_movableComponent_createSystemMovable] + val movableComponent = MovableComponent.createSystemMovable(session) + entity.addComponent(movableComponent) + // [END androidxr_scenecore_movableComponent_createSystemMovable] +} + +private fun movableComponentAnchorExample(session: Session, entity: Entity) { + // [START androidxr_scenecore_movableComponent_anchorable] + val anchorPlacement = AnchorPlacement.createForPlanes( + anchorablePlaneOrientations = setOf(PlaneOrientation.VERTICAL), + anchorablePlaneSemanticTypes = setOf(PlaneSemanticType.FLOOR, PlaneSemanticType.TABLE) + ) + + val movableComponent = MovableComponent.createAnchorable( + session = session, + anchorPlacement = setOf(anchorPlacement) + ) + entity.addComponent(movableComponent) + // [END androidxr_scenecore_movableComponent_anchorable] +} diff --git a/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt b/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt new file mode 100644 index 000000000..28b70027d --- /dev/null +++ b/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt @@ -0,0 +1,43 @@ +/* + * 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.xr.scenecore + +import androidx.xr.runtime.Session +import androidx.xr.runtime.math.FloatSize3d +import androidx.xr.scenecore.ResizableComponent +import androidx.xr.scenecore.ResizeEvent +import androidx.xr.scenecore.SurfaceEntity +import java.util.concurrent.Executor + +private fun resizableComponentExample( + session: Session, + surfaceEntity: SurfaceEntity, + executor: Executor +) { + // [START androidxr_scenecore_resizableComponentExample] + val resizableComponent = ResizableComponent.create(session) { event -> + if (event.resizeState == ResizeEvent.ResizeState.RESIZE_STATE_END) { + // update the Entity to reflect the new size + surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Quad(event.newSize.width, event.newSize.height) + } + } + resizableComponent.minimumEntitySize = FloatSize3d(177f, 100f, 1f) + resizableComponent.fixedAspectRatio = 16f / 9f // Specify a 16:9 aspect ratio + + surfaceEntity.addComponent(resizableComponent) + // [END androidxr_scenecore_resizableComponentExample] +} diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt index 1d1eac1ae..b68e67713 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt @@ -22,6 +22,10 @@ import android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION import android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION import android.media.MediaPlayer import android.media.SoundPool +import androidx.annotation.OptIn +import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.audio.AudioCapabilities import androidx.xr.runtime.Session import androidx.xr.scenecore.Entity import androidx.xr.scenecore.PointSourceParams @@ -149,3 +153,19 @@ private fun playSpatialAudioAtEntityAmbionics(session: Session, appContext: Cont } // [END androidxr_scenecore_playSpatialAudioAmbionics] } + +@OptIn(UnstableApi::class) +private fun detectSupport(context: Context) { + // [START androidxr_scenecore_dolby_detect_support] + val audioCapabilities = AudioCapabilities.getCapabilities(context, androidx.media3.common.AudioAttributes.DEFAULT, null) + if (audioCapabilities.supportsEncoding(C.ENCODING_AC3)) { + // Device supports playback of the Dolby Digital media format. + } + if (audioCapabilities.supportsEncoding(C.ENCODING_E_AC3)) { + // Device supports playback of the Dolby Digital Plus media format. + } + if (audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { + // Device supports playback of the Dolby Digital Plus with Dolby Atmos media format. + } + // [END androidxr_scenecore_dolby_detect_support] +} diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt index fcfcdf5a8..7fb784080 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.kt @@ -27,7 +27,7 @@ fun checkMultipleCapabilities(xrSession: Session) { SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL ) ) { - xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(0f) + xrSession.scene.spatialEnvironment.preferredPassthroughOpacity = 1f } // Example 2: multiple capability flags can be checked simultaneously: if (xrSession.scene.spatialCapabilities.hasCapability( diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index 460d35db2..ec733bb61 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -19,13 +19,19 @@ package com.example.xr.scenecore import android.content.ContentResolver import android.net.Uri import androidx.activity.ComponentActivity +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.xr.runtime.Session import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Vector3 import androidx.xr.scenecore.SurfaceEntity +import androidx.xr.scenecore.Texture +import androidx.xr.scenecore.TextureSampler import androidx.xr.scenecore.scene +import java.nio.file.Paths +import kotlinx.coroutines.launch private fun ComponentActivity.surfaceEntityCreate(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreate] @@ -105,3 +111,117 @@ private fun ComponentActivity.surfaceEntityCreateMVHEVC(xrSession: Session) { exoPlayer.play() // [END androidxr_scenecore_surfaceEntityCreateMVHEVC] } + +private fun ComponentActivity.surfaceEntityCreateDRM(xrSession: Session) { + // [START androidxr_scenecore_surfaceEntityCreateDRM] + // Create a SurfaceEntity with DRM content + + // Define the URI for your DRM-protected content and license server. + val videoUri = "https://your-content-provider.com/video.mpd" + val drmLicenseUrl = "https://your-license-server.com/license" + + // Create the SurfaceEntity with the PROTECTED content security level. + val protectedSurfaceEntity = SurfaceEntity.create( + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), + canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f), + contentSecurityLevel = SurfaceEntity.ContentSecurityLevel.PROTECTED + ) + + // Build a MediaItem with the necessary DRM configuration. + val mediaItem = MediaItem.Builder() + .setUri(videoUri) + .setDrmConfiguration( + MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) + .setLicenseUri(drmLicenseUrl) + .build() + ) + .build() + + // Initialize ExoPlayer and set the protected surface. + val exoPlayer = ExoPlayer.Builder(this).build() + exoPlayer.setVideoSurface(protectedSurfaceEntity.getSurface()) + + // Set the media item and start playback. + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.play() + + // [END androidxr_scenecore_surfaceEntityCreateDRM] +} + +private fun ComponentActivity.surfaceEntityHDR(xrSession: Session) { + // [START androidxr_scenecore_surfaceEntityHDR] + // Define the color properties for your HDR video. These values should be specific + // to your content. + val hdrMetadata = SurfaceEntity.ContentColorMetadata( + colorSpace = SurfaceEntity.ContentColorMetadata.ColorSpace.BT2020, + colorTransfer = SurfaceEntity.ContentColorMetadata.ColorTransfer.ST2084, // PQ + colorRange = SurfaceEntity.ContentColorMetadata.ColorRange.LIMITED, + maxCLL = 1000 // Example: 1000 nits + ) + + // Create a SurfaceEntity, passing the HDR metadata at creation time. + val hdrSurfaceEntity = SurfaceEntity.create( + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.MONO, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), + canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f), + contentColorMetadata = hdrMetadata + ) + + // Initialize ExoPlayer and set the surface. + val exoPlayer = ExoPlayer.Builder(this).build() + exoPlayer.setVideoSurface(hdrSurfaceEntity.getSurface()) + + // Define the URI for your HDR content. + val videoUri = "https://your-content-provider.com/hdr_video.mp4" + val mediaItem = MediaItem.fromUri(videoUri) + + // Set the media item and start playback. + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + exoPlayer.play() + // [END androidxr_scenecore_surfaceEntityHDR] +} + +private fun surfaceEntityEdgeFeathering(xrSession: Session) { + // [START androidxr_scenecore_surfaceEntityEdgeFeathering] + // Create a SurfaceEntity. + val surfaceEntity = SurfaceEntity.create( + session = xrSession, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)) + ) + + // Feather the edges of the surface. + surfaceEntity.edgeFeather = + SurfaceEntity.EdgeFeatheringParams.SmoothFeather(0.1f, 0.1f) + // [END androidxr_scenecore_surfaceEntityEdgeFeathering] +} + +private fun surfaceEntityAlphaMasking(xrSession: Session, activity: ComponentActivity) { + // [START androidxr_scenecore_surfaceEntityAlphaMasking] + // Create a SurfaceEntity. + val surfaceEntity = SurfaceEntity.create( + session = xrSession, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)) + ) + + // Load the texture in a coroutine scope. + activity.lifecycleScope.launch { + val alphaMaskTexture = + Texture.create( + xrSession, + Paths.get("textures", "alpha_mask.png"), + TextureSampler.create() + ) + + // Apply the alpha mask. + surfaceEntity.primaryAlphaMaskTexture = alphaMaskTexture + + // To remove the mask, set the property to null. + surfaceEntity.primaryAlphaMaskTexture = null + } + // [END androidxr_scenecore_surfaceEntityAlphaMasking] +} From 1b043a47ff88d06651e2f62883b653746711ede4 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 1 Aug 2025 08:33:48 +0300 Subject: [PATCH 038/120] Add example of Lottie animation (#572) Snippet will be used on https://developer.android.com/training/wearables/tiles/animations. --- wear/src/main/AndroidManifest.xml | 15 ++ .../example/wear/snippets/tile/Animations.kt | 52 ++++ wear/src/main/res/raw/lottie.json | 240 ++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 wear/src/main/res/raw/lottie.json diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 770b6d956..e72edbe49 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -166,6 +166,21 @@ android:resource="@drawable/tile_preview" /> + + + + + + + + { + + val layout = + LayoutElementBuilders.Image.Builder() + .setWidth(dp(150f)) + .setHeight(dp(150f)) + .setResourceId(lottieResourceId) + .build() + + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(Timeline.fromLayoutElement(layout)) + .build() + ) + } + + override fun onTileResourcesRequest( + requestParams: ResourcesRequest + ): ListenableFuture { + + val lottieImage = + ResourceBuilders.ImageResource.Builder() + .setAndroidLottieResourceByResId( + ResourceBuilders.AndroidLottieResourceByResId.Builder(R.raw.lottie) + .setStartTrigger(createOnVisibleTrigger()) + .build() + ) + .build() + + return Futures.immediateFuture( + Resources.Builder() + .setVersion(requestParams.version) + .addIdToImageMapping(lottieResourceId, lottieImage) + .build() + ) + } +} +// [END android_wear_tile_animations_lottie] diff --git a/wear/src/main/res/raw/lottie.json b/wear/src/main/res/raw/lottie.json new file mode 100644 index 000000000..8725aba51 --- /dev/null +++ b/wear/src/main/res/raw/lottie.json @@ -0,0 +1,240 @@ +{ + "v": "5.12.2", + "fr": 29.9700012207031, + "ip": 0, + "op": 149.000006068894, + "w": 256, + "h": 256, + "nm": "TestLotties", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [128, 128, 0], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [0, 0, 0], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [100, 100, 100], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "rc", + "d": 1, + "s": { + "a": 0, + "k": [147.598, 101.668], + "ix": 2 + }, + "p": { + "a": 0, + "k": [0, 0], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 4 + }, + "nm": "Rectangle Path 1", + "mn": "ADBE Vector Shape - Rect", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.373262771906, 0.896561446844, 0.9078125, 1], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 89, + "s": [-3.201, -3.166], + "to": [0, 0], + "ti": [0, 0] + }, + { + "t": 118.000004806239, + "s": [-61.201, 57.834] + } + ], + "ix": 2 + }, + "a": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 60, + "s": [26, 10], + "to": [0, 0], + "ti": [0, 0] + }, + { + "t": 89.0000036250443, + "s": [1, 45] + } + ], + "ix": 1 + }, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [0.833, 0.833], + "y": [0.833, 0.833] + }, + "o": { + "x": [0.167, 0.167], + "y": [0.167, 0.167] + }, + "t": 118, + "s": [57, 126] + }, + { + "t": 146.000005946702, + "s": [107, 69] + } + ], + "ix": 3 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [0.833], + "y": [0.833] + }, + "o": { + "x": [0.167], + "y": [0.167] + }, + "t": 28, + "s": [18] + }, + { + "t": 60.0000024438501, + "s": [60] + } + ], + "ix": 6 + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [0.833], + "y": [0.833] + }, + "o": { + "x": [0.167], + "y": [0.167] + }, + "t": 0, + "s": [48] + }, + { + "t": 28.0000011404634, + "s": [100] + } + ], + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Rectangle 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 900.000036657751, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [], + "props": {} +} From c703008cbef4268120c32c7d74aef6834a9744bf Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Tue, 5 Aug 2025 05:46:35 -0700 Subject: [PATCH 039/120] Fixing compile error in DeviceCompatibilityModeTestJavaSnippets (#575) Happened to notice a typo in the file, but it still didn't compile when I fixed the typo, so I made a second fix. The typo was a missing semicolon at the "import AppCompatActivity" line. But after I fixed that, I got an error complaining about `assertFalse(isLetterboxed(activity))` because `isLetterboxed` is expecting an AppCompatActivity but `activity` is a `MainActivity`. Changing `isLetterboxed` fixed the compile error and also meant we don't import AppCompatActivity so the original typo goes away. [shrug emoji] Is this fix okay? --- .../snippets/DeviceCompatibilityModeTestJavaSnippets.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java index 46ebe69df..ac4c0c28f 100644 --- a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java +++ b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java @@ -16,7 +16,6 @@ package com.example.snippets; -import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario; import androidx.test.ext.junit.rules.ActivityScenarioRule; import org.junit.Rule; @@ -42,7 +41,7 @@ public void activity_launched_notLetterBoxed() { // Method used by snippets. - public boolean isLetterboxed(AppCompatActivity activity) { + public boolean isLetterboxed(MainActivity activity) { return true; } From f8abcf7b25884dd8e647cec128eedf2ef908dc7f Mon Sep 17 00:00:00 2001 From: Ash <83780687+ashnohe@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:16:04 -0700 Subject: [PATCH 040/120] Add Glance in-app widget pinning snippet (#580) Add Glance in-app widget pinning snippet --- .../snippets/glance/GlancePinAppWidget.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt new file mode 100644 index 000000000..428212068 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt @@ -0,0 +1,60 @@ +/* + * 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.compose.snippets.glance + +import android.content.Context +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import kotlinx.coroutines.launch + +class MyWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = MyWidget() +} + +class MyWidget : GlanceAppWidget() { + override suspend fun provideGlance( + context: Context, + id: GlanceId + ) {} +} + +// [START android_compose_glance_in_app_pinning] +@Composable +fun AnInAppComposable() { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + Button( + onClick = { + coroutineScope.launch { + GlanceAppWidgetManager(context).requestPinGlanceAppWidget( + receiver = MyWidgetReceiver::class.java, + preview = MyWidget(), + previewState = DpSize(245.dp, 115.dp) + ) + } + } + ) {} +} +// [END android_compose_glance_in_app_pinning] From f64387467a7c655438a42c9e6c589a345fe55ed4 Mon Sep 17 00:00:00 2001 From: Jon Eckenrode <112520815+JonEckenrode@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:16:55 -0400 Subject: [PATCH 041/120] Revised snippets for consistency. (#579) * Revised snippets for consistency. * Apply Spotless * Fixed method reference. * Update misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java Co-authored-by: Alex Vanyo * Update misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java Co-authored-by: Alex Vanyo --------- Co-authored-by: Alex Vanyo --- .../DeviceCompatibilityModeTestJavaSnippets.java | 13 ++++++++----- .../DeviceCompatibilityModeTestKotlinSnippets.kt | 3 +-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java index ac4c0c28f..ba249b0e6 100644 --- a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java +++ b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java @@ -16,6 +16,7 @@ package com.example.snippets; +import androidx.appcompat.app.AppCompatActivity; import androidx.test.core.app.ActivityScenario; import androidx.test.ext.junit.rules.ActivityScenarioRule; import org.junit.Rule; @@ -33,16 +34,18 @@ public void activity_launched_notLetterBoxed() { try (ActivityScenario scenario = ActivityScenario.launch(MainActivity.class)) { scenario.onActivity( activity -> { - assertFalse(isLetterboxed(activity)); + assertFalse(activity.isLetterboxed()); }); } } // [END android_device_compatibility_mode_assert_isLetterboxed_java] - // Method used by snippets. - public boolean isLetterboxed(MainActivity activity) { - return true; - } + // Class used by snippets. + class MainActivity extends AppCompatActivity { + public boolean isLetterboxed() { + return true; + } + } } diff --git a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestKotlinSnippets.kt b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestKotlinSnippets.kt index 7b392c612..65f9a7f1f 100644 --- a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestKotlinSnippets.kt +++ b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestKotlinSnippets.kt @@ -36,10 +36,9 @@ class DeviceCompatibilityModeTestKotlinSnippets { } // [END android_device_compatibility_mode_assert_isLetterboxed_kotlin] - // Classes used by snippets. + // Class used by snippets. class MainActivity : AppCompatActivity() { - fun isLetterboxed(): Boolean { return true } From a87b0768109ca7e12e73a831d053a52bfe9a25e3 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Mon, 11 Aug 2025 13:30:12 -0700 Subject: [PATCH 042/120] Resolve build failures so repository will build with `./gradlew clean build` (#584) * Fix: Get ``./gradlew build`` to succeed This disables several checks to make the build succeed. We should try to enable the checks and fix the underlying issues. - Adds missing BLUETOOTH_CONNECT and BLUETOOTH_SCAN permissions to AndroidManifest.xml for BluetoothLe sample. - Adds ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION to AndroidManifest.xml and uses tools:ignore to suppress the CoarseFineLocation warning. - Adds `@SuppressWarnings("MissingPermission")` annotations to setUpBLE methods in MainActivity.java and MainActivity.kt, and scanLeDevice method in DeviceScanActivity.kt to suppress lint warnings. - Increases Xmx value to 4g in gradle.properties. - Disables lint checks during release builds in gradle.properties. - Sets an icon to the Credential Provider in identity/credentialmanager/src/main/AndroidManifest.xml. * Revert gradle.properties to 6042848a43c349e534f4c50d212a3690c7119760 * Use @RequiresPermission instead of @SuppressWarnings - @SuppressWarnings("MissingPermission") + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) * Simplify permissions ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION * Revert compose/snippets/build.gradle.kts * Update build.yml and xr/build.gradle.kts - Add `./gradlew build` to build all modules used by the root project - Add --stacktrace to gradlew commands in build.yml - Update Kotlin jvmToolchain in xr/build.gradle.kts to 17 * Revert Kotlin jvmToolchain to 11 in xr/build.gradle.kts * Remove duplicate build.yml steps The following are redundant because they will run during `./gradlew build --stacktrace` - name: Build Compose run: ./gradlew :compose:snippets:build --stacktrace - name: Build recompose snippets run: ./gradlew :compose:recomposehighlighter:build --stacktrace - name: Build kotlin snippets run: ./gradlew :kotlin:build --stacktrace - name: Build Wear snippets run: ./gradlew :wear:build --stacktrace - name: Build misc snippets run: ./gradlew :misc:build --stacktrace - name: Build XR snippets run: ./gradlew :xr:build --stacktrace * Refactor build.yml to build all modules first --- .github/workflows/build.yml | 16 +++------------- bluetoothle/src/main/AndroidManifest.xml | 5 ++--- .../android/bluetoothle/java/MainActivity.java | 7 +++++-- .../bluetoothle/kotlin/DeviceScanActivity.kt | 3 +++ .../android/bluetoothle/kotlin/MainActivity.kt | 3 +++ .../src/main/AndroidManifest.xml | 2 +- xr/build.gradle.kts | 4 ++-- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bca9bc586..72e815a31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,17 +37,7 @@ jobs: with: distribution: 'zulu' java-version: '17' - - name: Build Compose - run: ./gradlew :compose:snippets:build - - name: Build recompose snippets - run: ./gradlew :compose:recomposehighlighter:build - - name: Build kotlin snippets - run: ./gradlew :kotlin:build - - name: Build Wear snippets - run: ./gradlew :wear:build - - name: Build misc snippets - run: ./gradlew :misc:build - - name: Build XR snippets - run: ./gradlew :xr:build + - name: Build All + run: ./gradlew build --stacktrace - name: Build Watch Face Push validation snippets - run: ./gradlew :watchfacepush:validator:run + run: ./gradlew :watchfacepush:validator:run --stacktrace diff --git a/bluetoothle/src/main/AndroidManifest.xml b/bluetoothle/src/main/AndroidManifest.xml index 1661e746b..d15b213e2 100644 --- a/bluetoothle/src/main/AndroidManifest.xml +++ b/bluetoothle/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -23,6 +22,6 @@ + - \ No newline at end of file diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java index b296ce70b..5e25d58e6 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java @@ -1,13 +1,15 @@ package com.sample.android.bluetoothle.java; -import androidx.appcompat.app.AppCompatActivity; - +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.RequiresPermission; +import androidx.appcompat.app.AppCompatActivity; + import com.sample.android.bluetoothle.R; public class MainActivity extends AppCompatActivity { @@ -19,6 +21,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private void setUpBLE() { // [START get_bluetooth_adapter] // Initializes Bluetooth adapter. diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt index 8ed32d5fa..585a9e3f5 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt @@ -16,11 +16,13 @@ package com.sample.android.bluetoothle.kotlin +import android.Manifest import android.app.ListActivity import android.bluetooth.BluetoothAdapter import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.os.Handler +import androidx.annotation.RequiresPermission import com.sample.android.bluetoothle.java.LeDeviceListAdapter /** @@ -47,6 +49,7 @@ class DeviceScanActivity : ListActivity() { // Stops scanning after 10 seconds. private val SCAN_PERIOD: Long = 10000 + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) private fun scanLeDevice() { if (!mScanning) { // Stops scanning after a pre-defined scan period. handler.postDelayed({ diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt index a8857b4aa..95fe7df91 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt @@ -16,11 +16,13 @@ package com.sample.android.bluetoothle.kotlin +import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.annotation.RequiresPermission import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { @@ -31,6 +33,7 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun setUpBLE() { // [START get_bluetooth_adapter] // Initializes Bluetooth adapter. diff --git a/identity/credentialmanager/src/main/AndroidManifest.xml b/identity/credentialmanager/src/main/AndroidManifest.xml index a2ec1cef8..fb060cc44 100644 --- a/identity/credentialmanager/src/main/AndroidManifest.xml +++ b/identity/credentialmanager/src/main/AndroidManifest.xml @@ -45,7 +45,7 @@ android:enabled="true" android:exported="true" android:label="My Credential Provider" - android:icon="" + android:icon="@mipmap/ic_launcher" android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE" tools:targetApi="upside_down_cake"> diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index 138383c8d..51c224f20 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -19,8 +19,8 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" + kotlin { + jvmToolchain(11) } buildFeatures { compose = true From 79a26f932ec03efb2e8b4b86595d6d2f4be38fb6 Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:23:07 -0700 Subject: [PATCH 043/120] Adding code snippets for wake locks doc (#582) * Adding code snippets for wake locks doc Starting with the code snippets to add and release a wake lock, we can add other snippets later. Used by docs in: https://developer.android.com/develop/background-work/background-tasks/awake/wakelock/ After this is submitted, I'll follow with a CL to update the published docs. --- .../snippets/backgroundwork/MyException.java | 13 +++++ .../backgroundwork/WakeLockSnippetsJava.java | 38 +++++++++++++ .../backgroundwork/WakeLockSnippetsKotlin.kt | 57 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 misc/src/main/java/com/example/snippets/backgroundwork/MyException.java create mode 100644 misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java create mode 100644 misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/MyException.java b/misc/src/main/java/com/example/snippets/backgroundwork/MyException.java new file mode 100644 index 000000000..04d08bfe8 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/backgroundwork/MyException.java @@ -0,0 +1,13 @@ +package com.example.snippets.backgroundwork; + +/** + * Placeholder exception used by wake lock docs. + * + * Existing wake lock code snippets inclde a method that throws "MyException", I need to define + * it for the code snippets to use. + */ +public class MyException extends RuntimeException { + public MyException(String message) { + super(message); + } +} diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java new file mode 100644 index 000000000..f3a5e79b1 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java @@ -0,0 +1,38 @@ +package com.example.snippets.backgroundwork; + +import android.app.Activity; +import android.os.Bundle; +import android.os.PowerManager; + +import androidx.annotation.Nullable; + +public class WakeLockSnippetsJava extends Activity { + + PowerManager.WakeLock wakeLock; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + + // [START android_backgroundwork_wakelock_create_java] + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag"); + wakeLock.acquire(); + // [END android_backgroundwork_wakelock_create_java] + + super.onCreate(savedInstanceState); + } + + // [START android_backgroundwork_wakelock_release_java] + void doSomethingAndRelease() throws MyException { + try { + wakeLock.acquire(); + doTheWork(); + } finally { + wakeLock.release(); + } + } + // [END android_backgroundwork_wakelock_release_java] + + private void doTheWork() { + } +} diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt new file mode 100644 index 000000000..18ead1619 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt @@ -0,0 +1,57 @@ +/* + * 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.snippets.backgroundwork + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.os.PowerManager + +// Snippets for doc page go here +class WakeLockSnippetsKotlin : Activity() { + + // [START android_backgroundwork_wakelock_create_kotlin] + val wakeLock: PowerManager.WakeLock = + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag").apply { + acquire() + } + } + // [END android_backgroundwork_wakelock_create_kotlin] + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + } + + // [START android_backgroundwork_wakelock_release_kotlin] + @Throws(MyException::class) + fun doSomethingAndRelease() { + wakeLock.apply { + try { + acquire() + doTheWork() + } finally { + release() + } + } + } + // [END android_backgroundwork_wakelock_release_kotlin] + + private fun doTheWork() { + } +} From 910e37233010837978d68462e780cc436663fee7 Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Wed, 13 Aug 2025 09:46:37 -0700 Subject: [PATCH 044/120] Breaking long line in code snippet (#586) When I pull the Java "acquire wake lock" snippet into the DAC page, one code line is too long. Breaking that line at the assignment operator so the page user doesn't have to scroll horizontally. --- .../example/snippets/backgroundwork/WakeLockSnippetsJava.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java index f3a5e79b1..7b9a9e682 100644 --- a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java @@ -15,7 +15,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // [START android_backgroundwork_wakelock_create_java] PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); - PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag"); + PowerManager.WakeLock wakeLock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag"); wakeLock.acquire(); // [END android_backgroundwork_wakelock_create_java] From d1b418ea8bdd2643e4b418dcc7ea64ea8175d658 Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:55:34 -0700 Subject: [PATCH 045/120] Add PreloadManager snippets, and update media3 to 1.8.0 (#574) * Adding PreloadManager doc snippets Adding snippets to support new PreloadManager docs. That requires bumping the Media3 version up to 1.8.0. Separate CL is in play to update developer.android.com to pull the snippets in from Github. --- gradle/libs.versions.toml | 2 +- misc/build.gradle.kts | 2 + .../snippets/PreloadManagerKotlinSnippets.kt | 138 ++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 351974d06..d71c7ba05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ maps-compose = "6.6.0" material = "1.14.0-alpha02" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" -media3 = "1.7.1" +media3 = "1.8.0" # @keep minSdk = "35" okHttp = "4.12.0" diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts index af5893d75..cb0905420 100644 --- a/misc/build.gradle.kts +++ b/misc/build.gradle.kts @@ -57,6 +57,8 @@ dependencies { implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.tracing) implementation(libs.hilt.android) diff --git a/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt new file mode 100644 index 000000000..c120e824f --- /dev/null +++ b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt @@ -0,0 +1,138 @@ +/* + * 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.snippets + +import android.os.Bundle +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.preload.DefaultPreloadManager +import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl +import java.lang.Math.abs + +// constants to make the code snippets work +const val currentPlayingIndex = 10 + +@UnstableApi +// [START android_defaultpreloadmanager_MyTargetPreloadStatusControl] +class MyTargetPreloadStatusControl( + currentPlayingIndex: Int = C.INDEX_UNSET +) : TargetPreloadStatusControl { + + override fun getTargetPreloadStatus(index: Int): DefaultPreloadManager.PreloadStatus? { + if (index - currentPlayingIndex == 1) { // next track + // return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and + // suggest loading 3000ms from the default start position + return DefaultPreloadManager.PreloadStatus.specifiedRangeLoaded(3000L) + } else if (index - currentPlayingIndex == -1) { // previous track + // return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and + // suggest loading 3000ms from the default start position + return DefaultPreloadManager.PreloadStatus.specifiedRangeLoaded(3000L) + } else if (abs(index - currentPlayingIndex) == 2) { + // return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED + return DefaultPreloadManager.PreloadStatus.TRACKS_SELECTED + } else if (abs(index - currentPlayingIndex) <= 4) { + // return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED + return DefaultPreloadManager.PreloadStatus.SOURCE_PREPARED + } + return null + } +} +// [END android_defaultpreloadmanager_MyTargetPreloadStatusControl] + +class PreloadManagerSnippetsKotlin { + + class PreloadSnippetsActivity : AppCompatActivity() { + private val context = this + + @OptIn(UnstableApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // [START android_defaultpreloadmanager_createPLM] + val targetPreloadStatusControl = MyTargetPreloadStatusControl() + val preloadManagerBuilder = + DefaultPreloadManager.Builder(context, targetPreloadStatusControl) + val preloadManager = preloadManagerBuilder.build() + // [END android_defaultpreloadmanager_createPLM] + + val player = preloadManagerBuilder.buildExoPlayer() + + // [START android_defaultpreloadmanager_addMedia] + val initialMediaItems = pullMediaItemsFromService(/* count= */ 20) + for (index in 0 until initialMediaItems.size) { + preloadManager.add(initialMediaItems.get(index), /* rankingData= */ index) + } + // items aren't actually loaded yet! need to call invalidate() after this + // [END android_defaultpreloadmanager_addMedia] + + // [START android_defaultpreloadmanager_invalidate] + preloadManager.invalidate() + // [END android_defaultpreloadmanager_invalidate] + } + + @OptIn(UnstableApi::class) + private fun fetchMedia( + preloadManager: DefaultPreloadManager, + mediaItem: MediaItem, + player: ExoPlayer, + currentIndex: Int + ) { + // [START android_defaultpreloadmanager_getAndPlayMedia] + // When a media item is about to display on the screen + val mediaSource = preloadManager.getMediaSource(mediaItem) + if (mediaSource != null) { + player.setMediaSource(mediaSource) + } else { + // If mediaSource is null, that mediaItem hasn't been added to the preload manager + // yet. So, send it directly to the player when it's about to play + player.setMediaItem(mediaItem) + } + player.prepare() + + // When the media item is displaying at the center of the screen + player.play() + preloadManager.setCurrentPlayingIndex(currentIndex) + + // Need to call invalidate() to update the priorities + preloadManager.invalidate() + // [END android_defaultpreloadmanager_getAndPlayMedia] + } + + @OptIn(UnstableApi::class) + private fun removeMedia(mediaItem: MediaItem, preloadManager: DefaultPreloadManager) { + // [START android_defaultpreloadmanager_removeItem] + preloadManager.remove(mediaItem) + // [END android_defaultpreloadmanager_removeItem] + } + + @OptIn(UnstableApi::class) + private fun releasePLM(preloadManager: DefaultPreloadManager) { + // [START android_defaultpreloadmanager_releasePLM] + preloadManager.release() + // [END android_defaultpreloadmanager_releasePLM] + } + + // dummy methods to support the code snippets + private fun pullMediaItemsFromService(count: Int): List { + return listOf() + } + } +} From 39218ccca3e9852e325c1ef5358489d3778fd666 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Mon, 18 Aug 2025 11:07:51 -0700 Subject: [PATCH 046/120] Enable spotless on all modules (#587) * Enable spotless on all modules The `./gradlew spotlessApply` command was failing in the `:watchfacepush:validator` and `:identity:credentialmanager` modules due to incorrect path calculations during the format and apply steps. This was caused by ambiguous source directory configurations in their `build.gradle.kts` files, which led to errors like `NoSuchFileException` and `FileAlreadyExistsException`. This commit resolves these issues by explicitly defining the `sourceSets` in the build scripts for the affected modules. This removes the ambiguity and ensures that Gradle and the Spotless plugin can reliably locate the source files. As a result of this fix, the `apply_spotless.yml` GitHub workflow has been simplified to run a single, project-wide `spotlessApply` command. The `.gitignore` has also been updated to correctly ignore all `build` directories. * Apply Spotless --------- Co-authored-by: cartland <846051+cartland@users.noreply.github.com> --- .github/workflows/apply_spotless.yml | 11 +- .gitignore | 2 +- identity/credentialmanager/build.gradle.kts | 7 + .../CredentialManagerHandler.kt | 80 +-- .../CredentialProviderDummyActivity.kt | 39 +- .../Fido2ToCredmanMigration.kt | 111 ++-- .../MyCredentialProviderService.kt | 55 +- .../PasskeyAndPasswordFunctions.kt | 495 +++++++++--------- .../credentialmanager/PasskeyWebListener.kt | 423 ++++++++------- .../SignInWithGoogleFunctions.kt | 280 +++++----- .../identity/credentialmanager/SingleTap.kt | 26 +- .../credentialmanager/SmartLockToCredMan.kt | 20 +- .../credentialmanager/WebViewMainActivity.kt | 131 +++-- .../coroutines/testing/HomeViewModel.kt | 2 +- .../android/coroutines/testing/Repository.kt | 4 +- .../coroutines/testing/UserRepository.kt | 2 +- .../android/coroutines/testing/UserState.kt | 16 + .../coroutines/testing/CreatingYourOwn.kt | 2 +- .../coroutines/testing/DispatcherTypesTest.kt | 2 +- .../testing/DispatchersOutsideTests.kt | 5 +- .../coroutines/testing/HomeViewModelTest.kt | 2 +- .../testing/HomeViewModelTestUsingRule.kt | 2 +- .../coroutines/testing/RepositoryTest.kt | 16 + .../testing/StandardTestDispatcherTest.kt | 4 +- .../testing/SuspendingFunctionTests.kt | 2 +- .../testing/UnconfinedTestDispatcherTest.kt | 2 +- .../coroutines/testing/UserStateTest.kt | 16 + .../java/insets/SystemBarProtectionSnippet.kt | 2 +- watchfacepush/validator/build.gradle.kts | 8 + .../main/kotlin/com/example/validator/Main.kt | 18 +- 30 files changed, 1001 insertions(+), 784 deletions(-) diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index 0c8dcce4f..d69f817b0 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -42,16 +42,7 @@ jobs: java-version: '17' - name: Run spotlessApply - run: ./gradlew :compose:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for Wear - run: ./gradlew :wear:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for Misc - run: ./gradlew :misc:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - - - name: Run spotlessApply for XR - run: ./gradlew :xr:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + run: ./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace - name: Auto-commit if spotlessApply has changes uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/.gitignore b/.gitignore index 30e5f7bdc..9b10be5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /.idea/modules.xml /.idea/workspace.xml .DS_Store -/build +build /captures .externalNativeBuild .idea/* diff --git a/identity/credentialmanager/build.gradle.kts b/identity/credentialmanager/build.gradle.kts index b0a93839c..2e35608a7 100644 --- a/identity/credentialmanager/build.gradle.kts +++ b/identity/credentialmanager/build.gradle.kts @@ -39,6 +39,13 @@ android { buildFeatures { compose = true } + sourceSets { + named("main") { + java { + srcDir("src/main/java") + } + } + } } dependencies { diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt index 36f4ee175..381bc8fc3 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -13,39 +29,39 @@ import androidx.credentials.exceptions.GetCredentialException // This class is mostly copied from https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/CredentialManagerHandler.kt. class CredentialManagerHandler(private val activity: Activity) { - private val mCredMan = CredentialManager.create(activity.applicationContext) - private val TAG = "CredentialManagerHandler" - /** - * Encapsulates the create passkey API for credential manager in a less error-prone manner. - * - * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. - * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. - */ - suspend fun createPasskey(request: String): CreatePublicKeyCredentialResponse { - val createRequest = CreatePublicKeyCredentialRequest(request) - try { - return mCredMan.createCredential(activity, createRequest) as CreatePublicKeyCredentialResponse - } catch (e: CreateCredentialException) { - // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys - Log.i(TAG, "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}") - throw e + private val mCredMan = CredentialManager.create(activity.applicationContext) + private val TAG = "CredentialManagerHandler" + /** + * Encapsulates the create passkey API for credential manager in a less error-prone manner. + * + * @param request a create public key credential request JSON required by [CreatePublicKeyCredentialRequest]. + * @return [CreatePublicKeyCredentialResponse] containing the result of the credential creation. + */ + suspend fun createPasskey(request: String): CreatePublicKeyCredentialResponse { + val createRequest = CreatePublicKeyCredentialRequest(request) + try { + return mCredMan.createCredential(activity, createRequest) as CreatePublicKeyCredentialResponse + } catch (e: CreateCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error creating credential: ErrMessage: ${e.errorMessage}, ErrType: ${e.type}") + throw e + } } - } - /** - * Encapsulates the get passkey API for credential manager in a less error-prone manner. - * - * @param request a get public key credential request JSON required by [GetCredentialRequest]. - * @return [GetCredentialResponse] containing the result of the credential retrieval. - */ - suspend fun getPasskey(request: String): GetCredentialResponse { - val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) - try { - return mCredMan.getCredential(activity, getRequest) - } catch (e: GetCredentialException) { - // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys - Log.i(TAG, "Error retrieving credential: ${e.message}") - throw e + /** + * Encapsulates the get passkey API for credential manager in a less error-prone manner. + * + * @param request a get public key credential request JSON required by [GetCredentialRequest]. + * @return [GetCredentialResponse] containing the result of the credential retrieval. + */ + suspend fun getPasskey(request: String): GetCredentialResponse { + val getRequest = GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request, null))) + try { + return mCredMan.getCredential(activity, getRequest) + } catch (e: GetCredentialException) { + // For error handling use guidance from https://developer.android.com/training/sign-in/passkeys + Log.i(TAG, "Error retrieving credential: ${e.message}") + throw e + } } - } } diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt index 04e4fb91a..ed65bd831 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.annotation.SuppressLint @@ -46,7 +62,7 @@ import java.security.spec.ECParameterSpec import java.security.spec.ECPoint import java.security.spec.EllipticCurve -class CredentialProviderDummyActivity: FragmentActivity() { +class CredentialProviderDummyActivity : FragmentActivity() { private val PERSONAL_ACCOUNT_ID: String = "" private val FAMILY_ACCOUNT_ID: String = "" @@ -85,7 +101,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { val biometricPrompt = BiometricPrompt( this, - { }, // Pass in your own executor + { }, // Pass in your own executor object : AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) @@ -109,7 +125,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { // Generate a credential key pair val spec = ECGenParameterSpec("secp256r1") - val keyPairGen = KeyPairGenerator.getInstance("EC"); + val keyPairGen = KeyPairGenerator.getInstance("EC") keyPairGen.initialize(spec) val keyPair = keyPairGen.genKeyPair() @@ -165,7 +181,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { @RequiresApi(VERSION_CODES.P) fun appInfoToOrigin(info: CallingAppInfo): String { val cert = info.signingInfo.apkContentsSigners[0].toByteArray() - val md = MessageDigest.getInstance("SHA-256"); + val md = MessageDigest.getInstance("SHA-256") val certHash = md.digest(cert) // This is the format for origin return "android:apk-key-hash:${b64Encode(certHash)}" @@ -240,7 +256,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { ) ) - //Set the final response back + // Set the final response back val result = Intent() val response = CreatePasswordResponse() PendingIntentHandler.setCreateCredentialResponse(result, response) @@ -300,10 +316,11 @@ class CredentialProviderDummyActivity: FragmentActivity() { val biometricPrompt = BiometricPrompt( this, - { }, // Pass in your own executor + { }, // Pass in your own executor object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError( - errorCode: Int, errString: CharSequence + errorCode: Int, + errString: CharSequence ) { super.onAuthenticationError(errorCode, errString) finish() @@ -330,7 +347,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { packageName = packageName ) - val sig = Signature.getInstance("SHA256withECDSA"); + val sig = Signature.getInstance("SHA256withECDSA") sig.initSign(privateKey) sig.update(response.dataToSign()) response.signature = sig.sign() @@ -401,9 +418,7 @@ class CredentialProviderDummyActivity: FragmentActivity() { } // [START android_identity_credential_pending_intent] - fun createSettingsPendingIntent(): PendingIntent - // [END android_identity_credential_pending_intent] - { + fun createSettingsPendingIntent(): PendingIntent { // [END android_identity_credential_pending_intent] return PendingIntent.getBroadcast(this, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) } @@ -468,7 +483,7 @@ data class CredentialsInfo( val passwords: List = listOf() ) -class ECPrivateKeyImpl: ECPrivateKey { +class ECPrivateKeyImpl : ECPrivateKey { override fun getAlgorithm(): String = "" override fun getFormat(): String = "" override fun getEncoded(): ByteArray = byteArrayOf() diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt index 2e21bec4d..05b06cce7 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -16,6 +32,7 @@ import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.PublicKeyCredential import androidx.credentials.exceptions.CreateCredentialException import com.example.identity.credentialmanager.ApiResult.Success +import java.io.StringWriter import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request.Builder @@ -24,7 +41,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.ResponseBody import org.json.JSONObject -import java.io.StringWriter import ru.gildor.coroutines.okhttp.await class Fido2ToCredmanMigration( @@ -44,12 +60,15 @@ class Fido2ToCredmanMigration( // ... val call = client.newCall( Builder() - .method("POST", jsonRequestBody { - name("attestation").value("none") - name("authenticatorSelection").objectValue { - name("residentKey").value("required") + .method( + "POST", + jsonRequestBody { + name("attestation").value("none") + name("authenticatorSelection").objectValue { + name("residentKey").value("required") + } } - }).build() + ).build() ) // ... } @@ -61,14 +80,17 @@ class Fido2ToCredmanMigration( Builder() .url("$BASE_URL/") .addHeader("Cookie", formatCookie(sessionId)) - .method("POST", jsonRequestBody { - name("attestation").value("none") - name("authenticatorSelection").objectValue { - name("authenticatorAttachment").value("platform") - name("userVerification").value("required") - name("residentKey").value("required") + .method( + "POST", + jsonRequestBody { + name("attestation").value("none") + name("authenticatorSelection").objectValue { + name("authenticatorAttachment").value("platform") + name("userVerification").value("required") + name("residentKey").value("required") + } } - }).build() + ).build() ) val response = call.await() return response.result("Error calling the api") { @@ -108,10 +130,13 @@ class Fido2ToCredmanMigration( * @return a JSON object. */ suspend fun signinRequest(): ApiResult { - val call = client.newCall(Builder().url(buildString { - append("$BASE_URL/signinRequest") - }).method("POST", jsonRequestBody {}) - .build() + val call = client.newCall( + Builder().url( + buildString { + append("$BASE_URL/signinRequest") + } + ).method("POST", jsonRequestBody {}) + .build() ) val response = call.await() return response.result("Error calling /signinRequest") { @@ -129,31 +154,36 @@ class Fido2ToCredmanMigration( * including the newly-registered one. */ suspend fun signinResponse( - sessionId: String, response: JSONObject, credentialId: String + sessionId: String, + response: JSONObject, + credentialId: String ): ApiResult { val call = client.newCall( Builder().url("$BASE_URL/signinResponse") - .addHeader("Cookie",formatCookie(sessionId)) - .method("POST", jsonRequestBody { - name("id").value(credentialId) - name("type").value(PUBLIC_KEY.toString()) - name("rawId").value(credentialId) - name("response").objectValue { - name("clientDataJSON").value( - response.getString("clientDataJSON") - ) - name("authenticatorData").value( - response.getString("authenticatorData") - ) - name("signature").value( - response.getString("signature") - ) - name("userHandle").value( - response.getString("userHandle") - ) + .addHeader("Cookie", formatCookie(sessionId)) + .method( + "POST", + jsonRequestBody { + name("id").value(credentialId) + name("type").value(PUBLIC_KEY.toString()) + name("rawId").value(credentialId) + name("response").objectValue { + name("clientDataJSON").value( + response.getString("clientDataJSON") + ) + name("authenticatorData").value( + response.getString("authenticatorData") + ) + name("signature").value( + response.getString("signature") + ) + name("userHandle").value( + response.getString("userHandle") + ) + } } - }).build() + ).build() ) val apiResponse = call.await() return apiResponse.result("Error calling /signingResponse") { @@ -169,11 +199,12 @@ class Fido2ToCredmanMigration( Toast.makeText( activity, "Fetching previously stored credentials", - Toast.LENGTH_SHORT) + Toast.LENGTH_SHORT + ) .show() var result: GetCredentialResponse? = null try { - val request= GetCredentialRequest( + val request = GetCredentialRequest( listOf( GetPublicKeyCredentialOption( creationResult.toString(), @@ -234,7 +265,7 @@ class Fido2ToCredmanMigration( } sealed class ApiResult { - class Success: ApiResult() + class Success : ApiResult() } class ApiException(message: String) : RuntimeException(message) diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt index 50beedaeb..77db763d3 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.annotation.SuppressLint @@ -33,7 +49,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) -class MyCredentialProviderService: CredentialProviderService() { +class MyCredentialProviderService : CredentialProviderService() { private val PERSONAL_ACCOUNT_ID: String = "" private val FAMILY_ACCOUNT_ID: String = "" private val CREATE_PASSKEY_INTENT: String = "" @@ -78,15 +94,19 @@ class MyCredentialProviderService: CredentialProviderService() { // account, and one for storing them to the 'Family' account. These // accounts are local to this sample app only. val createEntries: MutableList = mutableListOf() - createEntries.add( CreateEntry( - PERSONAL_ACCOUNT_ID, - createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT) - )) + createEntries.add( + CreateEntry( + PERSONAL_ACCOUNT_ID, + createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT) + ) + ) - createEntries.add( CreateEntry( - FAMILY_ACCOUNT_ID, - createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT) - )) + createEntries.add( + CreateEntry( + FAMILY_ACCOUNT_ID, + createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT) + ) + ) return BeginCreateCredentialResponse(createEntries) } @@ -101,7 +121,8 @@ class MyCredentialProviderService: CredentialProviderService() { return PendingIntent.getActivity( applicationContext, UNIQUE_REQ_CODE, - intent, ( + intent, + ( PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) @@ -120,10 +141,12 @@ class MyCredentialProviderService: CredentialProviderService() { callback: OutcomeReceiver, ) { if (isAppLocked()) { - callback.onResult(BeginGetCredentialResponse( - authenticationActions = mutableListOf( - AuthenticationAction( - unlockEntryTitle, createUnlockPendingIntent()) + callback.onResult( + BeginGetCredentialResponse( + authenticationActions = mutableListOf( + AuthenticationAction( + unlockEntryTitle, createUnlockPendingIntent() + ) ) ) ) @@ -142,7 +165,8 @@ class MyCredentialProviderService: CredentialProviderService() { private fun createUnlockPendingIntent(): PendingIntent { val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME) return PendingIntent.getActivity( - applicationContext, UNIQUE_REQUEST_CODE, intent, ( + applicationContext, UNIQUE_REQUEST_CODE, intent, + ( PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) @@ -156,7 +180,6 @@ class MyCredentialProviderService: CredentialProviderService() { // that are to be invoked through the PendingIntent(s) private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY" private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD" - } fun processGetCredentialRequest( diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt index f6dfe4485..747f72c15 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt @@ -14,7 +14,6 @@ * limitations under the License. */ - package com.example.identity.credentialmanager import android.content.Context @@ -46,283 +45,287 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import org.json.JSONObject -class PasskeyAndPasswordFunctions ( - context: Context, +class PasskeyAndPasswordFunctions( + context: Context, ) { - // [START android_identity_initialize_credman] - // Use your app or activity context to instantiate a client instance of - // CredentialManager. - private val credentialManager = CredentialManager.create(context) - // [END android_identity_initialize_credman] - private val activityContext = context + // [START android_identity_initialize_credman] + // Use your app or activity context to instantiate a client instance of + // CredentialManager. + private val credentialManager = CredentialManager.create(context) + // [END android_identity_initialize_credman] + private val activityContext = context - // Placeholder for TAG log value. - val TAG = "" - /** - * Retrieves a passkey from the credential manager. - * - * @param creationResult The result of the passkey creation operation. - * @param context The activity context from the Composable, to be used in Credential Manager APIs - * @return The [GetCredentialResponse] object containing the passkey, or null if an error occurred. - */ - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - fun signInFlow( - creationResult: JSONObject - ) { - val requestJson = creationResult.toString() - // [START android_identity_get_password_passkey_options] - // Retrieves the user's saved password for your app from their - // password provider. - val getPasswordOption = GetPasswordOption() + // Placeholder for TAG log value. + val TAG = "" + /** + * Retrieves a passkey from the credential manager. + * + * @param creationResult The result of the passkey creation operation. + * @param context The activity context from the Composable, to be used in Credential Manager APIs + * @return The [GetCredentialResponse] object containing the passkey, or null if an error occurred. + */ + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + fun signInFlow( + creationResult: JSONObject + ) { + val requestJson = creationResult.toString() + // [START android_identity_get_password_passkey_options] + // Retrieves the user's saved password for your app from their + // password provider. + val getPasswordOption = GetPasswordOption() - // Get passkey from the user's public key credential provider. - val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( - requestJson = requestJson - ) - // [END android_identity_get_password_passkey_options] - var result: GetCredentialResponse - // [START android_identity_get_credential_request] - val credentialRequest = GetCredentialRequest( - listOf(getPasswordOption, getPublicKeyCredentialOption), - ) - // [END android_identity_get_credential_request] - runBlocking { - // getPrepareCredential request - // [START android_identity_prepare_get_credential] - coroutineScope { - val response = credentialManager.prepareGetCredential( - GetCredentialRequest( - listOf( - getPublicKeyCredentialOption, - getPasswordOption - ) - ) + // Get passkey from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson ) - } - // [END android_identity_prepare_get_credential] - // getCredential request without handling exception. - // [START android_identity_launch_sign_in_flow_1] - coroutineScope { - try { - result = credentialManager.getCredential( - // Use an activity-based context to avoid undefined system UI - // launching behavior. - context = activityContext, - request = credentialRequest - ) - handleSignIn(result) - } catch (e: GetCredentialException) { - // Handle failure - } - } - // [END android_identity_launch_sign_in_flow_1] - // getCredential request adding some exception handling. - // [START android_identity_handle_exceptions_no_credential] - coroutineScope { - try { - result = credentialManager.getCredential( - context = activityContext, - request = credentialRequest - ) - } catch (e: GetCredentialException) { - Log.e("CredentialManager", "No credential available", e) + // [END android_identity_get_password_passkey_options] + var result: GetCredentialResponse + // [START android_identity_get_credential_request] + val credentialRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption), + ) + // [END android_identity_get_credential_request] + runBlocking { + // getPrepareCredential request + // [START android_identity_prepare_get_credential] + coroutineScope { + val response = credentialManager.prepareGetCredential( + GetCredentialRequest( + listOf( + getPublicKeyCredentialOption, + getPasswordOption + ) + ) + ) + } + // [END android_identity_prepare_get_credential] + // getCredential request without handling exception. + // [START android_identity_launch_sign_in_flow_1] + coroutineScope { + try { + result = credentialManager.getCredential( + // Use an activity-based context to avoid undefined system UI + // launching behavior. + context = activityContext, + request = credentialRequest + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + // Handle failure + } + } + // [END android_identity_launch_sign_in_flow_1] + // getCredential request adding some exception handling. + // [START android_identity_handle_exceptions_no_credential] + coroutineScope { + try { + result = credentialManager.getCredential( + context = activityContext, + request = credentialRequest + ) + } catch (e: GetCredentialException) { + Log.e("CredentialManager", "No credential available", e) + } + } + // [END android_identity_handle_exceptions_no_credential] } - } - // [END android_identity_handle_exceptions_no_credential] } - } - fun autofillImplementation( - requestJson: String - ) { - // [START android_identity_autofill_construct_request] - // Retrieves the user's saved password for your app. - val getPasswordOption = GetPasswordOption() + fun autofillImplementation( + requestJson: String + ) { + // [START android_identity_autofill_construct_request] + // Retrieves the user's saved password for your app. + val getPasswordOption = GetPasswordOption() - // Get a passkey from the user's public key credential provider. - val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( - requestJson = requestJson - ) + // Get a passkey from the user's public key credential provider. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson + ) - val getCredRequest = GetCredentialRequest( - listOf(getPasswordOption, getPublicKeyCredentialOption) - ) - // [END android_identity_autofill_construct_request] + val getCredRequest = GetCredentialRequest( + listOf(getPasswordOption, getPublicKeyCredentialOption) + ) + // [END android_identity_autofill_construct_request] - runBlocking { - // [START android_identity_autofill_get_credential_api] - coroutineScope { - try { - val result = credentialManager.getCredential( - context = activityContext, // Use an activity-based context. - request = getCredRequest - ) - handleSignIn(result); - } catch (e: GetCredentialException) { - handleFailure(e); + runBlocking { + // [START android_identity_autofill_get_credential_api] + coroutineScope { + try { + val result = credentialManager.getCredential( + context = activityContext, // Use an activity-based context. + request = getCredRequest + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + handleFailure(e) + } + } + // [END android_identity_autofill_get_credential_api] } - } - // [END android_identity_autofill_get_credential_api] - } - val usernameEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) - val passwordEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + val usernameEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) + val passwordEditText: androidx.appcompat.widget.AppCompatEditText = AppCompatEditText(activityContext) - // [START android_identity_autofill_enable_edit_text] - usernameEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( - getCredRequest) { response -> handleSignIn(response) - } + // [START android_identity_autofill_enable_edit_text] + usernameEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest + ) { response -> + handleSignIn(response) + } - passwordEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( - getCredRequest) { response -> handleSignIn(response) + passwordEditText.pendingGetCredentialRequest = PendingGetCredentialRequest( + getCredRequest + ) { response -> + handleSignIn(response) + } + // [END android_identity_autofill_enable_edit_text] } - // [END android_identity_autofill_enable_edit_text] - } - // [START android_identity_launch_sign_in_flow_2] - fun handleSignIn(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential + // [START android_identity_launch_sign_in_flow_2] + fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential - when (credential) { - is PublicKeyCredential -> { - val responseJson = credential.authenticationResponseJson - // Share responseJson i.e. a GetCredentialResponse on your server to - // validate and authenticate - } + when (credential) { + is PublicKeyCredential -> { + val responseJson = credential.authenticationResponseJson + // Share responseJson i.e. a GetCredentialResponse on your server to + // validate and authenticate + } - is PasswordCredential -> { - val username = credential.id - val password = credential.password - // Use id and password to send to your server to validate - // and authenticate - } + is PasswordCredential -> { + val username = credential.id + val password = credential.password + // Use id and password to send to your server to validate + // and authenticate + } - is CustomCredential -> { - // If you are also using any external sign-in libraries, parse them - // here with the utility functions provided. - if (credential.type == ExampleCustomCredential.TYPE) { - try { - val ExampleCustomCredential = - ExampleCustomCredential.createFrom(credential.data) - // Extract the required credentials and complete the authentication as per - // the federated sign in or any external sign in library flow - } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) { - // Unlikely to happen. If it does, you likely need to update the dependency - // version of your external sign-in library. - Log.e(TAG, "Failed to parse an ExampleCustomCredential", e) - } - } else { - // Catch any unrecognized custom credential type here. - Log.e(TAG, "Unexpected type of credential") + is CustomCredential -> { + // If you are also using any external sign-in libraries, parse them + // here with the utility functions provided. + if (credential.type == ExampleCustomCredential.TYPE) { + try { + val ExampleCustomCredential = + ExampleCustomCredential.createFrom(credential.data) + // Extract the required credentials and complete the authentication as per + // the federated sign in or any external sign in library flow + } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) { + // Unlikely to happen. If it does, you likely need to update the dependency + // version of your external sign-in library. + Log.e(TAG, "Failed to parse an ExampleCustomCredential", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } } - } - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } } - } - // [END android_identity_launch_sign_in_flow_2] + // [END android_identity_launch_sign_in_flow_2] - // [START android_identity_create_passkey] - suspend fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) { - val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( - // Contains the request in JSON format. Uses the standard WebAuthn - // web JSON spec. - requestJson = requestJson, - // Defines whether you prefer to use only immediately available - // credentials, not hybrid credentials, to fulfill this request. - // This value is false by default. - preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, - ) - - // Execute CreateCredentialRequest asynchronously to register credentials - // for a user account. Handle success and failure cases with the result and - // exceptions, respectively. - coroutineScope { - try { - val result = credentialManager.createCredential( - // Use an activity-based context to avoid undefined system - // UI launching behavior - context = activityContext, - request = createPublicKeyCredentialRequest, + // [START android_identity_create_passkey] + suspend fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) { + val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest( + // Contains the request in JSON format. Uses the standard WebAuthn + // web JSON spec. + requestJson = requestJson, + // Defines whether you prefer to use only immediately available + // credentials, not hybrid credentials, to fulfill this request. + // This value is false by default. + preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, ) - // Handle passkey creation result - } catch (e : CreateCredentialException){ - handleFailure(e) - } + + // Execute CreateCredentialRequest asynchronously to register credentials + // for a user account. Handle success and failure cases with the result and + // exceptions, respectively. + coroutineScope { + try { + val result = credentialManager.createCredential( + // Use an activity-based context to avoid undefined system + // UI launching behavior + context = activityContext, + request = createPublicKeyCredentialRequest, + ) + // Handle passkey creation result + } catch (e: CreateCredentialException) { + handleFailure(e) + } + } } - } - // [END android_identity_create_passkey] + // [END android_identity_create_passkey] - // [START android_identity_handle_create_passkey_failure] - fun handleFailure(e: CreateCredentialException) { - when (e) { - is CreatePublicKeyCredentialDomException -> { - // Handle the passkey DOM errors thrown according to the - // WebAuthn spec. - } - is CreateCredentialCancellationException -> { - // The user intentionally canceled the operation and chose not - // to register the credential. - } - is CreateCredentialInterruptedException -> { - // Retry-able error. Consider retrying the call. - } - is CreateCredentialProviderConfigurationException -> { - // Your app is missing the provider configuration dependency. - // Most likely, you're missing the - // "credentials-play-services-auth" module. - } - is CreateCredentialCustomException -> { - // You have encountered an error from a 3rd-party SDK. If you - // make the API call with a request object that's a subclass of - // CreateCustomCredentialRequest using a 3rd-party SDK, then you - // should check for any custom exception type constants within - // that SDK to match with e.type. Otherwise, drop or log the - // exception. - } - else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") + // [START android_identity_handle_create_passkey_failure] + fun handleFailure(e: CreateCredentialException) { + when (e) { + is CreatePublicKeyCredentialDomException -> { + // Handle the passkey DOM errors thrown according to the + // WebAuthn spec. + } + is CreateCredentialCancellationException -> { + // The user intentionally canceled the operation and chose not + // to register the credential. + } + is CreateCredentialInterruptedException -> { + // Retry-able error. Consider retrying the call. + } + is CreateCredentialProviderConfigurationException -> { + // Your app is missing the provider configuration dependency. + // Most likely, you're missing the + // "credentials-play-services-auth" module. + } + is CreateCredentialCustomException -> { + // You have encountered an error from a 3rd-party SDK. If you + // make the API call with a request object that's a subclass of + // CreateCustomCredentialRequest using a 3rd-party SDK, then you + // should check for any custom exception type constants within + // that SDK to match with e.type. Otherwise, drop or log the + // exception. + } + else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") + } } - } - // [END android_identity_handle_create_passkey_failure] + // [END android_identity_handle_create_passkey_failure] - fun handleFailure(e: GetCredentialException) { } + fun handleFailure(e: GetCredentialException) { } - // [START android_identity_register_password] - suspend fun registerPassword(username: String, password: String) { - // Initialize a CreatePasswordRequest object. - val createPasswordRequest = - CreatePasswordRequest(id = username, password = password) + // [START android_identity_register_password] + suspend fun registerPassword(username: String, password: String) { + // Initialize a CreatePasswordRequest object. + val createPasswordRequest = + CreatePasswordRequest(id = username, password = password) - // Create credential and handle result. - coroutineScope { - try { - val result = - credentialManager.createCredential( - // Use an activity based context to avoid undefined - // system UI launching behavior. - activityContext, - createPasswordRequest - ) - // Handle register password result - } catch (e: CreateCredentialException) { - handleFailure(e) - } + // Create credential and handle result. + coroutineScope { + try { + val result = + credentialManager.createCredential( + // Use an activity based context to avoid undefined + // system UI launching behavior. + activityContext, + createPasswordRequest + ) + // Handle register password result + } catch (e: CreateCredentialException) { + handleFailure(e) + } + } } - } - // [END android_identity_register_password] + // [END android_identity_register_password] } sealed class ExampleCustomCredential { - class ExampleCustomCredentialParsingException : Throwable() {} + class ExampleCustomCredentialParsingException : Throwable() - companion object { - fun createFrom(data: Bundle): PublicKeyCredential { - return PublicKeyCredential("") - } + companion object { + fun createFrom(data: Bundle): PublicKeyCredential { + return PublicKeyCredential("") + } - const val TYPE: String = "" - } -} \ No newline at end of file + const val TYPE: String = "" + } +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt index 05119cd96..f13052b49 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.app.Activity @@ -25,213 +41,216 @@ const val TAG = "" // [START android_identity_create_listener_passkeys] // The class talking to Javascript should inherit: class PasskeyWebListener( - private val activity: Activity, - private val coroutineScope: CoroutineScope, - private val credentialManagerHandler: CredentialManagerHandler + private val activity: Activity, + private val coroutineScope: CoroutineScope, + private val credentialManagerHandler: CredentialManagerHandler ) : WebViewCompat.WebMessageListener { - /** havePendingRequest is true if there is an outstanding WebAuthn request. - There is only ever one request outstanding at a time. */ - private var havePendingRequest = false - - /** pendingRequestIsDoomed is true if the WebView has navigated since - starting a request. The FIDO module cannot be canceled, but the response - will never be delivered in this case. */ - private var pendingRequestIsDoomed = false - - /** replyChannel is the port that the page is listening for a response on. - It is valid if havePendingRequest is true. */ - private var replyChannel: ReplyChannel? = null - - /** - * Called by the page during a WebAuthn request. - * - * @param view Creates the WebView. - * @param message The message sent from the client using injected JavaScript. - * @param sourceOrigin The origin of the HTTPS request. Should not be null. - * @param isMainFrame Should be set to true. Embedded frames are not - supported. - * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in - the Channel. - * @return The message response. - */ - @UiThread - override fun onPostMessage( - view: WebView, - message: WebMessageCompat, - sourceOrigin: Uri, - isMainFrame: Boolean, - replyProxy: JavaScriptReplyProxy, - ) { - val messageData = message.data ?: return - onRequest( - messageData, - sourceOrigin, - isMainFrame, - JavaScriptReplyChannel(replyProxy) - ) - } - - private fun onRequest( - msg: String, - sourceOrigin: Uri, - isMainFrame: Boolean, - reply: ReplyChannel, - ) { - msg?.let { - val jsonObj = JSONObject(msg); - val type = jsonObj.getString(TYPE_KEY) - val message = jsonObj.getString(REQUEST_KEY) - - if (havePendingRequest) { - postErrorMessage(reply, "The request already in progress", type) - return - } - - replyChannel = reply - if (!isMainFrame) { - reportFailure("Requests from subframes are not supported", type) - return - } - val originScheme = sourceOrigin.scheme - if (originScheme == null || originScheme.lowercase() != "https") { - reportFailure("WebAuthn not permitted for current URL", type) - return - } - - // Verify that origin belongs to your website, - // it's because the unknown origin may gain credential info. - // if (isUnknownOrigin(originScheme)) { - // return - // } - - havePendingRequest = true - pendingRequestIsDoomed = false - - // Use a temporary "replyCurrent" variable to send the data back, while - // resetting the main "replyChannel" variable to null so it’s ready for - // the next request. - val replyCurrent = replyChannel - if (replyCurrent == null) { - Log.i(TAG, "The reply channel was null, cannot continue") - return; - } - - when (type) { - CREATE_UNIQUE_KEY -> - this.coroutineScope.launch { - handleCreateFlow(credentialManagerHandler, message, replyCurrent) - } - - GET_UNIQUE_KEY -> this.coroutineScope.launch { - handleGetFlow(credentialManagerHandler, message, replyCurrent) - } + /** havePendingRequest is true if there is an outstanding WebAuthn request. + There is only ever one request outstanding at a time. */ + private var havePendingRequest = false + + /** pendingRequestIsDoomed is true if the WebView has navigated since + starting a request. The FIDO module cannot be canceled, but the response + will never be delivered in this case. */ + private var pendingRequestIsDoomed = false + + /** replyChannel is the port that the page is listening for a response on. + It is valid if havePendingRequest is true. */ + private var replyChannel: ReplyChannel? = null - else -> Log.i(TAG, "Incorrect request json") - } + /** + * Called by the page during a WebAuthn request. + * + * @param view Creates the WebView. + * @param message The message sent from the client using injected JavaScript. + * @param sourceOrigin The origin of the HTTPS request. Should not be null. + * @param isMainFrame Should be set to true. Embedded frames are not + supported. + * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in + the Channel. + * @return The message response. + */ + @UiThread + override fun onPostMessage( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) { + val messageData = message.data ?: return + onRequest( + messageData, + sourceOrigin, + isMainFrame, + JavaScriptReplyChannel(replyProxy) + ) } - } - - private suspend fun handleCreateFlow( - credentialManagerHandler: CredentialManagerHandler, - message: String, - reply: ReplyChannel, - ) { - try { - havePendingRequest = false - pendingRequestIsDoomed = false - val response = credentialManagerHandler.createPasskey(message) - val successArray = ArrayList(); - successArray.add("success"); - successArray.add(JSONObject(response.registrationResponseJson)); - successArray.add(CREATE_UNIQUE_KEY); - reply.send(JSONArray(successArray).toString()) - replyChannel = null // setting initial replyChannel for the next request - } catch (e: CreateCredentialException) { - reportFailure( - "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", - CREATE_UNIQUE_KEY - ) - } catch (t: Throwable) { - reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + + private fun onRequest( + msg: String, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: ReplyChannel, + ) { + msg?.let { + val jsonObj = JSONObject(msg) + val type = jsonObj.getString(TYPE_KEY) + val message = jsonObj.getString(REQUEST_KEY) + + if (havePendingRequest) { + postErrorMessage(reply, "The request already in progress", type) + return + } + + replyChannel = reply + if (!isMainFrame) { + reportFailure("Requests from subframes are not supported", type) + return + } + val originScheme = sourceOrigin.scheme + if (originScheme == null || originScheme.lowercase() != "https") { + reportFailure("WebAuthn not permitted for current URL", type) + return + } + + // Verify that origin belongs to your website, + // it's because the unknown origin may gain credential info. + // if (isUnknownOrigin(originScheme)) { + // return + // } + + havePendingRequest = true + pendingRequestIsDoomed = false + + // Use a temporary "replyCurrent" variable to send the data back, while + // resetting the main "replyChannel" variable to null so it’s ready for + // the next request. + val replyCurrent = replyChannel + if (replyCurrent == null) { + Log.i(TAG, "The reply channel was null, cannot continue") + return + } + + when (type) { + CREATE_UNIQUE_KEY -> + this.coroutineScope.launch { + handleCreateFlow(credentialManagerHandler, message, replyCurrent) + } + + GET_UNIQUE_KEY -> this.coroutineScope.launch { + handleGetFlow(credentialManagerHandler, message, replyCurrent) + } + + else -> Log.i(TAG, "Incorrect request json") + } + } } - } - - companion object { - /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ - const val INTERFACE_NAME = "__webauthn_interface__" - const val TYPE_KEY = "type" - const val REQUEST_KEY = "request" - const val CREATE_UNIQUE_KEY = "create" - const val GET_UNIQUE_KEY = "get" - /** INJECTED_VAL is the minified version of the JavaScript code described at this class - * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ - const val INJECTED_VAL = """ + + private suspend fun handleCreateFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val response = credentialManagerHandler.createPasskey(message) + val successArray = ArrayList() + successArray.add("success") + successArray.add(JSONObject(response.registrationResponseJson)) + successArray.add(CREATE_UNIQUE_KEY) + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for the next request + } catch (e: CreateCredentialException) { + reportFailure( + "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", + CREATE_UNIQUE_KEY + ) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) + } + } + + companion object { + /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */ + const val INTERFACE_NAME = "__webauthn_interface__" + const val TYPE_KEY = "type" + const val REQUEST_KEY = "request" + const val CREATE_UNIQUE_KEY = "create" + const val GET_UNIQUE_KEY = "get" + /** INJECTED_VAL is the minified version of the JavaScript code described at this class + * heading. The non minified form is found at credmanweb/javascript/encode.js.*/ + const val INJECTED_VAL = """ var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)}; """ - } - // [END android_identity_create_listener_passkeys] - - // Handles the get flow in a less error-prone way - private suspend fun handleGetFlow( - credentialManagerHandler: CredentialManagerHandler, - message: String, - reply: ReplyChannel, - ) { - try { - havePendingRequest = false - pendingRequestIsDoomed = false - val r = credentialManagerHandler.getPasskey(message) - val successArray = ArrayList(); - successArray.add("success"); - successArray.add(JSONObject( - (r.credential as PublicKeyCredential).authenticationResponseJson)) - successArray.add(GET_UNIQUE_KEY); - reply.send(JSONArray(successArray).toString()) - replyChannel = null // setting initial replyChannel for next request given temp 'reply' - } catch (e: GetCredentialException) { - reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) - } catch (t: Throwable) { - reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) } - } - - /** Sends an error result to the page. */ - private fun reportFailure(message: String, type: String) { - havePendingRequest = false - pendingRequestIsDoomed = false - val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE - replyChannel = null - postErrorMessage(reply, message, type) - } - - private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { - Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage"); - val array: MutableList = ArrayList() - array.add("error") - array.add(errorMessage) - array.add(type) - reply.send(JSONArray(array).toString()) - var toastMsg = errorMessage - Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() - } - - // [START android_identity_javascript_reply_channel] - // The setup for the reply channel allows communication with JavaScript. - private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : - ReplyChannel { - override fun send(message: String?) { - try { - reply.postMessage(message!!) - } catch (t: Throwable) { - Log.i(TAG, "Reply failure due to: " + t.message); - } + // [END android_identity_create_listener_passkeys] + + // Handles the get flow in a less error-prone way + private suspend fun handleGetFlow( + credentialManagerHandler: CredentialManagerHandler, + message: String, + reply: ReplyChannel, + ) { + try { + havePendingRequest = false + pendingRequestIsDoomed = false + val r = credentialManagerHandler.getPasskey(message) + val successArray = ArrayList() + successArray.add("success") + successArray.add( + JSONObject( + (r.credential as PublicKeyCredential).authenticationResponseJson + ) + ) + successArray.add(GET_UNIQUE_KEY) + reply.send(JSONArray(successArray).toString()) + replyChannel = null // setting initial replyChannel for next request given temp 'reply' + } catch (e: GetCredentialException) { + reportFailure("Error: ${e.errorMessage} w type: ${e.type} w obj: $e", GET_UNIQUE_KEY) + } catch (t: Throwable) { + reportFailure("Error: ${t.message}", GET_UNIQUE_KEY) + } + } + + /** Sends an error result to the page. */ + private fun reportFailure(message: String, type: String) { + havePendingRequest = false + pendingRequestIsDoomed = false + val reply: ReplyChannel = replyChannel!! // verifies non null by throwing NPE + replyChannel = null + postErrorMessage(reply, message, type) + } + + private fun postErrorMessage(reply: ReplyChannel, errorMessage: String, type: String) { + Log.i(TAG, "Sending error message back to the page via replyChannel $errorMessage") + val array: MutableList = ArrayList() + array.add("error") + array.add(errorMessage) + array.add(type) + reply.send(JSONArray(array).toString()) + var toastMsg = errorMessage + Toast.makeText(this.activity.applicationContext, toastMsg, Toast.LENGTH_SHORT).show() + } + + // [START android_identity_javascript_reply_channel] + // The setup for the reply channel allows communication with JavaScript. + private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : + ReplyChannel { + override fun send(message: String?) { + try { + reply.postMessage(message!!) + } catch (t: Throwable) { + Log.i(TAG, "Reply failure due to: " + t.message) + } + } + } + + // ReplyChannel is the interface where replies to the embedded site are + // sent. This allows for testing since AndroidX bans mocking its objects. + interface ReplyChannel { + fun send(message: String?) } - } - - // ReplyChannel is the interface where replies to the embedded site are - // sent. This allows for testing since AndroidX bans mocking its objects. - interface ReplyChannel { - fun send(message: String?) - } - // [END android_identity_javascript_reply_channel] -} \ No newline at end of file + // [END android_identity_javascript_reply_channel] +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt index f051e7355..28c58ef25 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt @@ -30,153 +30,151 @@ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import kotlinx.coroutines.coroutineScope -import kotlin.math.sign const val WEB_CLIENT_ID = "" -class SignInWithGoogleFunctions ( - context: Context, +class SignInWithGoogleFunctions( + context: Context, ) { - private val credentialManager = CredentialManager.create(context) - private val activityContext = context - // Placeholder for TAG log value. - val TAG = "" - - fun createGoogleIdOption(nonce: String): GetGoogleIdOption { - // [START android_identity_siwg_instantiate_request] - val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(true) - .setServerClientId(WEB_CLIENT_ID) - .setAutoSelectEnabled(true) - // nonce string to use when generating a Google ID token - .setNonce(nonce) - .build() - // [END android_identity_siwg_instantiate_request] - - return googleIdOption - } - - private val googleIdOption = createGoogleIdOption("") - - suspend fun signInUser() { - // [START android_identity_siwg_signin_flow_create_request] - val request: GetCredentialRequest = GetCredentialRequest.Builder() - .addCredentialOption(googleIdOption) - .build() - - coroutineScope { - try { - val result = credentialManager.getCredential( - request = request, - context = activityContext, - ) - handleSignIn(result) - } catch (e: GetCredentialException) { - // Handle failure - } + private val credentialManager = CredentialManager.create(context) + private val activityContext = context + // Placeholder for TAG log value. + val TAG = "" + + fun createGoogleIdOption(nonce: String): GetGoogleIdOption { + // [START android_identity_siwg_instantiate_request] + val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(true) + .setServerClientId(WEB_CLIENT_ID) + .setAutoSelectEnabled(true) + // nonce string to use when generating a Google ID token + .setNonce(nonce) + .build() + // [END android_identity_siwg_instantiate_request] + + return googleIdOption } - // [END android_identity_siwg_signin_flow_create_request] - } - - // [START android_identity_siwg_signin_flow_handle_signin] - fun handleSignIn(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential - val responseJson: String - - when (credential) { - - // Passkey credential - is PublicKeyCredential -> { - // Share responseJson such as a GetCredentialResponse to your server to validate and - // authenticate - responseJson = credential.authenticationResponseJson - } - - // Password credential - is PasswordCredential -> { - // Send ID and password to your server to validate and authenticate. - val username = credential.id - val password = credential.password - } - - // GoogleIdToken credential - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - // Use googleIdTokenCredential and extract the ID to validate and - // authenticate on your server. - val googleIdTokenCredential = GoogleIdTokenCredential - .createFrom(credential.data) - // You can use the members of googleIdTokenCredential directly for UX - // purposes, but don't use them to store or control access to user - // data. For that you first need to validate the token: - // pass googleIdTokenCredential.getIdToken() to the backend server. - // see [validation instructions](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) - } catch (e: GoogleIdTokenParsingException) { - Log.e(TAG, "Received an invalid google id token response", e) - } - } else { - // Catch any unrecognized custom credential type here. - Log.e(TAG, "Unexpected type of credential") - } - } - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } - } - } - // [END android_identity_siwg_signin_flow_handle_signin] - - fun createGoogleSignInWithGoogleOption(nonce: String): GetSignInWithGoogleOption { - // [START android_identity_siwg_get_siwg_option] - val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder( - serverClientId = WEB_CLIENT_ID - ).setNonce(nonce) - .build() - // [END android_identity_siwg_get_siwg_option] - - return signInWithGoogleOption - } - - // [START android_identity_handle_siwg_option] - fun handleSignInWithGoogleOption(result: GetCredentialResponse) { - // Handle the successfully returned credential. - val credential = result.credential - - when (credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - // Use googleIdTokenCredential and extract id to validate and - // authenticate on your server. - val googleIdTokenCredential = GoogleIdTokenCredential - .createFrom(credential.data) - } catch (e: GoogleIdTokenParsingException) { - Log.e(TAG, "Received an invalid google id token response", e) - } + private val googleIdOption = createGoogleIdOption("") + + suspend fun signInUser() { + // [START android_identity_siwg_signin_flow_create_request] + val request: GetCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + coroutineScope { + try { + val result = credentialManager.getCredential( + request = request, + context = activityContext, + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + // Handle failure + } } - else { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") + // [END android_identity_siwg_signin_flow_create_request] + } + + // [START android_identity_siwg_signin_flow_handle_signin] + fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential + val responseJson: String + + when (credential) { + + // Passkey credential + is PublicKeyCredential -> { + // Share responseJson such as a GetCredentialResponse to your server to validate and + // authenticate + responseJson = credential.authenticationResponseJson + } + + // Password credential + is PasswordCredential -> { + // Send ID and password to your server to validate and authenticate. + val username = credential.id + val password = credential.password + } + + // GoogleIdToken credential + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + // Use googleIdTokenCredential and extract the ID to validate and + // authenticate on your server. + val googleIdTokenCredential = GoogleIdTokenCredential + .createFrom(credential.data) + // You can use the members of googleIdTokenCredential directly for UX + // purposes, but don't use them to store or control access to user + // data. For that you first need to validate the token: + // pass googleIdTokenCredential.getIdToken() to the backend server. + // see [validation instructions](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token) + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } } - } + } + // [END android_identity_siwg_signin_flow_handle_signin] - else -> { - // Catch any unrecognized credential type here. - Log.e(TAG, "Unexpected type of credential") - } + fun createGoogleSignInWithGoogleOption(nonce: String): GetSignInWithGoogleOption { + // [START android_identity_siwg_get_siwg_option] + val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder( + serverClientId = WEB_CLIENT_ID + ).setNonce(nonce) + .build() + // [END android_identity_siwg_get_siwg_option] + + return signInWithGoogleOption + } + + // [START android_identity_handle_siwg_option] + fun handleSignInWithGoogleOption(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential + + when (credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + // Use googleIdTokenCredential and extract id to validate and + // authenticate on your server. + val googleIdTokenCredential = GoogleIdTokenCredential + .createFrom(credential.data) + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) + } + } else { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + } + // [END android_identity_handle_siwg_option] + + fun googleIdOptionFalseFilter() { + // [START android_identity_siwg_instantiate_request_2] + val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(WEB_CLIENT_ID) + .build() + // [END android_identity_siwg_instantiate_request_2] } - } - // [END android_identity_handle_siwg_option] - - fun googleIdOptionFalseFilter() { - // [START android_identity_siwg_instantiate_request_2] - val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(false) - .setServerClientId(WEB_CLIENT_ID) - .build() - // [END android_identity_siwg_instantiate_request_2] - } -} \ No newline at end of file +} diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt index 52445b6df..b61ddbe5c 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.os.Build.VERSION_CODES @@ -11,7 +27,7 @@ import androidx.credentials.provider.BiometricPromptData import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.PendingIntentHandler -class SingleTap: ComponentActivity() { +class SingleTap : ComponentActivity() { private val x: Any? = null private val TAG: String = "" @@ -35,7 +51,7 @@ class SingleTap: ComponentActivity() { allowedAuthenticators = allowedAuthenticator ) ) - // [END android_identity_single_tap_set_biometric_prompt_data] + // [END android_identity_single_tap_set_biometric_prompt_data] when (x) { // [START android_identity_single_tap_pk_creation] @@ -105,8 +121,7 @@ class SingleTap: ComponentActivity() { if (biometricPromptResult == null) { // Do your own authentication flow, if needed - } - else if (biometricPromptResult.isSuccessful) { + } else if (biometricPromptResult.isSuccessful) { createPasskey( publicKeyRequest.requestJson, createRequest.callingAppInfo, @@ -150,8 +165,7 @@ class SingleTap: ComponentActivity() { // Add your logic based on what needs to be done // after getting biometrics - if (biometricPromptResult == null) - { + if (biometricPromptResult == null) { // Do your own authentication flow, if necessary } else if (biometricPromptResult.isSuccessful) { diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt index 0dc66ceba..f37f6d9da 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt @@ -1,11 +1,23 @@ +/* + * 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.identity.credentialmanager -import android.annotation.SuppressLint import android.content.Context import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope import androidx.credentials.CredentialManager import androidx.credentials.GetCredentialRequest import androidx.credentials.GetCredentialResponse diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt index 725ad7ff8..935da4971 100644 --- a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt +++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt @@ -1,3 +1,19 @@ +/* + * 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.identity.credentialmanager import android.graphics.Bitmap @@ -13,70 +29,73 @@ import androidx.webkit.WebViewFeature import kotlinx.coroutines.CoroutineScope class WebViewMainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - // [START android_identity_initialize_the_webview] - val credentialManagerHandler = CredentialManagerHandler(this) + // [START android_identity_initialize_the_webview] + val credentialManagerHandler = CredentialManagerHandler(this) - setContent { - val coroutineScope = rememberCoroutineScope() - AndroidView(factory = { - WebView(it).apply { - settings.javaScriptEnabled = true + setContent { + val coroutineScope = rememberCoroutineScope() + AndroidView( + factory = { + WebView(it).apply { + settings.javaScriptEnabled = true - // Test URL: - val url = "https://passkeys-codelab.glitch.me/" - val listenerSupported = WebViewFeature.isFeatureSupported( - WebViewFeature.WEB_MESSAGE_LISTENER - ) - if (listenerSupported) { - // Inject local JavaScript that calls Credential Manager. - hookWebAuthnWithListener( - this, this@WebViewMainActivity, - coroutineScope, credentialManagerHandler + // Test URL: + val url = "https://passkeys-codelab.glitch.me/" + val listenerSupported = WebViewFeature.isFeatureSupported( + WebViewFeature.WEB_MESSAGE_LISTENER + ) + if (listenerSupported) { + // Inject local JavaScript that calls Credential Manager. + hookWebAuthnWithListener( + this, this@WebViewMainActivity, + coroutineScope, credentialManagerHandler + ) + } else { + // Fallback routine for unsupported API levels. + } + loadUrl(url) + } + } ) - } else { - // Fallback routine for unsupported API levels. - } - loadUrl(url) } - } - ) + // [END android_identity_initialize_the_webview] } - // [END android_identity_initialize_the_webview] - } - /** - * Connects the local app logic with the web page via injection of javascript through a - * WebListener. Handles ensuring the [PasskeyWebListener] is hooked up to the webView page - * if compatible. - */ - fun hookWebAuthnWithListener( - webView: WebView, - activity: WebViewMainActivity, - coroutineScope: CoroutineScope, - credentialManagerHandler: CredentialManagerHandler - ) { - // [START android_identity_create_webview_object] - val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler) + /** + * Connects the local app logic with the web page via injection of javascript through a + * WebListener. Handles ensuring the [PasskeyWebListener] is hooked up to the webView page + * if compatible. + */ + fun hookWebAuthnWithListener( + webView: WebView, + activity: WebViewMainActivity, + coroutineScope: CoroutineScope, + credentialManagerHandler: CredentialManagerHandler + ) { + // [START android_identity_create_webview_object] + val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler) - val webViewClient = object : WebViewClient() { - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) - } - } + val webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) + } + } - webView.webViewClient = webViewClient - // [END android_identity_create_webview_object] + webView.webViewClient = webViewClient + // [END android_identity_create_webview_object] - // [START android_identity_set_web] - val rules = setOf("*") - if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { - WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME, - rules, passkeyWebListener) + // [START android_identity_set_web] + val rules = setOf("*") + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + webView, PasskeyWebListener.INTERFACE_NAME, + rules, passkeyWebListener + ) + } + // [END android_identity_set_web] } - // [END android_identity_set_web] - } -} \ No newline at end of file +} diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt index 9dea9a720..0b804c985 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/HomeViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt index babf205c6..0d6252d77 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/Repository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. @@ -16,6 +16,7 @@ package com.example.android.coroutines.testing +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,7 +24,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean // [START coroutine_test_repo_dispatcher_injection] // Example class demonstrating dispatcher use cases diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt index b53dee883..26b888543 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt index e06b7240c..7ac89ce50 100644 --- a/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt +++ b/kotlin/src/main/kotlin/com/example/android/coroutines/testing/UserState.kt @@ -1,3 +1,19 @@ +/* + * 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.android.coroutines.testing.scope import kotlinx.coroutines.CoroutineScope diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt index 727a7c8a7..05a6794c0 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/CreatingYourOwn.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt index bbe6b117e..81f206ad1 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatcherTypesTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt index 2f3228ccf..c586f2570 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/DispatchersOutsideTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. @@ -32,7 +32,6 @@ import org.junit.runner.RunWith // Helper function to let code below compile private fun ExampleRepository(): Repository = Repository(Dispatchers.IO) - // [START coroutine_test_repo_with_rule_blank] class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } @@ -89,5 +88,3 @@ class DispatchersOutsideTests { } // [END coroutine_test_repo_without_rule] } - - diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt index d4efc2e58..b588e90ca 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt index 54a5a9bdd..b7b926edb 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/HomeViewModelTestUsingRule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt index ec0a47c49..5cd4b2bab 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/RepositoryTest.kt @@ -1,3 +1,19 @@ +/* + * 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.android.coroutines.testing import kotlinx.coroutines.test.StandardTestDispatcher diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt index bf749c095..da07e4936 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/StandardTestDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. @@ -51,4 +51,4 @@ class StandardTestDispatcherTest_Fixed { assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes } // [END coroutine_test_standard_fixed] -} \ No newline at end of file +} diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt index 90e0ab79f..2dc83b6a3 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/SuspendingFunctionTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt index ae488974a..b2230c034 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UnconfinedTestDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. diff --git a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt index e2c625e4e..3b2ec514b 100644 --- a/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt +++ b/kotlin/src/test/kotlin/com/example/android/coroutines/testing/UserStateTest.kt @@ -1,3 +1,19 @@ +/* + * 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. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) package com.example.android.coroutines.testing diff --git a/views/src/main/java/insets/SystemBarProtectionSnippet.kt b/views/src/main/java/insets/SystemBarProtectionSnippet.kt index bd44cd4c9..c21011b44 100644 --- a/views/src/main/java/insets/SystemBarProtectionSnippet.kt +++ b/views/src/main/java/insets/SystemBarProtectionSnippet.kt @@ -40,7 +40,7 @@ class SystemBarProtectionSnippet : AppCompatActivity() { ) { v: View, insets: WindowInsetsCompat -> val innerPadding = insets.getInsets( WindowInsetsCompat.Type.systemBars() or - WindowInsetsCompat.Type.displayCutout() + WindowInsetsCompat.Type.displayCutout() ) v.setPadding( innerPadding.left, diff --git a/watchfacepush/validator/build.gradle.kts b/watchfacepush/validator/build.gradle.kts index 66cb693bf..0b9289a5c 100644 --- a/watchfacepush/validator/build.gradle.kts +++ b/watchfacepush/validator/build.gradle.kts @@ -26,6 +26,14 @@ application { mainClass.set("com.example.validator.Main") } +sourceSets { + named("main") { + java { + srcDir("src/main/java") + } + } +} + dependencies { implementation(libs.validator.push) } \ No newline at end of file diff --git a/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt index f046ebe09..c1aa20ef0 100644 --- a/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt +++ b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt @@ -1,3 +1,19 @@ +/* + * 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.validator import com.google.android.wearable.watchface.validator.client.DwfValidatorFactory @@ -56,4 +72,4 @@ private fun obtainTempWatchFaceFile(): File { inputStream.copyTo(fos) } return tempFile -} \ No newline at end of file +} From f11b3bc3e4b41456aaf0da5d369e9b3f8621705a Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Thu, 21 Aug 2025 02:37:49 -0700 Subject: [PATCH 047/120] Build: Increase Gradle JVM heap size to 4g (#595) Increased the `org.gradle.jvmargs` from -Xmx2048m to -Xmx4g in `gradle.properties` to provide more heap space for the Gradle daemon. This helps prevent out-of-memory errors during large builds. Co-authored-by: Florina Muntenescu <2998890+florina-muntenescu@users.noreply.github.com> --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2dedea847..24e350250 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Mon May 22 14:59:56 BST 2023 -org.gradle.jvmargs=-Xmx2048m +org.gradle.jvmargs=-Xmx4g # Turn on parallel compilation, caching and on-demand configuration org.gradle.configureondemand=true From 982ea5ec89aeb27ec404978c89460324a6700496 Mon Sep 17 00:00:00 2001 From: compose-devrel-github-bot <118755852+compose-devrel-github-bot@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:50:18 +0100 Subject: [PATCH 048/120] =?UTF-8?q?=F0=9F=A4=96=20Update=20Dependencies=20?= =?UTF-8?q?(#596)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🤖 Update Dependencies * Update compileSdk to 36 * Added remember for FocusRequester --------- Co-authored-by: srikrishnasakunia --- .../touchinput/focus/FocusSnippets.kt | 2 +- gradle/libs.versions.toml | 70 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 2fce7f0aa..d48412c3c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -297,7 +297,7 @@ private fun RequestFocus2() { private fun Capture() { var text by remember { mutableStateOf("") } // [START android_compose_touchinput_focus_capture] - val textField = FocusRequester() + val textField = remember { FocusRequester() } TextField( value = text, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d71c7ba05..cff01387c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,41 +2,41 @@ accompanist = "0.36.0" activityKtx = "1.10.1" android-googleid = "1.1.1" -androidGradlePlugin = "8.10.1" +androidGradlePlugin = "8.12.1" androidx-activity-compose = "1.10.1" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2025.06.01" +androidx-compose-bom = "2025.08.00" androidx-compose-ui-test = "1.7.0-alpha08" -androidx-compose-ui-test-junit4-accessibility = "1.9.0-beta01" +androidx-compose-ui-test-junit4-accessibility = "1.10.0-alpha01" androidx-constraintlayout = "2.2.1" androidx-constraintlayout-compose = "1.1.1" androidx-coordinator-layout = "1.3.0" -androidx-corektx = "1.16.0" +androidx-corektx = "1.17.0" androidx-credentials = "1.5.0" androidx-credentials-play-services-auth = "1.5.0" androidx-emoji2-views = "1.5.0" -androidx-fragment-ktx = "1.8.8" +androidx-fragment-ktx = "1.8.9" androidx-glance-appwidget = "1.1.1" -androidx-lifecycle-compose = "2.9.1" -androidx-lifecycle-runtime-compose = "2.9.1" -androidx-navigation = "2.9.0" +androidx-lifecycle-compose = "2.9.2" +androidx-lifecycle-runtime-compose = "2.9.2" +androidx-navigation = "2.9.3" androidx-paging = "3.3.6" androidx-startup-runtime = "1.2.0" -androidx-test = "1.6.1" -androidx-test-espresso = "3.6.1" -androidx-test-junit = "1.2.1" -androidx-window = "1.5.0-alpha02" -androidx-window-core = "1.5.0-alpha02" -androidx-window-java = "1.5.0-alpha02" +androidx-test = "1.7.0" +androidx-test-espresso = "3.7.0" +androidx-test-junit = "1.3.0" +androidx-window = "1.5.0-beta02" +androidx-window-core = "1.5.0-beta02" +androidx-window-java = "1.5.0-beta02" androidx-xr-arcore = "1.0.0-alpha05" -androidx-xr-scenecore = "1.0.0-alpha05" -androidx-xr-compose = "1.0.0-alpha05" +androidx-xr-compose = "1.0.0-alpha06" +androidx-xr-scenecore = "1.0.0-alpha06" androidxHiltNavigationCompose = "1.2.0" appcompat = "1.7.1" coil = "2.7.0" # @keep -compileSdk = "35" -compose-latest = "1.8.3" +compileSdk = "36" +compose-latest = "1.9.0" composeUiTooling = "1.4.1" coreSplashscreen = "1.0.1" coroutines = "1.10.2" @@ -44,35 +44,35 @@ glide = "1.0.0-beta01" google-maps = "19.2.0" gradle-versions = "0.52.0" guava = "33.4.8-jre" -hilt = "2.56.2" -horologist = "0.7.14-beta" +hilt = "2.57" +horologist = "0.8.1-alpha" junit = "4.13.2" -kotlin = "2.2.0" +kotlin = "2.2.10" kotlinCoroutinesOkhttp = "1.0" kotlinxCoroutinesGuava = "1.10.2" -kotlinxSerializationJson = "1.8.1" -ksp = "2.1.21-2.0.2" -lifecycleService = "2.9.1" -maps-compose = "6.6.0" -material = "1.14.0-alpha02" +kotlinxSerializationJson = "1.9.0" +ksp = "2.2.10-2.0.2" +lifecycleService = "2.9.2" +maps-compose = "6.7.2" +material = "1.14.0-alpha03" material3-adaptive = "1.1.0" material3-adaptive-navigation-suite = "1.3.2" media3 = "1.8.0" # @keep minSdk = "35" -okHttp = "4.12.0" +okHttp = "5.1.0" playServicesWearable = "19.0.0" protolayout = "1.3.0" recyclerview = "1.4.0" targetSdk = "35" tiles = "1.5.0" tracing = "1.3.0" -validatorPush = "1.0.0-alpha03" +validatorPush = "1.0.0-alpha06" version-catalog-update = "1.0.0" wear = "1.3.0" -wearComposeFoundation = "1.5.0-beta04" -wearComposeMaterial = "1.5.0-beta04" -wearComposeMaterial3 = "1.5.0-beta04" +wearComposeFoundation = "1.5.0-rc02" +wearComposeMaterial = "1.5.0-rc02" +wearComposeMaterial3 = "1.5.0-rc02" wearOngoing = "1.0.0" wearToolingPreview = "1.0.0" webkit = "1.14.0" @@ -144,13 +144,13 @@ androidx-startup-runtime = { module = "androidx.startup:startup-runtime", versio androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } -androidx-test-runner = "androidx.test:runner:1.6.2" +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } androidx-tiles = { module = "androidx.wear.tiles:tiles", version.ref = "tiles" } androidx-tiles-renderer = { module = "androidx.wear.tiles:tiles-renderer", version.ref = "tiles" } androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version.ref = "tiles" } androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } -androidx-tracing = { group = "androidx.tracing", name = "tracing", version.ref = "tracing" } +androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "tracing" } androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } @@ -158,7 +158,7 @@ androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" } androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window-java" } -androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.2" +androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.3" androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr-arcore" } androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr-compose" } androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr-scenecore" } @@ -194,7 +194,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -kotlin-android = "org.jetbrains.kotlin.android:2.2.0" +kotlin-android = "org.jetbrains.kotlin.android:2.2.10" kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } From 97d93a1c385a21a0343e641caef01b0496be59fb Mon Sep 17 00:00:00 2001 From: garanj Date: Fri, 22 Aug 2025 14:16:39 +0100 Subject: [PATCH 049/120] Adds complications snippets (#597) * Adds complications snippets * Fix snippet tag * Fix snippet tag * Apply Spotless --------- Co-authored-by: garanj <25058277+garanj@users.noreply.github.com> --- gradle/libs.versions.toml | 2 + settings.gradle.kts | 3 + wear/build.gradle.kts | 2 + wear/src/main/AndroidManifest.xml | 40 +++++++++ .../MyComplicationDataSourceService.kt | 50 +++++++++++ ...MyTimelineComplicationDataSourceService.kt | 83 +++++++++++++++++++ .../main/res/drawable/complication_icon.xml | 5 ++ wear/src/main/res/values/strings.xml | 2 + 8 files changed, 187 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt create mode 100644 wear/src/main/java/com/example/wear/snippets/complication/MyTimelineComplicationDataSourceService.kt create mode 100644 wear/src/main/res/drawable/complication_icon.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cff01387c..77f80c6d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ tiles = "1.5.0" tracing = "1.3.0" validatorPush = "1.0.0-alpha06" version-catalog-update = "1.0.0" +watchfaceComplicationsDataSourceKtx = "1.2.1" wear = "1.3.0" wearComposeFoundation = "1.5.0-rc02" wearComposeMaterial = "1.5.0-rc02" @@ -151,6 +152,7 @@ androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } androidx-tracing = { module = "androidx.tracing:tracing", version.ref = "tracing" } +androidx-watchface-complications-data-source-ktx = { module = "androidx.wear.watchface:watchface-complications-data-source-ktx", version.ref = "watchfaceComplicationsDataSourceKtx" } androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } androidx-wear-ongoing = { module = "androidx.wear:wear-ongoing", version.ref = "wearOngoing" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } diff --git a/settings.gradle.kts b/settings.gradle.kts index eb485975e..316110b4e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,9 @@ pluginManagement { mavenCentral() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index eb7c66445..d88421c1e 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -85,6 +85,8 @@ dependencies { implementation(libs.horologist.compose.layout) implementation(libs.horologist.compose.material) implementation(libs.androidx.material.icons.core) + implementation(libs.androidx.watchface.complications.data.source.ktx) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling.preview) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index e72edbe49..8aebe45b5 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -201,6 +201,46 @@ android:value="For Ongoing Activity"/> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt b/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt new file mode 100644 index 000000000..ce08ea6ff --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt @@ -0,0 +1,50 @@ +/* + * 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.complication + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +// [START android_wear_complication] +class MyComplicationDataSourceService : SuspendingComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + // Retrieve latest info for inclusion in the data + val text = getLatestData() + return shortTextComplicationData(text) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? { + return shortTextComplicationData("Event 1") + } + + private fun shortTextComplicationData(text: String) = + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder(text).build() + ) + // Add further optional details here such as icon, tap action, title etc + .build() + + // [START_EXCLUDE] + private fun getLatestData() = "Test" + // [END_EXCLUDE] +} +// [END android_wear_complication] diff --git a/wear/src/main/java/com/example/wear/snippets/complication/MyTimelineComplicationDataSourceService.kt b/wear/src/main/java/com/example/wear/snippets/complication/MyTimelineComplicationDataSourceService.kt new file mode 100644 index 000000000..00b6daea3 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/complication/MyTimelineComplicationDataSourceService.kt @@ -0,0 +1,83 @@ +/* + * 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.complication + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationDataTimeline +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingTimelineComplicationDataSourceService +import androidx.wear.watchface.complications.datasource.TimeInterval +import androidx.wear.watchface.complications.datasource.TimelineEntry +import java.time.Instant +import java.time.temporal.ChronoUnit + +data class CalendarEntry( + val start: Instant, + val end: Instant, + val name: String +) + +// [START android_wear_timeline_complication] +class MyTimelineComplicationDataSourceService : SuspendingTimelineComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationDataTimeline? { + if (request.complicationType != ComplicationType.SHORT_TEXT) { + return ComplicationDataTimeline( + defaultComplicationData = NoDataComplicationData(), + timelineEntries = emptyList() + ) + } + // Retrieve list of events from your own datasource / database. + val events = getCalendarEvents() + return ComplicationDataTimeline( + defaultComplicationData = shortTextComplicationData("No event"), + timelineEntries = events.map { + TimelineEntry( + validity = TimeInterval(it.start, it.end), + complicationData = shortTextComplicationData(it.name) + ) + } + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? { + return shortTextComplicationData("Event 1") + } + + private fun shortTextComplicationData(text: String) = + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder(text).build() + ) + // Add further optional details here such as icon, tap action, title etc + .build() + + // [START_EXCLUDE] + private fun getCalendarEvents(): List { + val now = Instant.now() + return listOf( + CalendarEntry(now, now.plus(1, ChronoUnit.HOURS), "Event 1"), + CalendarEntry(now.plus(2, ChronoUnit.HOURS), now.plus(3, ChronoUnit.HOURS), "Event 2"), + CalendarEntry(now.plus(4, ChronoUnit.HOURS), now.plus(5, ChronoUnit.HOURS), "Event 3"), + ) + } + // [END_EXCLUDE] +} +// [END android_wear_timeline_complication] diff --git a/wear/src/main/res/drawable/complication_icon.xml b/wear/src/main/res/drawable/complication_icon.xml new file mode 100644 index 000000000..1793d6ba5 --- /dev/null +++ b/wear/src/main/res/drawable/complication_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 90f5cb258..ccf86dbde 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -6,4 +6,6 @@ Message Detail Hello Tile Hello Tile Description + My Complication + My Timeline Complication \ No newline at end of file From dd30aee903e8c247786c064faab1a9ca8d10b46e Mon Sep 17 00:00:00 2001 From: garanj Date: Fri, 22 Aug 2025 22:49:43 +0100 Subject: [PATCH 050/120] Wear complications (#598) * Adds complications snippets * Fix snippet tag * Fix snippet tag * Apply Spotless * Updates complication snippets * Add missing copyright * Apply Spotless --------- Co-authored-by: garanj <25058277+garanj@users.noreply.github.com> --- wear/src/main/AndroidManifest.xml | 25 ++++++ .../complication/ConfigurationActivity.kt | 81 +++++++++++++++++++ wear/src/main/res/values/strings.xml | 1 + 3 files changed, 107 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 8aebe45b5..aaad81bba 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -212,12 +212,22 @@ + + + + + @@ -241,6 +251,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt b/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt new file mode 100644 index 000000000..551588854 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt @@ -0,0 +1,81 @@ +/* + * 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.complication + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.Text +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_COMPLICATION_ID +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_COMPLICATION_TYPE +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_DATA_SOURCE_COMPONENT + +class ConfigurationActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // [START android_wear_complication_configuration_intent] + // Keys defined on ComplicationDataSourceService + val id = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_ID, -1) + val type = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_TYPE, -1) + val source = intent.getStringExtra(EXTRA_CONFIG_DATA_SOURCE_COMPONENT) + // [END android_wear_complication_configuration_intent] + setContent { + ComplicationConfig( + id = id, + type = type, + source = source + ) + } + } + + @Composable + fun ComplicationConfig( + modifier: Modifier = Modifier, + id: Int, + type: Int, + source: String? + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("ID: $id") + Text("Type: $type") + Text("Source: $source") + Spacer(modifier = Modifier.height(4.dp)) + Button(onClick = { + // [START android_wear_complication_configuration_finish] + setResult(RESULT_OK) // Or RESULT_CANCELLED to cancel configuration + finish() + // [END android_wear_complication_configuration_finish] + }) { + Text("Done!") + } + } + } +} diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index ccf86dbde..19e308a68 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -8,4 +8,5 @@ Hello Tile Description My Complication My Timeline Complication + Configuration activity \ No newline at end of file From 41d2e50631e62d64f2d0b6b0e5218e344de0dbe8 Mon Sep 17 00:00:00 2001 From: Kevin <7457579+khufdev@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:33:30 -0400 Subject: [PATCH 051/120] Update comments in MyComplicationDataSourceService.kt, visible in Android developer documentation (#604) --- .../snippets/complication/MyComplicationDataSourceService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt b/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt index ce08ea6ff..1b4263604 100644 --- a/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt +++ b/wear/src/main/java/com/example/wear/snippets/complication/MyComplicationDataSourceService.kt @@ -26,7 +26,7 @@ import androidx.wear.watchface.complications.datasource.SuspendingComplicationDa // [START android_wear_complication] class MyComplicationDataSourceService : SuspendingComplicationDataSourceService() { override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { - // Retrieve latest info for inclusion in the data + // Retrieve the latest info for inclusion in the data. val text = getLatestData() return shortTextComplicationData(text) } @@ -40,7 +40,7 @@ class MyComplicationDataSourceService : SuspendingComplicationDataSourceService( text = PlainComplicationText.Builder(text).build(), contentDescription = PlainComplicationText.Builder(text).build() ) - // Add further optional details here such as icon, tap action, title etc + // Add further optional details here such as icon, tap action, and title. .build() // [START_EXCLUDE] From 32e681d0621a4e5338c1e48295853f9c3ec2cd8c Mon Sep 17 00:00:00 2001 From: Kevin <7457579+khufdev@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:38:52 -0400 Subject: [PATCH 052/120] Fix typo for Activity constant (CANCELLED -> CANCELED) (#606) The constant uses U.S. English spelling: https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/core/java/android/app/Activity.java;l=777 --- .../example/wear/snippets/complication/ConfigurationActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt b/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt index 551588854..2ff8758fd 100644 --- a/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/complication/ConfigurationActivity.kt @@ -70,7 +70,7 @@ class ConfigurationActivity : ComponentActivity() { Spacer(modifier = Modifier.height(4.dp)) Button(onClick = { // [START android_wear_complication_configuration_finish] - setResult(RESULT_OK) // Or RESULT_CANCELLED to cancel configuration + setResult(RESULT_OK) // Or RESULT_CANCELED to cancel configuration finish() // [END android_wear_complication_configuration_finish] }) { From 5673ffc60b614daf028ee936227128eb8c4f9781 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 26 Aug 2025 17:03:48 +0100 Subject: [PATCH 053/120] Add lottie animation fallback example (#608) --- .../example/wear/snippets/tile/Animations.kt | 56 +++++++++++++++++- .../src/main/res/drawable/lottie_fallback.png | Bin 0 -> 16017 bytes 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 wear/src/main/res/drawable/lottie_fallback.png diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt index bb53c5ca5..dc8591aed 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt @@ -271,9 +271,7 @@ class AnimationScaling : TileService() { // Shrink the element by a scale factor // of 0.5 horizontally and 0.75 vertically. .setScaleX(FloatProp.Builder(0.5f).build()) - .setScaleY( - FloatProp.Builder(0.75f).build() - ) + .setScaleY(FloatProp.Builder(0.75f).build()) .build() ) .build() @@ -361,4 +359,56 @@ class LottieAnimation : TileService() { ) } } + // [END android_wear_tile_animations_lottie] + +class LottieAnimationFallback : TileService() { + + val lottieResourceId = "lottie_animation" + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + + val layout = + LayoutElementBuilders.Image.Builder() + .setWidth(dp(150f)) + .setHeight(dp(150f)) + .setResourceId(lottieResourceId) + .build() + + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(Timeline.fromLayoutElement(layout)) + .build() + ) + } + + override fun onTileResourcesRequest( + requestParams: ResourcesRequest + ): ListenableFuture { + + // [START android_wear_tile_animations_lottie_fallback] + val lottieImage = + ResourceBuilders.ImageResource.Builder() + .setAndroidLottieResourceByResId( + ResourceBuilders.AndroidLottieResourceByResId.Builder(R.raw.lottie) + .setStartTrigger(createOnVisibleTrigger()) + .build() + ) + // Fallback if lottie is not supported + .setAndroidResourceByResId( + ResourceBuilders.AndroidImageResourceByResId.Builder() + .setResourceId(R.drawable.lottie_fallback) + .build() + ) + .build() + // [END android_wear_tile_animations_lottie_fallback] + + return Futures.immediateFuture( + Resources.Builder() + .setVersion(requestParams.version) + .addIdToImageMapping(lottieResourceId, lottieImage) + .build() + ) + } +} diff --git a/wear/src/main/res/drawable/lottie_fallback.png b/wear/src/main/res/drawable/lottie_fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8b179208d6cafe1c5e1f3b09015a9366b5ab45 GIT binary patch literal 16017 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 Date: Wed, 27 Aug 2025 11:25:32 -0400 Subject: [PATCH 054/120] Update comments in complication part of AndroidManifest.xml (#605) Co-authored-by: garanj --- wear/src/main/AndroidManifest.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index aaad81bba..02b60438b 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -212,7 +212,7 @@ - + @@ -220,10 +220,7 @@ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS" android:value="300" /> - + @@ -268,4 +265,4 @@ - \ No newline at end of file + From fcea14b5f5e8c6c889b29fe02d8e2dc49460dea6 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 28 Aug 2025 16:56:42 +0100 Subject: [PATCH 055/120] Add padding to avoid overwriting TimeText (#529) --- .../com/example/wear/snippets/m3/navigation/Navigation.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt index c595c54ef..83f0f58d0 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt @@ -72,10 +72,13 @@ fun MessageDetail(id: String) { ScreenScaffold( scrollState = scrollState, contentPadding = padding - ) { + ) { scaffoldPaddingValues -> // Screen content goes here // [END android_wear_navigation] - TransformingLazyColumn(state = scrollState) { + TransformingLazyColumn( + state = scrollState, + contentPadding = scaffoldPaddingValues + ) { item { Text( text = id, From 3c8b2837de010d1e79afbfd92cf4a7175f20e2f9 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 13:32:40 -0700 Subject: [PATCH 056/120] Refactor(SearchBar): Reorder modifier parameter to follow convention (#594) Moved the modifier parameter in the CustomizableSearchBar composable to be the first optional parameter, adhering to Jetpack Compose API guidelines. --- .../java/com/example/compose/snippets/components/SearchBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt index f3243e299..d97f78524 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt @@ -178,13 +178,13 @@ fun CustomizableSearchBar( onSearch: (String) -> Unit, searchResults: List, onResultClick: (String) -> Unit, + modifier: Modifier = Modifier, // Customization options placeholder: @Composable () -> Unit = { Text("Search") }, leadingIcon: @Composable (() -> Unit)? = { Icon(Icons.Default.Search, contentDescription = "Search") }, trailingIcon: @Composable (() -> Unit)? = null, supportingContent: (@Composable (String) -> Unit)? = null, leadingContent: (@Composable () -> Unit)? = null, - modifier: Modifier = Modifier ) { // Track expanded state of search bar var expanded by rememberSaveable { mutableStateOf(false) } From a73d3b450bb1de17fb3e4845a204bfc079926feb Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 13:51:04 -0700 Subject: [PATCH 057/120] Refactor(text): Use KTX extension for isDigitsOnly check (#593) The lint tool reported a "UseKtx" warning in StateBasedText.kt. The code was using the static `TextUtils.isDigitsOnly()` method. This commit refactors the code to use the more idiomatic `isDigitsOnly()` Kotlin extension function from the Android KTX library. --- .../java/com/example/compose/snippets/text/StateBasedText.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt index b43aef25f..fb3e75807 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt @@ -15,8 +15,6 @@ */ package com.example.compose.snippets.text - -import android.text.TextUtils import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -249,7 +247,7 @@ class CustomInputTransformation : InputTransformation { // [START android_compose_state_text_16] class DigitOnlyInputTransformation : InputTransformation { override fun TextFieldBuffer.transformInput() { - if (!TextUtils.isDigitsOnly(asCharSequence())) { + if (!asCharSequence().isDigitsOnly()) { revertAllChanges() } } From d6d74ed652e50d67f59657e15fba93ffc24d2b92 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 14:19:58 -0700 Subject: [PATCH 058/120] Refactor: Rename Composable functions to follow naming conventions (#592) The lint tool reported "ComposableNaming" warnings in GlanceSnippets.kt. Composable functions that return Unit should be named using PascalCase, like classes, to clearly identify them as UI components. This commit renames 12 Composable functions to adhere to this convention. For example, `actionLambda` has been renamed to `ActionLambda`. This improves code readability and follows Compose best practices. --- .../compose/snippets/glance/GlanceSnippets.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt index bb575a8b5..431bc6776 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt @@ -225,7 +225,7 @@ private object ActionLaunchSendBroadcastEvent { private object ActionLambda { @Composable - fun actionLambda() { + fun ActionLambda() { // [START android_compose_glance_lambda01] Text( text = "Submit", @@ -237,7 +237,7 @@ private object ActionLambda { } @Composable - fun actionLambda2() { + fun ActionLambda2() { // [START android_compose_glance_lambda02] Button( text = "Submit", @@ -436,7 +436,7 @@ object ManageAndUpdate { object BuildUIWithGlance { @Composable - fun example1() { + fun Example1() { // [START android_compose_glance_buildUI01] Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) { val modifier = GlanceModifier.defaultWeight() @@ -448,7 +448,7 @@ object BuildUIWithGlance { } @Composable - fun example2() { + fun Example2() { // [START android_compose_glance_buildUI02] // Remember to import Glance Composables @@ -466,7 +466,7 @@ object BuildUIWithGlance { } @Composable - fun example3() { + fun Example3() { // [START android_compose_glance_buildUI03] LazyColumn { item { @@ -480,7 +480,7 @@ object BuildUIWithGlance { } @Composable - fun example4() { + fun Example4() { val peopleNameList = arrayListOf() val peopleList = arrayListOf() @@ -631,7 +631,7 @@ object SizeModeSnippets3 { object AccessResources { @Composable - fun example1() { + fun Example1() { // [START android_compose_glance_buildUI10] LocalContext.current.getString(R.string.glance_title) // [END android_compose_glance_buildUI10] @@ -651,7 +651,7 @@ object AccessResources { object CompoundButton { @Composable - fun example1() { + fun Example1() { // [START android_compose_glance_buildUI12] var isApplesChecked by remember { mutableStateOf(false) } var isEnabledSwitched by remember { mutableStateOf(false) } @@ -860,7 +860,7 @@ object GlanceTheming { } @Composable - fun shapeExample() { + fun ShapeExample() { // Note : android_compose_glance_glancetheming04 is found in button_outline.xml // [START android_compose_glance_glancetheming05] GlanceModifier.background( @@ -898,7 +898,7 @@ object GlanceInnerPadding { object GlanceInteroperability { @Composable - fun example01() { + fun Example01() { // [START android_compose_glance_glanceinteroperability01] val packageName = LocalContext.current.packageName Column(modifier = GlanceModifier.fillMaxSize()) { @@ -909,7 +909,7 @@ object GlanceInteroperability { } @Composable - fun example02() { + fun Example02() { val packageName = null // [START android_compose_glance_glanceinteroperability02] From 959474f1feee8bc13cd36818b46972a5a8dee570 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 14:55:07 -0700 Subject: [PATCH 059/120] Lint: Use specialized state holders to avoid autoboxing (#591) * Perf: Use specialized state holders to avoid autoboxing The lint tool reported "AutoboxingStateCreation" warnings in multiple files. Using the generic `mutableStateOf` for primitive types (Int, Float, Long) can lead to performance overhead due to autoboxing. This commit resolves these warnings by replacing `mutableStateOf` with its specialized, more performant counterparts (`mutableIntStateOf`, `mutableFloatStateOf`, `mutableLongStateOf`) where applicable. This change improves performance by avoiding unnecessary object allocations for primitive state values. --- .../compose/snippets/components/Badges.kt | 4 ++-- .../snippets/components/ProgressIndicator.kt | 5 +++-- .../compose/snippets/glance/GlanceSnippets.kt | 3 ++- .../interop/InteroperabilityAPIsSnippets.kt | 4 ++-- .../snippets/layouts/FlowLayoutSnippets.kt | 4 ++-- .../performance/PerformanceSnippets.kt | 3 ++- .../compose/snippets/phases/PhasesSnippets.kt | 3 ++- .../sideeffects/SideEffectsSnippets.kt | 3 ++- .../touchinput/gestures/GesturesSnippets.kt | 18 ++++++++++-------- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt index f2f83b01c..895c52def 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt @@ -30,7 +30,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -78,7 +78,7 @@ fun BadgeExample() { // [START android_compose_components_badgeinteractive] @Composable fun BadgeInteractiveExample() { - var itemCount by remember { mutableStateOf(0) } + var itemCount by remember { mutableIntStateOf(0) } Column( verticalArrangement = Arrangement.spacedBy(16.dp) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt index 4d5055457..be18829e2 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -65,7 +66,7 @@ fun ProgressIndicatorExamples() { // [START android_compose_components_determinateindicator] @Composable fun LinearDeterminateIndicator() { - var currentProgress by remember { mutableStateOf(0f) } + var currentProgress by remember { mutableFloatStateOf(0f) } var loading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() // Create a coroutine scope @@ -107,7 +108,7 @@ suspend fun loadProgress(updateProgress: (Float) -> Unit) { @Preview @Composable fun CircularDeterminateIndicator() { - var currentProgress by remember { mutableStateOf(0f) } + var currentProgress by remember { mutableFloatStateOf(0f) } var loading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() // Create a coroutine scope diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt index 431bc6776..df5efbd79 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -655,7 +656,7 @@ object CompoundButton { // [START android_compose_glance_buildUI12] var isApplesChecked by remember { mutableStateOf(false) } var isEnabledSwitched by remember { mutableStateOf(false) } - var isRadioChecked by remember { mutableStateOf(0) } + var isRadioChecked by remember { mutableIntStateOf(0) } CheckBox( checked = isApplesChecked, diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt index bfafc95df..1c030ae32 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt @@ -38,7 +38,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue @@ -194,7 +194,7 @@ class ExampleFragmentMultipleComposeView : Fragment() { // [START android_compose_interop_apis_views_in_compose] @Composable fun CustomView() { - var selectedItem by remember { mutableStateOf(0) } + var selectedItem by remember { mutableIntStateOf(0) } // Adds view to Compose AndroidView( diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt index 164dd3ea4..60e795874 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt @@ -45,7 +45,7 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -472,7 +472,7 @@ fun ContextualFlowLayoutExample() { // [START android_compose_layouts_contextual_flow] val totalCount = 40 var maxLines by remember { - mutableStateOf(2) + mutableIntStateOf(2) } val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope -> diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt index dcb7786e5..fc9af5d63 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -264,7 +265,7 @@ private object BackwardsWrite { // [START android_compose_performance_backwardswrite] @Composable fun BadComposable() { - var count by remember { mutableStateOf(0) } + var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt index c785052c4..1ca55b9e0 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -178,7 +179,7 @@ private object Loop { fun Loop() { // [START android_compose_phases_loop] Box { - var imageHeightPx by remember { mutableStateOf(0) } + var imageHeightPx by remember { mutableIntStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt index dca1a2b02..cba065a33 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -59,7 +60,7 @@ fun MyScreen() { // [START android_compose_side_effects_launchedeffect] // Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time - var pulseRateMs by remember { mutableStateOf(3000L) } + var pulseRateMs by remember { mutableLongStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt index d5f7fffed..9fcedffe2 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt @@ -52,6 +52,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -77,7 +79,7 @@ import kotlin.math.roundToInt // [START android_compose_touchinput_gestures_clickable] @Composable private fun ClickableSample() { - val count = remember { mutableStateOf(0) } + val count = remember { mutableIntStateOf(0) } // content that you want to make clickable Text( text = count.value.toString(), @@ -89,7 +91,7 @@ private fun ClickableSample() { @Preview @Composable private fun WithPointerInput() { - val count = remember { mutableStateOf(0) } + val count = remember { mutableIntStateOf(0) } // content that you want to make clickable Text( text = count.value.toString(), @@ -151,7 +153,7 @@ private fun ScrollBoxesSmooth() { @Composable private fun ScrollableSample() { // actual composable state - var offset by remember { mutableStateOf(0f) } + var offset by remember { mutableFloatStateOf(0f) } Box( Modifier .size(150.dp) @@ -249,7 +251,7 @@ private object NestedScrollInterop { // [START android_compose_touchinput_gestures_draggable] @Composable private fun DraggableText() { - var offsetX by remember { mutableStateOf(0f) } + var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } @@ -268,8 +270,8 @@ private fun DraggableText() { @Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } + var offsetX by remember { mutableFloatStateOf(0f) } + var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier @@ -324,8 +326,8 @@ private fun SwipeableSample() { @Composable private fun TransformableSample() { // set up all transformation states - var scale by remember { mutableStateOf(1f) } - var rotation by remember { mutableStateOf(0f) } + var scale by remember { mutableFloatStateOf(1f) } + var rotation by remember { mutableFloatStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange From e2de07a13834eb5472313af05e26c68b607435a4 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 15:26:58 -0700 Subject: [PATCH 060/120] Fix(gestures): Handle pointer events inside awaitPointerEventScope (#590) The lint tool reported a "ReturnFromAwaitPointerEventScope" warning in KotlinSnippets.kt. Returning a value from this scope can lead to dropped input events due to the way pointer event queues are handled. This commit resolves the warning by moving the animation logic, which depends on the tap position, directly inside the awaitPointerEventScope block. This ensures that the event data is processed immediately within the correct context, preventing potential event loss. --- .../compose/snippets/kotlin/KotlinSnippets.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt index 12c5618b4..447b37031 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt @@ -340,15 +340,17 @@ fun MoveBoxWhereTapped() { // coroutines inside a suspend function coroutineScope { while (true) { - // Wait for the user to tap on the screen - val offset = awaitPointerEventScope { - awaitFirstDown().position - } - // Launch a new coroutine to asynchronously animate to - // where the user tapped on the screen - launch { - // Animate to the pressed position - animatedOffset.animateTo(offset) + // Wait for the user to tap on the screen and animate + // in the same block + awaitPointerEventScope { + val offset = awaitFirstDown().position + + // Launch a new coroutine to asynchronously animate to + // where the user tapped on the screen + launch { + // Animate to the pressed position + animatedOffset.animateTo(offset) + } } } } From f95ab59fad80aeaf5d6a90bab8a01a126f20f44e Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 15:57:39 -0700 Subject: [PATCH 061/120] Fix: Prevent memory leak from implicit SAM conversion (#588) The lint tool reported an "ImplicitSamInstance" warning in PictureInPictureSnippets.kt. Lambdas used as listeners were being implicitly converted to new SAM instances, causing removeOnUserLeaveHintListener to fail because the listener instances did not match. This could lead to a memory leak. This commit fixes the issue by explicitly wrapping the listener lambdas in a 'Runnable' instance. This ensures a stable object reference is used for both adding and removing the listener. --- .../snippets/pictureinpicture/PictureInPictureSnippets.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt index 02d65d57c..295894832 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt @@ -356,7 +356,7 @@ fun PipListenerPreAPI12(shouldEnterPipMode: Boolean) { ) { val context = LocalContext.current DisposableEffect(context) { - val onUserLeaveBehavior: () -> Unit = { + val onUserLeaveBehavior = Runnable { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } @@ -384,7 +384,7 @@ fun EnterPiPPre12(shouldEnterPipMode: Boolean) { ) { val context = LocalContext.current DisposableEffect(context) { - val onUserLeaveBehavior: () -> Unit = { + val onUserLeaveBehavior = Runnable { if (currentShouldEnterPipMode) { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) From 6301d76e6d8858451ce7ccc28f0bbb2ad06def37 Mon Sep 17 00:00:00 2001 From: Christopher Cartland Date: Fri, 29 Aug 2025 22:22:15 -0700 Subject: [PATCH 062/120] docs(modifiers): Add TODOs for inspectableProperties lint warnings (#589) The lint tool reported warnings for `ModifierNodeElement` classes missing an override for `inspectableProperties`. This function is used by development tools like the Layout Inspector to display debug information. Rather than implementing the function directly and adding complexity to the existing minimal snippets, this commit adds `TODO` comments. This serves as a reminder to create a dedicated snippet that demonstrates how to properly implement `inspectableProperties`, thereby keeping the current examples focused and clean while still addressing the lint warning. --- .../compose/snippets/modifiers/CustomModifierSnippets.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt index 650c4beb8..665fea4b7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt @@ -142,6 +142,7 @@ private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { } // [END android_compose_custom_modifiers_7] +// TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. // [START android_compose_custom_modifiers_8] // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement() { @@ -159,6 +160,7 @@ fun Modifier.circle(color: Color) = this then CircleElement(color) // [END android_compose_custom_modifiers_9] private object CustomModifierSnippets10 { + // TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. // [START android_compose_custom_modifiers_10] // Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) @@ -181,6 +183,7 @@ private object CustomModifierSnippets10 { // [END android_compose_custom_modifiers_10] } +// TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. // [START android_compose_custom_modifiers_11] fun Modifier.fixedPadding() = this then FixedPaddingElement From fb858176e2f6f3b73607768f8c3e516d72fa3bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 20:02:13 +0200 Subject: [PATCH 063/120] Add KMP ViewModel snippets (#611) * Add kotlin gitignore * Add KMP shared module * Add xcode gitignore * Add kmp ios app * Add KMP ViewModel * Fix ios build * Add viewmodel snippet * Move to directory * Move kmp to :shared * Add KMP regions * Add swift content * Add comments with file path * Add build-ios workflow * Fix concurrency * Apply Spotless --------- Co-authored-by: mlykotom <3973717+mlykotom@users.noreply.github.com> --- .github/workflows/build-ios.yml | 51 +++ .gitignore | 14 + build.gradle.kts | 3 + gradle/libs.versions.toml | 8 +- kmp/iosApp/Configuration/Config.xcconfig | 7 + kmp/iosApp/iosApp.xcodeproj/project.pbxproj | 379 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/xcschememanagement.plist | 5 + .../xcschemes/iosApp.xcscheme | 66 +++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../iosApp/Assets.xcassets/Contents.json | 6 + kmp/iosApp/iosApp/ContentView.swift | 33 ++ kmp/iosApp/iosApp/Info.plist | 8 + .../iosApp/IosViewModelStoreOwner.swift | 35 ++ .../Preview Assets.xcassets/Contents.json | 6 + kmp/iosApp/iosApp/iOSApp.swift | 10 + kmp/shared/.gitignore | 1 + kmp/shared/build.gradle.kts | 92 +++++ .../src/androidMain/AndroidManifest.xml | 4 + .../com/example/kmp/snippets/MainViewModel.kt | 40 ++ .../kmp/snippets/ViewModelResolver.ios.kt | 51 +++ settings.gradle.kts | 21 +- 23 files changed, 882 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/build-ios.yml create mode 100644 kmp/iosApp/Configuration/Config.xcconfig create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 kmp/iosApp/iosApp/ContentView.swift create mode 100644 kmp/iosApp/iosApp/Info.plist create mode 100644 kmp/iosApp/iosApp/IosViewModelStoreOwner.swift create mode 100644 kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 kmp/iosApp/iosApp/iOSApp.swift create mode 100644 kmp/shared/.gitignore create mode 100644 kmp/shared/build.gradle.kts create mode 100644 kmp/shared/src/androidMain/AndroidManifest.xml create mode 100644 kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt create mode 100644 kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml new file mode 100644 index 000000000..6e6d9a06a --- /dev/null +++ b/.github/workflows/build-ios.yml @@ -0,0 +1,51 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# http://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. +name: Build snippets + +on: + push: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + pull_request: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-build-ios + cancel-in-progress: true +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build_ios: + name: Build iOS app + runs-on: macos-latest + steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build iOS app + uses: mxcl/xcodebuild@v3 + with: + xcode: ^16 + scheme: iosApp + platform: iOS + action: build + working-directory: kmp/iosApp diff --git a/.gitignore b/.gitignore index 9b10be5e8..b984a9c26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,17 @@ build .externalNativeBuild .idea/* /.idea/* +.kotlin + +### Xcode ### +## User settings +xcuserdata/ + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings diff --git a/build.gradle.kts b/build.gradle.kts index 6b4ec9ad0..899385a15 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,9 @@ plugins { alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.android.lint) apply false } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77f80c6d1..8bab1c791 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -130,6 +130,7 @@ androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleService" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } @@ -179,7 +180,7 @@ horologist-compose-layout = { module = "com.google.android.horologist:horologist horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-coroutines-okhttp = { module = "ru.gildor.coroutines:kotlin-coroutines-okhttp", version.ref = "kotlinCoroutinesOkhttp" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -189,6 +190,8 @@ play-services-wearable = { module = "com.google.android.gms:play-services-wearab validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" } wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } +jetbrains-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -197,7 +200,10 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = "org.jetbrains.kotlin.android:2.2.10" +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } +android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } diff --git a/kmp/iosApp/Configuration/Config.xcconfig b/kmp/iosApp/Configuration/Config.xcconfig new file mode 100644 index 000000000..1ebca56e2 --- /dev/null +++ b/kmp/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +TEAM_ID= + +PRODUCT_NAME=KotlinProject +PRODUCT_BUNDLE_IDENTIFIER=org.example.project.KotlinProject$(TEAM_ID) + +CURRENT_PROJECT_VERSION=1 +MARKETING_VERSION=1.0 \ No newline at end of file diff --git a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 000000000..c868dec41 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,379 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + B99700CA2DC9B8D800C7335B /* Exceptions for "iosApp" folder in "iosApp" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = B9DA97B02DC1472C00A4DA20 /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B9DA97B32DC1472C00A4DA20 /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + B99700CA2DC9B8D800C7335B /* Exceptions for "iosApp" folder in "iosApp" target */, + ); + path = iosApp; + sourceTree = ""; + }; + B9DA98002DC14AA900A4DA20 /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Configuration; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + B9DA97AE2DC1472C00A4DA20 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B9DA97A82DC1472C00A4DA20 = { + isa = PBXGroup; + children = ( + B9DA98002DC14AA900A4DA20 /* Configuration */, + B9DA97B32DC1472C00A4DA20 /* iosApp */, + B9DA97B22DC1472C00A4DA20 /* Products */, + ); + sourceTree = ""; + }; + B9DA97B22DC1472C00A4DA20 /* Products */ = { + isa = PBXGroup; + children = ( + B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B9DA97B02DC1472C00A4DA20 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = B9DA97BF2DC1472D00A4DA20 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + B9DA97F42DC1497100A4DA20 /* Compile Kotlin Framework */, + B9DA97AD2DC1472C00A4DA20 /* Sources */, + B9DA97AE2DC1472C00A4DA20 /* Frameworks */, + B9DA97AF2DC1472C00A4DA20 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + B9DA97B32DC1472C00A4DA20 /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B9DA97A92DC1472C00A4DA20 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + B9DA97B02DC1472C00A4DA20 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = B9DA97AC2DC1472C00A4DA20 /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B9DA97A82DC1472C00A4DA20; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = B9DA97B22DC1472C00A4DA20 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B9DA97B02DC1472C00A4DA20 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B9DA97AF2DC1472C00A4DA20 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + B9DA97F42DC1497100A4DA20 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/../..\"\n./gradlew :kmp:shared:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B9DA97AD2DC1472C00A4DA20 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B9DA97BD2DC1472D00A4DA20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B9DA97BE2DC1472D00A4DA20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B9DA97C02DC1472D00A4DA20 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = B9DA98002DC14AA900A4DA20 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B9DA97C12DC1472D00A4DA20 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = B9DA98002DC14AA900A4DA20 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B9DA97AC2DC1472C00A4DA20 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9DA97BD2DC1472D00A4DA20 /* Debug */, + B9DA97BE2DC1472D00A4DA20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B9DA97BF2DC1472D00A4DA20 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9DA97C02DC1472D00A4DA20 /* Debug */, + B9DA97C12DC1472D00A4DA20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B9DA97A92DC1472C00A4DA20 /* Project object */; +} diff --git a/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..ee3458dd7 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme b/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 000000000..0fe25fa51 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/Assets.xcassets/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/ContentView.swift b/kmp/iosApp/iosApp/ContentView.swift new file mode 100644 index 000000000..9e10c066d --- /dev/null +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -0,0 +1,33 @@ +import Foundation +import KmpKit +import SwiftUI + +// [START android_kmp_viewmodel_ios_contentview] +// iosApp/ContentView.swift + +struct ContentView: View { + + /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen. + @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner() + + var body: some View { + /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`. + /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection + /// and proper initialization of the ViewModel with its required `AppContainer`. + let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel( + factory: MainViewModelKt.mainViewModelFactory + ) + // [START_EXCLUDE] + VStack(spacing: 16) { + Image(systemName: "swift") + .font(.system(size: 200)) + .foregroundColor(.accentColor) + Text("SwiftUI") + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .padding() + // [END_EXCLUDE] + // .. the rest of the SwiftUI code + } +} +// [END android_kmp_viewmodel_ios_contentview] diff --git a/kmp/iosApp/iosApp/Info.plist b/kmp/iosApp/iosApp/Info.plist new file mode 100644 index 000000000..11845e1da --- /dev/null +++ b/kmp/iosApp/iosApp/Info.plist @@ -0,0 +1,8 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift new file mode 100644 index 000000000..b2e6c52df --- /dev/null +++ b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift @@ -0,0 +1,35 @@ +import Foundation +import KmpKit + +// [START android_kmp_viewmodel_ios_viewmodel_storeowner] +// iosApp/IosViewModelStoreOwner.swift + +class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner { + + let viewModelStore = ViewModelStore() + + /// This function allows retrieving the androidx ViewModel from the store. + /// It uses the utilify function to pass the generic type T to shared code + func viewModel( + key: String? = nil, + factory: ViewModelProviderFactory, + extras: CreationExtras? = nil + ) -> T { + do { + return try viewModelStore.resolveViewModel( + modelClass: T.self, + factory: factory, + key: key, + extras: extras + ) as! T + } catch { + fatalError("Failed to create ViewModel of type \(T.self)") + } + } + + /// This is called when this class is used as a `@StateObject` + deinit { + viewModelStore.clear() + } +} +// [END android_kmp_viewmodel_ios_viewmodel_storeowner] diff --git a/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/iOSApp.swift b/kmp/iosApp/iosApp/iOSApp.swift new file mode 100644 index 000000000..d83dca611 --- /dev/null +++ b/kmp/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/kmp/shared/.gitignore b/kmp/shared/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/kmp/shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/kmp/shared/build.gradle.kts b/kmp/shared/build.gradle.kts new file mode 100644 index 000000000..ed832ccec --- /dev/null +++ b/kmp/shared/build.gradle.kts @@ -0,0 +1,92 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.android.lint) +} + +kotlin { + + // Target declarations - add or remove as needed below. These define + // which platforms this KMP module supports. + // See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets + androidLibrary { + namespace = "com.example.kmp.snippets" + compileSdk = 36 + minSdk = 24 + + withHostTestBuilder { + } + + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + // For iOS targets, this is also where you should + // configure native binary output. For more information, see: + // https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks + + // A step-by-step guide on how to include this library in an XCode + // project can be found here: + // https://developer.android.com/kotlin/multiplatform/migrate + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + export(libs.androidx.lifecycle.viewmodel) + baseName = "KmpKit" + } + } + + // Source set declarations. + // Declaring a target automatically creates a source set with the same name. By default, the + // Kotlin Gradle Plugin creates additional source sets that depend on each other, since it is + // common to share sources between related targets. + // See: https://kotlinlang.org/docs/multiplatform-hierarchy.html + sourceSets { + commonMain { + dependencies { + implementation(libs.jetbrains.kotlin.stdlib) + api(libs.androidx.lifecycle.viewmodel) + } + } + + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + + androidMain { + dependencies { + // Add Android-specific dependencies here. Note that this source set depends on + // commonMain by default and will correctly pull the Android artifacts of any KMP + // dependencies declared in commonMain. + } + } + + getByName("androidDeviceTest") { + dependencies { + implementation(libs.androidx.test.runner) + implementation(libs.androidx.test.core) + implementation(libs.androidx.test.ext.junit) + } + } + + iosMain { + dependencies { + // Add iOS-specific dependencies here. This a source set created by Kotlin Gradle + // Plugin (KGP) that each specific iOS target (e.g., iosX64) depends on as + // part of KMP’s default source set hierarchy. Note that this source set depends + // on common by default and will correctly pull the iOS artifacts of any + // KMP dependencies declared in commonMain. + } + } + } + +} \ No newline at end of file diff --git a/kmp/shared/src/androidMain/AndroidManifest.xml b/kmp/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/kmp/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt new file mode 100644 index 000000000..c4a06f5fd --- /dev/null +++ b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -0,0 +1,40 @@ +/* + * 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.kmp.snippets + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + +// [START android_kmp_viewmodel_class] +// commonMain/MainViewModel.kt + +class MainViewModel( + private val repository: DataRepository, +) : ViewModel() { /* some logic */ } + +// ViewModelFactory that retrieves the data repository for your app. +val mainViewModelFactory = viewModelFactory { + initializer { + MainViewModel(repository = getDataRepository()) + } +} + +fun getDataRepository(): DataRepository = DataRepository() +// [END android_kmp_viewmodel_class] + +class DataRepository diff --git a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt new file mode 100644 index 000000000..1cf6fa2a3 --- /dev/null +++ b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt @@ -0,0 +1,51 @@ +/* + * 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.kmp.snippets + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewmodel.CreationExtras +import kotlin.reflect.KClass +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ObjCClass +import kotlinx.cinterop.getOriginalKotlinClass + +// [START android_kmp_viewmodel_resolve_viewmodel] +// iosMain/ViewModelResolver.ios.kt + +/** + * This function allows retrieving any ViewModel from Swift Code with generics. We only get + * [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code + * doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin. + */ +@BetaInteropApi +@Throws(IllegalArgumentException::class) +fun ViewModelStore.resolveViewModel( + modelClass: ObjCClass, + factory: ViewModelProvider.Factory, + key: String?, + extras: CreationExtras? = null, +): ViewModel { + @Suppress("UNCHECKED_CAST") + val vmClass = getOriginalKotlinClass(modelClass) as? KClass + require(vmClass != null) { "The modelClass parameter must be a ViewModel type." } + + val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty) + return key?.let { provider[key, vmClass] } ?: provider[vmClass] +} +// [END android_kmp_viewmodel_resolve_viewmodel] diff --git a/settings.gradle.kts b/settings.gradle.kts index 316110b4e..111fd452e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -val snapshotVersion : String? = System.getenv("COMPOSE_SNAPSHOT_ID") +val snapshotVersion: String? = System.getenv("COMPOSE_SNAPSHOT_ID") pluginManagement { repositories { @@ -30,13 +30,14 @@ dependencyResolutionManagement { rootProject.name = "snippets" include( ":bluetoothle", - ":compose:recomposehighlighter", - ":kotlin", - ":compose:snippets", - ":wear", - ":views", - ":misc", - ":identity:credentialmanager", - ":xr", - ":watchfacepush:validator" + ":compose:recomposehighlighter", + ":kotlin", + ":compose:snippets", + ":wear", + ":views", + ":misc", + ":identity:credentialmanager", + ":xr", + ":watchfacepush:validator", + ":kmp:shared", ) From e4396f6dd13aaa8099c4baa671cdd549a10f201c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 21:16:48 +0200 Subject: [PATCH 064/120] Fix missing snippet (#612) * Fix missing snippet * Apply Spotless --------- Co-authored-by: mlykotom <3973717+mlykotom@users.noreply.github.com> --- .../interop/InteroperabilityAPIsSnippets.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt index 1c030ae32..efad40fd7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt @@ -32,6 +32,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -191,6 +192,28 @@ class ExampleFragmentMultipleComposeView : Fragment() { } // [END android_compose_interop_apis_compose_in_fragment_multiple] +// [START android_compose_interop_apis_android_view_reuse] +@Composable +fun AndroidViewInLazyList() { + LazyColumn { + items(100) { index -> + AndroidView( + modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree + factory = { context -> + MyView(context) + }, + update = { view -> + view.selectedItem = index + }, + onReset = { view -> + view.clear() + } + ) + } + } +} +// [END android_compose_interop_apis_android_view_reuse] + // [START android_compose_interop_apis_views_in_compose] @Composable fun CustomView() { @@ -231,6 +254,8 @@ fun ContentExample() { // [START_EXCLUDE silent] class MyView(context: Context) : View(context) { var selectedItem: Int = 0 + + fun clear() { } } // [END_EXCLUDE silent] // [END android_compose_interop_apis_views_in_compose] From 7a692165377d2c0343083820df3d68fad3aa4913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Wed, 3 Sep 2025 10:55:50 +0200 Subject: [PATCH 065/120] Add viewmodel screen for KMP (#613) * Add viewmodel screen for KMP * Apply Spotless * Fix snippets by adding compose compiler * Add androidApp to prevent Compose issue --------- Co-authored-by: mlykotom <3973717+mlykotom@users.noreply.github.com> --- kmp/androidApp/.gitignore | 1 + kmp/androidApp/build.gradle.kts | 58 +++++++++++++++++++ kmp/androidApp/proguard-rules.pro | 21 +++++++ kmp/androidApp/src/main/AndroidManifest.xml | 2 + .../com/example/kmp/snippets/MainScreen.kt | 33 +++++++++++ kmp/shared/build.gradle.kts | 14 ++--- settings.gradle.kts | 3 +- 7 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 kmp/androidApp/.gitignore create mode 100644 kmp/androidApp/build.gradle.kts create mode 100644 kmp/androidApp/proguard-rules.pro create mode 100644 kmp/androidApp/src/main/AndroidManifest.xml create mode 100644 kmp/androidApp/src/main/kotlin/com/example/kmp/snippets/MainScreen.kt diff --git a/kmp/androidApp/.gitignore b/kmp/androidApp/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/kmp/androidApp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/kmp/androidApp/build.gradle.kts b/kmp/androidApp/build.gradle.kts new file mode 100644 index 000000000..eedb0b66a --- /dev/null +++ b/kmp/androidApp/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "com.example.kmp.snippets" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "com.example.kmp.snippets" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) + } + + buildFeatures { + compose = true + } +} + +dependencies { + val composeBom = project.dependencies.platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation(project(":kmp:shared")) + + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.lifecycle.viewModelCompose) +} \ No newline at end of file diff --git a/kmp/androidApp/proguard-rules.pro b/kmp/androidApp/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/kmp/androidApp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/kmp/androidApp/src/main/AndroidManifest.xml b/kmp/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..de749acb7 --- /dev/null +++ b/kmp/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/kmp/androidApp/src/main/kotlin/com/example/kmp/snippets/MainScreen.kt b/kmp/androidApp/src/main/kotlin/com/example/kmp/snippets/MainScreen.kt new file mode 100644 index 000000000..e27576794 --- /dev/null +++ b/kmp/androidApp/src/main/kotlin/com/example/kmp/snippets/MainScreen.kt @@ -0,0 +1,33 @@ +/* + * 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.kmp.snippets + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel + +// [START android_kmp_viewmodel_screen] +// androidApp/ui/MainScreen.kt + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel( + factory = mainViewModelFactory, + ), +) { +// observe the viewModel state +} +// [END android_kmp_viewmodel_screen] diff --git a/kmp/shared/build.gradle.kts b/kmp/shared/build.gradle.kts index ed832ccec..2ef25a933 100644 --- a/kmp/shared/build.gradle.kts +++ b/kmp/shared/build.gradle.kts @@ -5,14 +5,13 @@ plugins { } kotlin { - // Target declarations - add or remove as needed below. These define // which platforms this KMP module supports. // See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets androidLibrary { - namespace = "com.example.kmp.snippets" - compileSdk = 36 - minSdk = 24 + namespace = "com.example.kmp.snippets.shared" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() withHostTestBuilder { } @@ -24,6 +23,8 @@ kotlin { } } + jvmToolchain(17) + // For iOS targets, this is also where you should // configure native binary output. For more information, see: // https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks @@ -64,9 +65,7 @@ kotlin { androidMain { dependencies { - // Add Android-specific dependencies here. Note that this source set depends on - // commonMain by default and will correctly pull the Android artifacts of any KMP - // dependencies declared in commonMain. + } } @@ -88,5 +87,4 @@ kotlin { } } } - } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 111fd452e..2a71512aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,5 +39,6 @@ include( ":identity:credentialmanager", ":xr", ":watchfacepush:validator", - ":kmp:shared", + ":kmp:androidApp", + ":kmp:shared" ) From a75378c2dd57fa5bc184995473d1bc75592850be Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 3 Sep 2025 17:11:02 +0100 Subject: [PATCH 066/120] Add android_wear_tile_version_fallback (#614) --- wear/src/main/AndroidManifest.xml | 15 ++++++ .../com/example/wear/snippets/tile/Tile.kt | 48 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 02b60438b..a585fb701 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -181,6 +181,21 @@ android:resource="@drawable/tile_preview" /> + + + + + + + + { + + // [START android_wear_tile_version_fallback] + val rendererVersion = requestParams.deviceConfiguration.rendererSchemaVersion + + val arcElement = + // DashedArcLine has the annotation @RequiresSchemaVersion(major = 1, minor = 500) + // and so is supported by renderer versions 1.500 and greater + if ( + rendererVersion.major > 1 || + (rendererVersion.major == 1 && rendererVersion.minor >= 500) + ) { + // Use DashedArcLine if the renderer supports it … + DashedArcLine.Builder() + .setLength(degrees(270f)) + .setThickness(8f) + .setLinePattern( + LayoutElementBuilders.DashedLinePattern.Builder() + .setGapSize(8f) + .setGapInterval(10f) + .build() + ) + .build() + } else { + // … otherwise use ArcLine. + ArcLine.Builder().setLength(degrees(270f)).setThickness(dp(8f)).build() + } + // [END android_wear_tile_version_fallback] + + val layout = + materialScope(this, requestParams.deviceConfiguration) { + primaryLayout(mainSlot = { Arc.Builder().addContent(arcElement).build() }) + } + + return Futures.immediateFuture( + Tile.Builder().setTileTimeline(Timeline.fromLayoutElement(layout)).build() + ) + } +} From 3d5181b4813a17a1c236b134cc207dfee625885c Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:30:18 -0700 Subject: [PATCH 067/120] Disabling some "unused" warnings (#610) * Disabling some "unused" warnings I'd submitted code that's never actually used in the android/snippets apps. Ideally we'd include it in an app, but in the meantime, disabling the "unused" warnings. Also removed one line (outside of a published-on-DAC code block) that turned out not to be needed. No changes to the code that's included on DAC, and the code still compiles. (I also see some areas where the actual code snippet used on DAC is generating warnings, but I'll resolve those in separate PRs that can be reviewed by the appropriate SMEs for those docs.) --- .../java/com/example/snippets/PreloadManagerKotlinSnippets.kt | 4 ++-- .../example/snippets/backgroundwork/WakeLockSnippetsJava.java | 2 ++ .../example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt index c120e824f..9a67db280 100644 --- a/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt +++ b/misc/src/main/java/com/example/snippets/PreloadManagerKotlinSnippets.kt @@ -30,6 +30,7 @@ import java.lang.Math.abs // constants to make the code snippets work const val currentPlayingIndex = 10 +@Suppress("unused_parameter") @UnstableApi // [START android_defaultpreloadmanager_MyTargetPreloadStatusControl] class MyTargetPreloadStatusControl( @@ -57,6 +58,7 @@ class MyTargetPreloadStatusControl( } // [END android_defaultpreloadmanager_MyTargetPreloadStatusControl] +@Suppress("unused_parameter") class PreloadManagerSnippetsKotlin { class PreloadSnippetsActivity : AppCompatActivity() { @@ -73,8 +75,6 @@ class PreloadManagerSnippetsKotlin { val preloadManager = preloadManagerBuilder.build() // [END android_defaultpreloadmanager_createPLM] - val player = preloadManagerBuilder.buildExoPlayer() - // [START android_defaultpreloadmanager_addMedia] val initialMediaItems = pullMediaItemsFromService(/* count= */ 20) for (index in 0 until initialMediaItems.size) { diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java index 7b9a9e682..0ab8d0314 100644 --- a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; +@SuppressWarnings("unused") public class WakeLockSnippetsJava extends Activity { PowerManager.WakeLock wakeLock; @@ -23,6 +24,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } + @SuppressWarnings("unused") // [START android_backgroundwork_wakelock_release_java] void doSomethingAndRelease() throws MyException { try { diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt index 18ead1619..5a398bfdc 100644 --- a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.os.PowerManager // Snippets for doc page go here +@Suppress("unused_parameter") class WakeLockSnippetsKotlin : Activity() { // [START android_backgroundwork_wakelock_create_kotlin] From 30ed522851a9273c94afcd3a4c30bf674346ad18 Mon Sep 17 00:00:00 2001 From: Andrew S <149095394+asolovay@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:17:59 -0700 Subject: [PATCH 068/120] Resolve warnings in the Wakelock code snippets (#615) * Resolve warnings in the Wakelock code snippets I was getting code warnings in Android Studio, made some minor changes to resolve them: * used WakeLock.acquire(timeout) instead of acquire() * removed the redundant qualifier for POWER_SERVICE Once this PR is approved, I'll set up a corresponding docs CL to update the description to explain the acquire() parameter. * Apply Spotless * Per Alice's suggestion, used constant casing for the wakelock timeout (wlTimeout -> WAKELOCK_TIMEOUT) --- .../snippets/backgroundwork/WakeLockSnippetsJava.java | 5 +++-- .../snippets/backgroundwork/WakeLockSnippetsKotlin.kt | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java index 0ab8d0314..d7fca9b2e 100644 --- a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java @@ -10,6 +10,7 @@ public class WakeLockSnippetsJava extends Activity { PowerManager.WakeLock wakeLock; + final long WAKELOCK_TIMEOUT = 10*60*1000L; // 10 minutes @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -18,7 +19,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag"); - wakeLock.acquire(); + wakeLock.acquire(WAKELOCK_TIMEOUT); // [END android_backgroundwork_wakelock_create_java] super.onCreate(savedInstanceState); @@ -28,7 +29,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // [START android_backgroundwork_wakelock_release_java] void doSomethingAndRelease() throws MyException { try { - wakeLock.acquire(); + wakeLock.acquire(WAKELOCK_TIMEOUT); doTheWork(); } finally { wakeLock.release(); diff --git a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt index 5a398bfdc..2a2f3278a 100644 --- a/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt +++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt @@ -17,7 +17,6 @@ package com.example.snippets.backgroundwork import android.app.Activity -import android.content.Context import android.os.Bundle import android.os.PowerManager @@ -25,11 +24,12 @@ import android.os.PowerManager @Suppress("unused_parameter") class WakeLockSnippetsKotlin : Activity() { + val WAKELOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes // [START android_backgroundwork_wakelock_create_kotlin] val wakeLock: PowerManager.WakeLock = - (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + (getSystemService(POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag").apply { - acquire() + acquire(WAKELOCK_TIMEOUT) } } // [END android_backgroundwork_wakelock_create_kotlin] @@ -44,7 +44,7 @@ class WakeLockSnippetsKotlin : Activity() { fun doSomethingAndRelease() { wakeLock.apply { try { - acquire() + acquire(WAKELOCK_TIMEOUT) doTheWork() } finally { release() From 89ecf7101aea0ad3c5797916d11a07ce0bd14321 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 10 Sep 2025 16:29:56 +0100 Subject: [PATCH 069/120] Migrate snippets from dac/training/wearables/tiles/screen-size (#617) --- .../com/example/wear/snippets/m3/tile/Tile.kt | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt index 2ad9812e2..da8ec0897 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt @@ -16,17 +16,25 @@ package com.example.wear.snippets.m3.tile +import android.content.Context import androidx.wear.protolayout.ResourceBuilders.Resources import androidx.wear.protolayout.TimelineBuilders.Timeline import androidx.wear.protolayout.material3.Typography.BODY_LARGE +import androidx.wear.protolayout.material3.button +import androidx.wear.protolayout.material3.buttonGroup import androidx.wear.protolayout.material3.materialScope import androidx.wear.protolayout.material3.primaryLayout import androidx.wear.protolayout.material3.text +import androidx.wear.protolayout.modifiers.clickable 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 androidx.wear.tiles.tooling.preview.Preview +import androidx.wear.tiles.tooling.preview.TilePreviewData +import androidx.wear.tiles.tooling.preview.TilePreviewHelper +import androidx.wear.tooling.preview.devices.WearDevices import com.google.common.util.concurrent.Futures private const val RESOURCES_VERSION = "1" @@ -53,8 +61,77 @@ class MyTileService : TileService() { ) override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture(Resources.Builder().setVersion(RESOURCES_VERSION).build()) +} + +// [END android_wear_m3_tile_mytileservice] + +class TileBreakpoints : TileService() { + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = Futures.immediateFuture( - Resources.Builder().setVersion(RESOURCES_VERSION).build() + Tile.Builder() + .setTileTimeline( + Timeline.fromLayoutElement( + // [START android_wear_tile_breakpoints] + materialScope(this, requestParams.deviceConfiguration) { + // [START_EXCLUDE] + val button1 = + button( + onClick = clickable(), + labelContent = { text("button1".layoutString) }, + ) + val button2 = + button( + onClick = clickable(), + labelContent = { text("button2".layoutString) }, + ) + val button3 = + button( + onClick = clickable(), + labelContent = { text("button3".layoutString) }, + ) + val button4 = + button( + onClick = clickable(), + labelContent = { text("button4".layoutString) }, + ) + val button5 = + button( + onClick = clickable(), + labelContent = { text("button5".layoutString) }, + ) + // [END_EXCLUDE] + val isLargeScreen = deviceConfiguration.screenWidthDp >= 225 + primaryLayout( + mainSlot = { + buttonGroup { + buttonGroupItem { button1 } + buttonGroupItem { button2 } + buttonGroupItem { button3 } + if (isLargeScreen) { + buttonGroupItem { button4 } + buttonGroupItem { button5 } + } + } + } + ) + } + // [END android_wear_tile_breakpoints] + ) + ) + .build() ) } -// [END android_wear_m3_tile_mytileservice] + +// [START android_wear_tile_preview] +@Preview(device = WearDevices.LARGE_ROUND) +fun smallPreview(context: Context) = TilePreviewData { + TilePreviewHelper.singleTimelineEntryTileBuilder( + materialScope(context, it.deviceConfiguration) { + primaryLayout(mainSlot = { text("Hello, World".layoutString) }) + } + ) + .build() +} +// [END android_wear_tile_preview] From 9efabddf7d28b059560ab8e3d3e584fabe481501 Mon Sep 17 00:00:00 2001 From: Ash <83780687+ashnohe@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:02:00 -0700 Subject: [PATCH 070/120] add WindowInsetsRulers snippet (#616) * add WindowInsetsRulers snippet --- .../snippets/layouts/InsetsSnippets.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt index 77568686d..031d78dfb 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt @@ -28,9 +28,12 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding @@ -44,10 +47,16 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.WindowInsetsRulers +import androidx.compose.ui.layout.layout import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt class InsetSnippetActivity : ComponentActivity() { @@ -147,3 +156,55 @@ fun OverrideDefaultInsetsSnippet() { ) // [END android_compose_insets_override_defaults] } + +// [START android_compose_insets_rulers] +@Composable +fun WindowInsetsRulersDemo(modifier: Modifier) { + Box( + contentAlignment = BottomCenter, + modifier = modifier + .fillMaxSize() + // The mistake that causes issues downstream, as .padding doesn't consume insets. + // While it's correct to instead use .windowInsetsPadding(WindowInsets.navigationBars), + // assume it's difficult to identify this issue to see how WindowInsetsRulers can help. + .padding(WindowInsets.navigationBars.asPaddingValues()) + ) { + TextField( + value = "Demo IME Insets", + onValueChange = {}, + modifier = modifier + // Use alignToSafeDrawing() instead of .imePadding() to precisely place this child + // Composable without having to fix the parent upstream. + .alignToSafeDrawing() + + // .imePadding() + // .fillMaxWidth() + ) + } +} + +fun Modifier.alignToSafeDrawing(): Modifier { + return layout { measurable, constraints -> + if (constraints.hasBoundedWidth && constraints.hasBoundedHeight) { + val placeable = measurable.measure(constraints) + val width = placeable.width + val height = placeable.height + layout(width, height) { + val bottom = WindowInsetsRulers.SafeDrawing.current.bottom + .current(0f).roundToInt() - height + val right = WindowInsetsRulers.SafeDrawing.current.right + .current(0f).roundToInt() + val left = WindowInsetsRulers.SafeDrawing.current.left + .current(0f).roundToInt() + measurable.measure(Constraints.fixed(right - left, height)) + .place(left, bottom) + } + } else { + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + } +} +// [END android_compose_insets_rulers] From 9436a8d28b52eccdaab4e4e3565a3419f81bb94a Mon Sep 17 00:00:00 2001 From: MagicalMeghan <46006059+MagicalMeghan@users.noreply.github.com> Date: Thu, 11 Sep 2025 01:29:15 -0400 Subject: [PATCH 071/120] Update state based (#619) * Update StateBasedText.kt * Update StateBasedText.kt * Update StateBasedText.kt * Apply Spotless * Update StateBasedText.kt * Update StateBasedText.kt --- .../compose/snippets/text/StateBasedText.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt index fb3e75807..e4e901bd8 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt @@ -38,7 +38,6 @@ import androidx.compose.material.TextField //noinspection UsingMaterialAndMaterial3Libraries import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -46,6 +45,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly @@ -144,21 +144,19 @@ fun TextFieldInitialState() { // [END android_compose_state_text_7] } +@Preview(showBackground = true) @Composable fun TextFieldBuffer() { // [START android_compose_state_text_8] - val phoneNumberState = rememberTextFieldState() - - LaunchedEffect(phoneNumberState) { - phoneNumberState.edit { // TextFieldBuffer scope - append("123456789") - } - } + val phoneNumberState = rememberTextFieldState("1234567890") TextField( state = phoneNumberState, - inputTransformation = InputTransformation { // TextFieldBuffer scope - if (asCharSequence().isDigitsOnly()) { + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone + ), + inputTransformation = InputTransformation.maxLength(10).then { + if (!asCharSequence().isDigitsOnly()) { revertAllChanges() } }, @@ -174,32 +172,37 @@ fun TextFieldBuffer() { @Preview @Composable fun EditTextFieldState() { - // [START android_compose_state_text_9] val usernameState = rememberTextFieldState("I love Android") + editTFState(usernameState) +} + +fun editTFState(textFieldState: TextFieldState) { + // [START android_compose_state_text_9] + // Initial textFieldState text passed in is "I love Android" // textFieldState.text : I love Android // textFieldState.selection: TextRange(14, 14) - usernameState.edit { insert(14, "!") } + textFieldState.edit { insert(14, "!") } // textFieldState.text : I love Android! // textFieldState.selection: TextRange(15, 15) - usernameState.edit { replace(7, 14, "Compose") } + textFieldState.edit { replace(7, 14, "Compose") } // textFieldState.text : I love Compose! // textFieldState.selection: TextRange(15, 15) - usernameState.edit { append("!!!") } + textFieldState.edit { append("!!!") } // textFieldState.text : I love Compose!!!! // textFieldState.selection: TextRange(18, 18) - usernameState.edit { selectAll() } + textFieldState.edit { selectAll() } // textFieldState.text : I love Compose!!!! // textFieldState.selection: TextRange(0, 18) // [END android_compose_state_text_9] // [START android_compose_state_text_10] - usernameState.setTextAndPlaceCursorAtEnd("I really love Android") + textFieldState.setTextAndPlaceCursorAtEnd("I really love Android") // textFieldState.text : I really love Android // textFieldState.selection : TextRange(21, 21) // [END android_compose_state_text_10] // [START android_compose_state_text_11] - usernameState.clearText() + textFieldState.clearText() // textFieldState.text : // textFieldState.selection : TextRange(0, 0) // [END android_compose_state_text_11] From 292ef1d272c0bc68a2d226871394065a97b06795 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 11 Sep 2025 13:24:32 +0100 Subject: [PATCH 072/120] Update the wear preview code --- .../src/main/java/com/example/wear/snippets/m3/tile/Tile.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt index da8ec0897..5299b685d 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt @@ -19,6 +19,7 @@ package com.example.wear.snippets.m3.tile import android.content.Context import androidx.wear.protolayout.ResourceBuilders.Resources import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.material3.MaterialScope import androidx.wear.protolayout.material3.Typography.BODY_LARGE import androidx.wear.protolayout.material3.button import androidx.wear.protolayout.material3.buttonGroup @@ -124,12 +125,15 @@ class TileBreakpoints : TileService() { ) } +fun MaterialScope.myAdaptiveLayout() = + primaryLayout(mainSlot = { text("Hello, World".layoutString) }) + // [START android_wear_tile_preview] @Preview(device = WearDevices.LARGE_ROUND) fun smallPreview(context: Context) = TilePreviewData { TilePreviewHelper.singleTimelineEntryTileBuilder( materialScope(context, it.deviceConfiguration) { - primaryLayout(mainSlot = { text("Hello, World".layoutString) }) + myAdaptiveLayout() // varies the layout depending on the size of the screen } ) .build() From 73e600d758d1cd0e0769a85ad16d5a773452f170 Mon Sep 17 00:00:00 2001 From: Lauren Ward Date: Thu, 11 Sep 2025 12:56:29 -0600 Subject: [PATCH 073/120] Adding shadows code snippets (#620) * Adding shadows code snippets * Apply Spotless * Moving @Preview before code tags and updating ELevationBasedShadow() * Apply Spotless * Adding delay import --------- Co-authored-by: wardlauren <203715894+wardlauren@users.noreply.github.com> --- .../compose/snippets/graphics/Shadows.kt | 616 ++++++++++++++++++ 1 file changed, 616 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt new file mode 100644 index 000000000..8e2b208b5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt @@ -0,0 +1,616 @@ +/* + * 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.compose.snippets.graphics + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.ui.theme.SnippetsTheme +import kotlinx.coroutines.delay + +@Preview(showBackground = true) +// [START android_compose_graphics_simple_shadow] +@Composable +fun ElevationBasedShadow() { + Box( + modifier = Modifier.aspectRatio(1f).fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Box( + Modifier + .size(100.dp, 100.dp) + .shadow(10.dp, RectangleShape) + .background(Color.White) + ) + } +} +// [END android_compose_graphics_simple_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_simple_drop_shadow] +@Composable +fun SimpleDropShadowUsage() { + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(300.dp) + .dropShadow( + shape = RoundedCornerShape(20.dp), + shadow = Shadow( + radius = 10.dp, + spread = 6.dp, + color = Color(0x40000000), + offset = DpOffset(x = 4.dp, 4.dp) + ) + ) + .align(Alignment.Center) + .background( + color = Color.White, + shape = RoundedCornerShape(20.dp) + ) + ) { + Text( + "Drop Shadow", + modifier = Modifier.align(Alignment.Center), + fontSize = 32.sp + ) + } + } +} +// [END android_compose_graphics_simple_drop_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_simple_inner_shadow] +@Composable +fun SimpleInnerShadowUsage() { + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color.White, + shape = RoundedCornerShape(20.dp) + ) + .innerShadow( + shape = RoundedCornerShape(20.dp), + shadow = Shadow( + radius = 10.dp, + spread = 2.dp, + color = Color(0x40000000), + offset = DpOffset(x = 6.dp, 7.dp) + ) + ) + + ) { + Text( + "Inner Shadow", + modifier = Modifier.align(Alignment.Center), + fontSize = 32.sp + ) + } + } +} +// [END android_compose_graphics_simple_inner_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFF232323 +) +// [START android_compose_graphics_realistic_shadow] +@Composable +fun RealisticShadows() { + Box(Modifier.fillMaxSize()) { + val dropShadowColor1 = Color(0xB3000000) + val dropShadowColor2 = Color(0x66000000) + + val innerShadowColor1 = Color(0xCC000000) + val innerShadowColor2 = Color(0xFF050505) + val innerShadowColor3 = Color(0x40FFFFFF) + val innerShadowColor4 = Color(0x1A050505) + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 40.dp, + spread = 0.dp, + color = dropShadowColor1, + offset = DpOffset(x = 2.dp, 8.dp) + ) + ) + .dropShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 4.dp, + spread = 0.dp, + color = dropShadowColor2, + offset = DpOffset(x = 0.dp, 4.dp) + ) + ) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color.Black, + shape = RoundedCornerShape(100.dp) + ) +// // + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 12.dp, + spread = 3.dp, + color = innerShadowColor1, + offset = DpOffset(x = 6.dp, 6.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 4.dp, + spread = 1.dp, + color = Color.White, + offset = DpOffset(x = 5.dp, 5.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 12.dp, + spread = 5.dp, + color = innerShadowColor2, + offset = DpOffset(x = (-3).dp, (-12).dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 3.dp, + spread = 10.dp, + color = innerShadowColor3, + offset = DpOffset(x = 0.dp, 0.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 3.dp, + spread = 9.dp, + color = innerShadowColor4, + offset = DpOffset(x = 1.dp, 1.dp) + ) + ) + + ) { + Text( + "Realistic Shadows", + modifier = Modifier.align(Alignment.Center), + fontSize = 24.sp, + color = Color.White + ) + } + } +} +// [END android_compose_graphics_realistic_shadow] + +// Define breathing states +enum class BreathingState { + Inhaling, + Exhaling +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +@Composable +fun GradientBasedShadowAnimation() { + SnippetsTheme { + val colors = listOf( + Color(0xFF4cc9f0), + Color(0xFFf72585), + Color(0xFFb5179e), + Color(0xFF7209b7), + Color(0xFF560bad), + Color(0xFF480ca8), + Color(0xFF3a0ca3), + Color(0xFF3f37c9), + Color(0xFF4361ee), + Color(0xFF4895ef), + Color(0xFF4cc9f0) + ) + + // .. + + // State for the breathing animation + var breathingState by remember { mutableStateOf(BreathingState.Inhaling) } + + // Create transition based on breathing state + val transition = updateTransition( + targetState = breathingState, + label = "breathing_transition" + ) + + // Animate spread based on breathing state + val animatedSpread by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 5000, + easing = FastOutSlowInEasing + ) + }, + label = "spread_animation" + ) { state -> + when (state) { + BreathingState.Inhaling -> 10f + BreathingState.Exhaling -> 2f + } + } + + // Animate alpha based on breathing state (optional) + val animatedAlpha by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 2000, + easing = FastOutSlowInEasing + ) + }, + label = "alpha_animation" + ) { state -> + when (state) { + BreathingState.Inhaling -> 1f + BreathingState.Exhaling -> 1f + } + } + + // Get text based on current state + val breathingText = when (breathingState) { + BreathingState.Inhaling -> "Inhale" + BreathingState.Exhaling -> "Exhale" + } + + // Switch states when animation completes + LaunchedEffect(breathingState) { + delay(5000) // Wait for animation to complete + breathingState = when (breathingState) { + BreathingState.Inhaling -> BreathingState.Exhaling + BreathingState.Exhaling -> BreathingState.Inhaling + } + } + + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + // [START android_compose_graphics_gradient_shadow] + Box( + modifier = Modifier + .width(240.dp) + .height(200.dp) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = animatedSpread.dp, + brush = Brush.sweepGradient( + colors + ), + offset = DpOffset(x = 0.dp, y = 0.dp), + alpha = animatedAlpha + ) + ) + .clip(RoundedCornerShape(70.dp)) + .background(Color(0xEDFFFFFF)), + contentAlignment = Alignment.Center + ) { + Text( + text = breathingText, + color = Color.Black, + style = MaterialTheme.typography.bodyLarge + ) + } + // [END android_compose_graphics_gradient_shadow] + } + } +} + +@Preview +// [START android_compose_graphics_neumorphic_shadow] +@Composable +fun NeumorphicRaisedButton( + shape: RoundedCornerShape = RoundedCornerShape(30.dp) +) { + val bgColor = Color(0xFFe0e0e0) + val lightShadow = Color(0xFFFFFFFF) + val darkShadow = Color(0xFFb1b1b1) + val upperOffset = -10.dp + val lowerOffset = 10.dp + val radius = 15.dp + val spread = 0.dp + Box( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + .wrapContentSize(Alignment.Center) + .size(240.dp) + .dropShadow( + shape, + shadow = Shadow( + radius = radius, + color = lightShadow, + spread = spread, + offset = DpOffset(upperOffset, upperOffset) + ), + ) + .dropShadow( + shape, + shadow = Shadow( + radius = radius, + color = darkShadow, + spread = spread, + offset = DpOffset(lowerOffset, lowerOffset) + ), + + ) + .background(bgColor, shape) + ) +} +// [END android_compose_graphics_neumorphic_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_animated_shadow] +@Composable +fun AnimatedColoredShadows() { + SnippetsTheme { + Box(Modifier.fillMaxSize()) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + // Create transition with pressed state + val transition = updateTransition( + targetState = isPressed, + label = "button_press_transition" + ) + + fun buttonPressAnimation() = tween( + durationMillis = 400, + easing = EaseInOut + ) + + // Animate all properties using the transition + val shadowAlpha by transition.animateFloat( + label = "shadow_alpha", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) 0f else 1f + } + // [START_EXCLUDE] + val innerShadowAlpha by transition.animateFloat( + label = "shadow_alpha", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) 1f else 0f + } + + val blueDropShadowColor = Color(0x5C007AFF) + + val darkBlueDropShadowColor = Color(0x66007AFF) + + val greyInnerShadowColor1 = Color(0x1A007AFF) + + val greyInnerShadowColor2 = Color(0x1A007AFF) + // [END_EXCLUDE] + + val blueDropShadow by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color.Transparent else blueDropShadowColor + } + + // [START_EXCLUDE] + val darkBlueDropShadow by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color.Transparent else darkBlueDropShadowColor + } + + val innerShadowColor1 by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) greyInnerShadowColor1 + else greyInnerShadowColor2 + } + + val innerShadowColor2 by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color(0x4D007AFF) + else Color(0x1A007AFF) + } + // [END_EXCLUDE] + + Box( + Modifier + .clickable( + interactionSource, indication = null + ) { + // ** ...... **// + } + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = 0.dp, + color = blueDropShadow, + offset = DpOffset(x = 0.dp, -(2).dp), + alpha = shadowAlpha + ) + ) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = 0.dp, + color = darkBlueDropShadow, + offset = DpOffset(x = 2.dp, 6.dp), + alpha = shadowAlpha + ) + ) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(70.dp) + ) + .innerShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 8.dp, + spread = 4.dp, + color = innerShadowColor2, + offset = DpOffset(x = 4.dp, 0.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 20.dp, + spread = 4.dp, + color = innerShadowColor1, + offset = DpOffset(x = 4.dp, 0.dp), + alpha = innerShadowAlpha + ) + ) + + ) { + Text( + "Animated Shadows", + // [START_EXCLUDE] + modifier = Modifier + .align(Alignment.Center), + style = MaterialTheme.typography.bodyLarge, + fontSize = 24.sp, + color = Color.Black + // [END_EXCLUDE] + ) + } + } + } +} +// [END android_compose_graphics_animated_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFCC00 +) +// [START android_compose_graphics_neobrutal_shadow] +@Composable +fun NeoBrutalShadows() { + SnippetsTheme { + val dropShadowColor = Color(0xFF007AFF) + val borderColor = Color(0xFFFF2D55) + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(0.dp), + shadow = Shadow( + radius = 0.dp, + spread = 0.dp, + color = dropShadowColor, + offset = DpOffset(x = 8.dp, 8.dp) + ) + ) + .border( + 8.dp, borderColor + ) + .background( + color = Color.White, + shape = RoundedCornerShape(0.dp) + ) + ) { + Text( + "Neobrutal Shadows", + modifier = Modifier.align(Alignment.Center), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} +// [END android_compose_graphics_neobrutal_shadow] From 1da1d9d645cd1a8e693981900e04d6bc32287a5c Mon Sep 17 00:00:00 2001 From: Ash <83780687+ashnohe@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:23:31 -0700 Subject: [PATCH 074/120] Add basic WindowInsetsRulers cases (#621) --- .../snippets/layouts/InsetsSnippets.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt index 031d78dfb..e893e8099 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt @@ -31,6 +31,8 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fitInside import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars @@ -49,9 +51,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.RectRulers import androidx.compose.ui.layout.WindowInsetsRulers +import androidx.compose.ui.layout.innermostOf import androidx.compose.ui.layout.layout import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Constraints @@ -208,3 +213,53 @@ fun Modifier.alignToSafeDrawing(): Modifier { } } // [END android_compose_insets_rulers] + +// [START android_compose_insets_fit_inside] +@Composable +fun FitInsideDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + // Or DisplayCutout, Ime, NavigationBars, StatusBar, etc... + .fitInside(WindowInsetsRulers.SafeDrawing.current) + ) +} +// [END android_compose_insets_fit_inside] + +// [START android_compose_insets_rulers_ime] +@Composable +fun FitInsideWithImeDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .fitInside( + RectRulers.innermostOf( + WindowInsetsRulers.NavigationBars.current, + WindowInsetsRulers.Ime.current + ) + ) + ) { + TextField( + value = "Demo IME Insets", + onValueChange = {}, + modifier = modifier.align(Alignment.BottomStart).fillMaxWidth() + ) + } +} +// [END android_compose_insets_rulers_ime] + +// [START android_compose_insets_rulers_status_caption_bars] +@Composable +fun FitInsideWithStatusAndCaptionBarDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .fitInside( + RectRulers.innermostOf( + WindowInsetsRulers.StatusBars.current, + WindowInsetsRulers.CaptionBar.current + ) + ) + ) +} +// [END android_compose_insets_rulers_status_caption_bars] From 2f246fb3f712172fefe2e1761f3483568348bf90 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 26 Sep 2025 15:55:09 +0200 Subject: [PATCH 075/120] Update XR snippets to be compatible with the September release versions (#622) --- gradle/libs.versions.toml | 6 +- .../java/com/example/xr/arcore/Anchors.kt | 5 +- .../main/java/com/example/xr/arcore/Hands.kt | 21 +++---- .../main/java/com/example/xr/arcore/Planes.kt | 5 +- .../java/com/example/xr/compose/Orbiter.kt | 10 ++-- .../com/example/xr/compose/SpatialPanel.kt | 15 +++-- .../java/com/example/xr/compose/Volume.kt | 9 +-- .../xr/scenecore/ResizableComponent.kt | 5 +- .../com/example/xr/scenecore/SpatialVideo.kt | 59 +++++++++---------- 9 files changed, 63 insertions(+), 72 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bab1c791..4330d2542 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,9 +28,9 @@ androidx-test-junit = "1.3.0" androidx-window = "1.5.0-beta02" androidx-window-core = "1.5.0-beta02" androidx-window-java = "1.5.0-beta02" -androidx-xr-arcore = "1.0.0-alpha05" -androidx-xr-compose = "1.0.0-alpha06" -androidx-xr-scenecore = "1.0.0-alpha06" +androidx-xr-arcore = "1.0.0-alpha06" +androidx-xr-compose = "1.0.0-alpha07" +androidx-xr-scenecore = "1.0.0-alpha07" androidxHiltNavigationCompose = "1.2.0" appcompat = "1.7.1" coil = "2.7.0" diff --git a/xr/src/main/java/com/example/xr/arcore/Anchors.kt b/xr/src/main/java/com/example/xr/arcore/Anchors.kt index 56c7cb8d7..ad8ac26a6 100644 --- a/xr/src/main/java/com/example/xr/arcore/Anchors.kt +++ b/xr/src/main/java/com/example/xr/arcore/Anchors.kt @@ -21,7 +21,6 @@ import androidx.xr.arcore.AnchorCreateSuccess import androidx.xr.arcore.Trackable import androidx.xr.runtime.Config import androidx.xr.runtime.Session -import androidx.xr.runtime.SessionConfigureConfigurationNotSupported import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.scenecore.AnchorEntity @@ -34,11 +33,9 @@ fun configureAnchoring(session: Session) { anchorPersistence = Config.AnchorPersistenceMode.LOCAL, ) when (val result = session.configure(newConfig)) { - is SessionConfigureConfigurationNotSupported -> - TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) is SessionConfigureSuccess -> TODO(/* Success! */) else -> - TODO(/* A different unhandled exception was thrown. */) + TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */) } // [END androidxr_arcore_anchoring_configure] } diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt index 1507bed7c..3c0fd82fb 100644 --- a/xr/src/main/java/com/example/xr/arcore/Hands.kt +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -20,10 +20,9 @@ import android.app.Activity import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope import androidx.xr.arcore.Hand +import androidx.xr.arcore.HandJointType import androidx.xr.runtime.Config -import androidx.xr.runtime.HandJointType import androidx.xr.runtime.Session -import androidx.xr.runtime.SessionConfigureConfigurationNotSupported import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion @@ -40,11 +39,9 @@ fun ComponentActivity.configureSession(session: Session) { handTracking = Config.HandTrackingMode.BOTH ) when (val result = session.configure(newConfig)) { - is SessionConfigureConfigurationNotSupported -> - TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) is SessionConfigureSuccess -> TODO(/* Success! */) else -> - TODO(/* A different unhandled exception was thrown. */) + TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */) } // [END androidxr_arcore_hand_configure] } @@ -80,7 +77,7 @@ fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { val session: Session = null!! val palmEntity: GltfModelEntity = null!! // [START androidxr_arcore_hand_entityAtHandPalm] - val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return + val palmPose = leftHandState.handJoints[HandJointType.HAND_JOINT_TYPE_PALM] ?: return // the down direction points in the same direction as the palm val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up) @@ -101,7 +98,7 @@ fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { val indexFingerEntity: GltfModelEntity = null!! // [START androidxr_arcore_hand_entityAtIndexFingerTip] - val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return + val tipPose = rightHandState.handJoints[HandJointType.HAND_JOINT_TYPE_INDEX_TIP] ?: return // the forward direction points towards the finger tip. val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up) @@ -120,9 +117,9 @@ fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { private fun detectPinch(session: Session, handState: Hand.State): Boolean { // [START androidxr_arcore_hand_pinch_gesture] - val thumbTip = handState.handJoints[HandJointType.THUMB_TIP] ?: return false + val thumbTip = handState.handJoints[HandJointType.HAND_JOINT_TYPE_THUMB_TIP] ?: return false val thumbTipPose = session.scene.perceptionSpace.transformPoseTo(thumbTip, session.scene.activitySpace) - val indexTip = handState.handJoints[HandJointType.INDEX_TIP] ?: return false + val indexTip = handState.handJoints[HandJointType.HAND_JOINT_TYPE_INDEX_TIP] ?: return false val indexTipPose = session.scene.perceptionSpace.transformPoseTo(indexTip, session.scene.activitySpace) return Vector3.distance(thumbTipPose.translation, indexTipPose.translation) < 0.05 // [END androidxr_arcore_hand_pinch_gesture] @@ -136,8 +133,8 @@ private fun detectStop(session: Session, handState: Hand.State): Boolean { val forward2 = handState.handJoints[joint2]?.forward ?: return false return Vector3.angleBetween(forward1, forward2) < threshold } - return pointingInSameDirection(HandJointType.INDEX_PROXIMAL, HandJointType.INDEX_TIP) && - pointingInSameDirection(HandJointType.MIDDLE_PROXIMAL, HandJointType.MIDDLE_TIP) && - pointingInSameDirection(HandJointType.RING_PROXIMAL, HandJointType.RING_TIP) + return pointingInSameDirection(HandJointType.HAND_JOINT_TYPE_INDEX_PROXIMAL, HandJointType.HAND_JOINT_TYPE_INDEX_TIP) && + pointingInSameDirection(HandJointType.HAND_JOINT_TYPE_MIDDLE_PROXIMAL, HandJointType.HAND_JOINT_TYPE_MIDDLE_TIP) && + pointingInSameDirection(HandJointType.HAND_JOINT_TYPE_RING_PROXIMAL, HandJointType.HAND_JOINT_TYPE_RING_TIP) // [END androidxr_arcore_hand_stop_gesture] } diff --git a/xr/src/main/java/com/example/xr/arcore/Planes.kt b/xr/src/main/java/com/example/xr/arcore/Planes.kt index b5017dcb7..828f37857 100644 --- a/xr/src/main/java/com/example/xr/arcore/Planes.kt +++ b/xr/src/main/java/com/example/xr/arcore/Planes.kt @@ -19,7 +19,6 @@ package com.example.xr.arcore import androidx.xr.arcore.Plane import androidx.xr.runtime.Config import androidx.xr.runtime.Session -import androidx.xr.runtime.SessionConfigureConfigurationNotSupported import androidx.xr.runtime.SessionConfigureSuccess import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Ray @@ -31,11 +30,9 @@ fun configurePlaneTracking(session: Session) { planeTracking = Config.PlaneTrackingMode.HORIZONTAL_AND_VERTICAL, ) when (val result = session.configure(newConfig)) { - is SessionConfigureConfigurationNotSupported -> - TODO(/* Some combinations of configurations are not valid. Handle this failure case. */) is SessionConfigureSuccess -> TODO(/* Success! */) else -> - TODO(/* A different unhandled exception was thrown. */) + TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */) } // [END androidxr_arcore_planetracking_configure] } diff --git a/xr/src/main/java/com/example/xr/compose/Orbiter.kt b/xr/src/main/java/com/example/xr/compose/Orbiter.kt index f01c4cd1f..8b89e4c63 100644 --- a/xr/src/main/java/com/example/xr/compose/Orbiter.kt +++ b/xr/src/main/java/com/example/xr/compose/Orbiter.kt @@ -40,13 +40,13 @@ import androidx.xr.compose.spatial.ContentEdge import androidx.xr.compose.spatial.Orbiter import androidx.xr.compose.spatial.OrbiterOffsetType import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.MovePolicy +import androidx.xr.compose.subspace.ResizePolicy import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.SpatialRow import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.height -import androidx.xr.compose.subspace.layout.movable -import androidx.xr.compose.subspace.layout.resizable import androidx.xr.compose.subspace.layout.width import com.example.xr.R @@ -57,9 +57,9 @@ private fun OrbiterExampleSubspace() { SpatialPanel( SubspaceModifier .height(824.dp) - .width(1400.dp) - .movable() - .resizable() + .width(1400.dp), + dragPolicy = MovePolicy(), + resizePolicy = ResizePolicy(), ) { SpatialPanelContent() OrbiterExample() diff --git a/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt index c3a3a58ef..2b75a426b 100644 --- a/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt +++ b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt @@ -29,11 +29,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.xr.compose.platform.LocalSpatialCapabilities import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.MovePolicy +import androidx.xr.compose.subspace.ResizePolicy import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.height -import androidx.xr.compose.subspace.layout.movable -import androidx.xr.compose.subspace.layout.resizable import androidx.xr.compose.subspace.layout.width @Composable @@ -43,9 +43,9 @@ private fun SpatialPanelExample() { SpatialPanel( SubspaceModifier .height(824.dp) - .width(1400.dp) - .movable() - .resizable() + .width(1400.dp), + dragPolicy = MovePolicy(), + resizePolicy = ResizePolicy(), ) { SpatialPanelContent() } @@ -81,9 +81,8 @@ private fun ContentInSpatialPanel() { if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { Subspace { SpatialPanel( - SubspaceModifier - .resizable(true) - .movable(true) + dragPolicy = MovePolicy(), + resizePolicy = ResizePolicy(), ) { AppContent() } diff --git a/xr/src/main/java/com/example/xr/compose/Volume.kt b/xr/src/main/java/com/example/xr/compose/Volume.kt index cce37ca83..587cc3019 100644 --- a/xr/src/main/java/com/example/xr/compose/Volume.kt +++ b/xr/src/main/java/com/example/xr/compose/Volume.kt @@ -28,13 +28,13 @@ import androidx.compose.ui.unit.sp import androidx.xr.compose.platform.LocalSession import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.ExperimentalSubspaceVolumeApi +import androidx.xr.compose.subspace.MovePolicy +import androidx.xr.compose.subspace.ResizePolicy import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.Volume import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.height -import androidx.xr.compose.subspace.layout.movable import androidx.xr.compose.subspace.layout.offset -import androidx.xr.compose.subspace.layout.resizable import androidx.xr.compose.subspace.layout.scale import androidx.xr.compose.subspace.layout.width import kotlinx.coroutines.launch @@ -44,8 +44,9 @@ private fun VolumeExample() { // [START androidxr_compose_Volume] Subspace { SpatialPanel( - SubspaceModifier.height(1500.dp).width(1500.dp) - .resizable().movable() + SubspaceModifier.height(1500.dp).width(1500.dp), + dragPolicy = MovePolicy(), + resizePolicy = ResizePolicy(), ) { ObjectInAVolume(true) Box( diff --git a/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt b/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt index 28b70027d..e4136ea72 100644 --- a/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt +++ b/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt @@ -17,6 +17,7 @@ package com.example.xr.scenecore import androidx.xr.runtime.Session +import androidx.xr.runtime.math.FloatSize2d import androidx.xr.runtime.math.FloatSize3d import androidx.xr.scenecore.ResizableComponent import androidx.xr.scenecore.ResizeEvent @@ -32,11 +33,11 @@ private fun resizableComponentExample( val resizableComponent = ResizableComponent.create(session) { event -> if (event.resizeState == ResizeEvent.ResizeState.RESIZE_STATE_END) { // update the Entity to reflect the new size - surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Quad(event.newSize.width, event.newSize.height) + surfaceEntity.shape = SurfaceEntity.Shape.Quad(FloatSize2d(event.newSize.width, event.newSize.height)) } } resizableComponent.minimumEntitySize = FloatSize3d(177f, 100f, 1f) - resizableComponent.fixedAspectRatio = 16f / 9f // Specify a 16:9 aspect ratio + resizableComponent.isFixedAspectRatioEnabled = true // Maintain a fixed aspect ratio when resizing surfaceEntity.addComponent(resizableComponent) // [END androidxr_scenecore_resizableComponentExample] diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index ec733bb61..85f1dfa05 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -24,11 +24,11 @@ import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.xr.runtime.Session +import androidx.xr.runtime.math.FloatSize2d import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Vector3 import androidx.xr.scenecore.SurfaceEntity import androidx.xr.scenecore.Texture -import androidx.xr.scenecore.TextureSampler import androidx.xr.scenecore.scene import java.nio.file.Paths import kotlinx.coroutines.launch @@ -36,10 +36,10 @@ import kotlinx.coroutines.launch private fun ComponentActivity.surfaceEntityCreate(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreate] val stereoSurfaceEntity = SurfaceEntity.create( - xrSession, - SurfaceEntity.StereoMode.SIDE_BY_SIDE, - Pose(Vector3(0.0f, 0.0f, -1.5f)), - SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), + shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)) ) val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) @@ -60,13 +60,13 @@ private fun ComponentActivity.surfaceEntityCreateSbs(xrSession: Session) { // Set up the surface for playing a 180° video on a hemisphere. val hemisphereStereoSurfaceEntity = SurfaceEntity.create( - xrSession, - SurfaceEntity.StereoMode.SIDE_BY_SIDE, - xrSession.scene.spatialUser.head?.transformPoseTo( + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE, + pose = xrSession.scene.spatialUser.head?.transformPoseTo( Pose.Identity, xrSession.scene.activitySpace )!!, - SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f), + shape = SurfaceEntity.Shape.Hemisphere(1.0f), ) // ... and use the surface for playing the media. // [END androidxr_scenecore_surfaceEntityCreateSbs] @@ -77,13 +77,13 @@ private fun ComponentActivity.surfaceEntityCreateTb(xrSession: Session) { // Set up the surface for playing a 360° video on a sphere. val sphereStereoSurfaceEntity = SurfaceEntity.create( - xrSession, - SurfaceEntity.StereoMode.TOP_BOTTOM, - xrSession.scene.spatialUser.head?.transformPoseTo( + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM, + pose = xrSession.scene.spatialUser.head?.transformPoseTo( Pose.Identity, xrSession.scene.activitySpace )!!, - SurfaceEntity.CanvasShape.Vr360Sphere(1.0f), + shape = SurfaceEntity.Shape.Sphere(1.0f), ) // ... and use the surface for playing the media. // [END androidxr_scenecore_surfaceEntityCreateTb] @@ -93,10 +93,10 @@ private fun ComponentActivity.surfaceEntityCreateMVHEVC(xrSession: Session) { // [START androidxr_scenecore_surfaceEntityCreateMVHEVC] // Create the SurfaceEntity with the StereoMode corresponding to the MV-HEVC content val stereoSurfaceEntity = SurfaceEntity.create( - xrSession, - SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY, - Pose(Vector3(0.0f, 0.0f, -1.5f)), - SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f) + session = xrSession, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY, + pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), + shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)) ) val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) @@ -123,10 +123,10 @@ private fun ComponentActivity.surfaceEntityCreateDRM(xrSession: Session) { // Create the SurfaceEntity with the PROTECTED content security level. val protectedSurfaceEntity = SurfaceEntity.create( session = xrSession, - stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE, pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), - canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f), - contentSecurityLevel = SurfaceEntity.ContentSecurityLevel.PROTECTED + shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)), + surfaceProtection = SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED ) // Build a MediaItem with the necessary DRM configuration. @@ -156,20 +156,20 @@ private fun ComponentActivity.surfaceEntityHDR(xrSession: Session) { // Define the color properties for your HDR video. These values should be specific // to your content. val hdrMetadata = SurfaceEntity.ContentColorMetadata( - colorSpace = SurfaceEntity.ContentColorMetadata.ColorSpace.BT2020, - colorTransfer = SurfaceEntity.ContentColorMetadata.ColorTransfer.ST2084, // PQ - colorRange = SurfaceEntity.ContentColorMetadata.ColorRange.LIMITED, - maxCLL = 1000 // Example: 1000 nits + colorSpace = SurfaceEntity.ContentColorMetadata.ColorSpace.COLOR_SPACE_BT2020, + colorTransfer = SurfaceEntity.ContentColorMetadata.ColorTransfer.COLOR_TRANSFER_ST2084, // PQ + colorRange = SurfaceEntity.ContentColorMetadata.ColorRange.COLOR_RANGE_LIMITED, + maxContentLightLevel = 1000 // Example: 1000 nits ) // Create a SurfaceEntity, passing the HDR metadata at creation time. val hdrSurfaceEntity = SurfaceEntity.create( session = xrSession, - stereoMode = SurfaceEntity.StereoMode.MONO, + stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO, pose = Pose(Vector3(0.0f, 0.0f, -1.5f)), - canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f), - contentColorMetadata = hdrMetadata + shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)), ) + hdrSurfaceEntity.contentColorMetadata = hdrMetadata // Initialize ExoPlayer and set the surface. val exoPlayer = ExoPlayer.Builder(this).build() @@ -195,8 +195,8 @@ private fun surfaceEntityEdgeFeathering(xrSession: Session) { ) // Feather the edges of the surface. - surfaceEntity.edgeFeather = - SurfaceEntity.EdgeFeatheringParams.SmoothFeather(0.1f, 0.1f) + surfaceEntity.edgeFeatheringParams = + SurfaceEntity.EdgeFeatheringParams.RectangleFeather(0.1f, 0.1f) // [END androidxr_scenecore_surfaceEntityEdgeFeathering] } @@ -214,7 +214,6 @@ private fun surfaceEntityAlphaMasking(xrSession: Session, activity: ComponentAct Texture.create( xrSession, Paths.get("textures", "alpha_mask.png"), - TextureSampler.create() ) // Apply the alpha mask. From c2e40eecf3f9cb5a57ab4bffa42993bbb54742b7 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 26 Sep 2025 19:01:47 +0200 Subject: [PATCH 076/120] Upgrade to Gradle wrapper version 9.1.0 (#627) --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c..2e1113280 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 27c462ce7811288e941a5432beccd63b02db80c9 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Fri, 26 Sep 2025 19:24:53 +0200 Subject: [PATCH 077/120] Upgrade to Spotless 7 (#628) --- .github/workflows/apply_spotless.yml | 2 +- bluetoothle/src/main/AndroidManifest.xml | 17 +++- .../src/main/res/layout/activity_main.xml | 17 +++- bluetoothle/src/main/res/values/strings.xml | 16 ++++ build.gradle.kts | 83 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 15 ++++ .../src/main/res/values/strings.xml | 26 +++--- .../src/main/res/values/themes.xml | 18 +++- compose/snippets/lint.xml | 19 ++++- compose/snippets/src/main/AndroidManifest.xml | 31 ++++--- .../example/compose/snippets/ui/theme/Type.kt | 2 +- .../drawable-v24/ic_launcher_foreground.xml | 32 +++---- .../drawable/baseline_directions_bus_24.xml | 30 +++---- .../drawable/baseline_shopping_cart_24.xml | 30 +++---- .../src/main/res/drawable/button_outline.xml | 32 +++---- .../src/main/res/drawable/fast_forward.xml | 16 ++++ .../main/res/drawable/fast_forward_filled.xml | 16 ++++ .../src/main/res/drawable/fast_rewind.xml | 16 ++++ .../main/res/drawable/fast_rewind_filled.xml | 16 ++++ .../src/main/res/drawable/favorite.xml | 16 ++++ .../src/main/res/drawable/favorite_filled.xml | 16 ++++ .../res/drawable/ic_hourglass_animated.xml | 20 ++--- .../res/drawable/ic_launcher_background.xml | 29 ++++--- .../src/main/res/drawable/ic_logo.xml | 30 +++---- .../src/main/res/drawable/ic_moon_24.xml | 16 ++++ .../src/main/res/drawable/ic_sun_24.xml | 16 ++++ .../src/main/res/layout/activity_example.xml | 31 ++++--- .../src/main/res/layout/example_layout.xml | 31 ++++--- .../src/main/res/layout/example_view.xml | 29 ++++--- .../src/main/res/layout/fragment_example.xml | 31 ++++--- .../interop_layout_preview_composable.xml | 31 ++++--- .../migration_strategy_existing_screens.xml | 31 ++++--- .../layout/migration_strategy_xml_example.xml | 31 ++++--- .../src/main/res/layout/my_container_view.xml | 32 +++---- .../main/res/layout/my_fragment_layout.xml | 31 ++++--- ...chinput_gestures_nested_scroll_interop.xml | 31 ++++--- .../res/mipmap-anydpi-v26/ic_launcher.xml | 31 ++++--- .../mipmap-anydpi-v26/ic_launcher_round.xml | 31 ++++--- .../src/main/res/values-es/strings.xml | 32 +++---- .../snippets/src/main/res/values/colors.xml | 31 ++++--- .../snippets/src/main/res/values/dimens.xml | 31 ++++--- compose/snippets/src/main/res/values/ids.xml | 31 ++++--- .../snippets/src/main/res/values/strings.xml | 32 +++---- .../snippets/src/main/res/values/themes.xml | 31 ++++--- .../src/main/res/xml/my_app_widget_info.xml | 32 +++---- gradle/init.gradle.kts | 52 ------------ gradle/libs.versions.toml | 3 + .../src/main/AndroidManifest.xml | 27 +++--- .../drawable-v24/ic_launcher_foreground.xml | 18 +++- .../res/drawable/ic_launcher_background.xml | 15 ++++ .../src/main/res/layout-v34/xmlsnippets.xml | 29 ++++--- .../res/mipmap-anydpi-v26/ic_launcher.xml | 17 +++- .../mipmap-anydpi-v26/ic_launcher_round.xml | 17 +++- .../src/main/res/values/colors.xml | 17 +++- .../src/main/res/values/strings.xml | 18 +++- .../src/main/res/values/themes.xml | 17 +++- .../src/main/res/xml/provider.xml | 15 ++++ .../src/main/res/xml/provider_settings.xml | 15 ++++ kmp/androidApp/src/main/AndroidManifest.xml | 17 +++- .../src/androidMain/AndroidManifest.xml | 17 +++- kotlin/src/main/AndroidManifest.xml | 15 ++++ misc/src/main/AndroidManifest.xml | 15 ++++ .../ActivityEmbeddingKotlinSnippets.kt | 4 +- .../com/example/snippets/ui/theme/Type.kt | 2 +- .../res/drawable/ic_launcher_background.xml | 15 ++++ .../res/drawable/ic_launcher_foreground.xml | 18 +++- misc/src/main/res/layout/activity_main.xml | 16 +++- .../res/mipmap-anydpi-v26/ic_launcher.xml | 17 +++- .../mipmap-anydpi-v26/ic_launcher_round.xml | 17 +++- misc/src/main/res/values/colors.xml | 15 ++++ misc/src/main/res/values/strings.xml | 18 +++- misc/src/main/res/values/themes.xml | 17 +++- misc/src/main/res/xml/main_split_config.xml | 18 +++- spotless/copyright.kts | 16 ++++ spotless/copyright.xml | 16 ++++ views/src/main/AndroidManifest.xml | 23 +++-- .../main/res/layout/system_bar_protection.xml | 25 +++--- views/src/main/res/layout/widget_preview.xml | 24 +++--- wear/lint.xml | 19 ++++- wear/src/main/AndroidManifest.xml | 15 ++++ wear/src/main/res/drawable/animated_walk.xml | 4 +- .../main/res/drawable/complication_icon.xml | 16 ++++ .../res/drawable/ic_launcher_background.xml | 15 ++++ .../res/drawable/ic_launcher_foreground.xml | 18 +++- wear/src/main/res/drawable/ic_walk.xml | 16 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 17 +++- .../mipmap-anydpi-v26/ic_launcher_round.xml | 17 +++- wear/src/main/res/values/strings.xml | 18 +++- xr/src/main/AndroidManifest.xml | 17 +++- .../com/example/xr/scenecore/GltfEntity.kt | 2 +- .../com/example/xr/scenecore/SpatialAudio.kt | 2 +- xr/src/main/res/layout/example_fragment.xml | 17 +++- xr/src/main/res/values/dimens.xml | 17 +++- 93 files changed, 1373 insertions(+), 590 deletions(-) delete mode 100644 gradle/init.gradle.kts create mode 100644 spotless/copyright.kts create mode 100644 spotless/copyright.xml diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index d69f817b0..3727da63a 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -42,7 +42,7 @@ jobs: java-version: '17' - name: Run spotlessApply - run: ./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + run: ./gradlew spotlessApply --stacktrace - name: Auto-commit if spotlessApply has changes uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/bluetoothle/src/main/AndroidManifest.xml b/bluetoothle/src/main/AndroidManifest.xml index d15b213e2..b42865576 100644 --- a/bluetoothle/src/main/AndroidManifest.xml +++ b/bluetoothle/src/main/AndroidManifest.xml @@ -1,4 +1,19 @@ + @@ -24,4 +39,4 @@ - \ No newline at end of file + diff --git a/bluetoothle/src/main/res/layout/activity_main.xml b/bluetoothle/src/main/res/layout/activity_main.xml index b692bf4a3..b46563beb 100644 --- a/bluetoothle/src/main/res/layout/activity_main.xml +++ b/bluetoothle/src/main/res/layout/activity_main.xml @@ -1,4 +1,19 @@ + - \ No newline at end of file + diff --git a/bluetoothle/src/main/res/values/strings.xml b/bluetoothle/src/main/res/values/strings.xml index 7abc06d3b..fa7796f20 100644 --- a/bluetoothle/src/main/res/values/strings.xml +++ b/bluetoothle/src/main/res/values/strings.xml @@ -1 +1,17 @@ + + diff --git a/build.gradle.kts b/build.gradle.kts index 899385a15..bafd85c4e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,89 @@ plugins { alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.android.kotlin.multiplatform.library) apply false alias(libs.plugins.android.lint) apply false + alias(libs.plugins.spotless) apply false +} + +subprojects { + apply(plugin = "com.diffplug.spotless") + extensions.configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + + val disabledRules = arrayOf( + // These rules were introduced in ktlint 0.46.0 and should not be + // enabled without further discussion. They are disabled for now. + // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 + "filename", + "annotation", + "annotation-spacing", + "argument-list-wrapping", + "double-colon-spacing", + "enum-entry-name-case", + "multiline-if-else", + "no-empty-first-line-in-method-block", + "package-name", + "trailing-comma", + "spacing-around-angle-brackets", + "spacing-between-declarations-with-annotations", + "spacing-between-declarations-with-comments", + "unary-op-spacing", + "no-trailing-spaces", + "max-line-length", + // Disabled rules that were introduced or changed between 0.46.0 ~ 1.50.0 + "class-signature", + "trailing-comma-on-call-site", + "trailing-comma-on-declaration-site", + "comment-wrapping", + "function-literal", + "function-signature", + "function-expression-body", + "function-start-of-body-spacing", + "multiline-expression-wrapping", + ) + + ktlint(libs.versions.ktlint.get()).editorConfigOverride( + mapOf( + "android" to "true", + "ktlint_code_style" to "android_studio", + "ij_kotlin_allow_trailing_comma" to "true", + ) + disabledRules.map { Pair("ktlint_standard_$it", "disabled") } + ) + + // ktlint 7.0.0 introduces lints, which existing snippets do not satisfy + val kotlinSuppressLints = arrayOf( + "standard:function-naming", + "standard:property-naming", + "standard:class-naming", + "standard:max-line-length", + "standard:comment-wrapping", + "standard:import-ordering", + "standard:filename", + "standard:backing-property-naming", + ) + for (lint in kotlinSuppressLints) { + suppressLintsFor { + step = "ktlint" + shortCode = lint + } + } + + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts") + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)") + } + format("xml") { + target("**/*.xml") + targetExclude("**/build/**/*.xml") + // Look for the root tag or a tag that is a snippet + licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[a-zA-Z])|( + Copyright 2018 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. +--> RecomposeHighlighter diff --git a/compose/recomposehighlighter/src/main/res/values/themes.xml b/compose/recomposehighlighter/src/main/res/values/themes.xml index 918929c1b..f94279433 100644 --- a/compose/recomposehighlighter/src/main/res/values/themes.xml +++ b/compose/recomposehighlighter/src/main/res/values/themes.xml @@ -1,5 +1,21 @@ + + - \ No newline at end of file + diff --git a/compose/snippets/lint.xml b/compose/snippets/lint.xml index 778dd98f3..9ec5c3429 100644 --- a/compose/snippets/lint.xml +++ b/compose/snippets/lint.xml @@ -1,6 +1,21 @@ - + + - \ No newline at end of file + diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index 4ec4d9b70..5c0f57b2e 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> @@ -70,4 +69,4 @@ - \ No newline at end of file + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt index acf007bfd..ab604e852 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt @@ -46,5 +46,5 @@ val Typography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) - */ + */ ) diff --git a/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml b/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml index 2f9758487..0f6026a41 100644 --- a/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -1,19 +1,19 @@ + + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml b/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml index 2de5d0a86..745eab18a 100644 --- a/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml +++ b/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml @@ -1,19 +1,19 @@ + + Copyright 2023 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. +--> diff --git a/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml b/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml index 817fb0539..15368afa6 100644 --- a/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml +++ b/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml @@ -1,19 +1,19 @@ + + Copyright 2023 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. +--> diff --git a/compose/snippets/src/main/res/drawable/button_outline.xml b/compose/snippets/src/main/res/drawable/button_outline.xml index f4eb02291..790fd2eb2 100644 --- a/compose/snippets/src/main/res/drawable/button_outline.xml +++ b/compose/snippets/src/main/res/drawable/button_outline.xml @@ -1,22 +1,22 @@ + + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/drawable/fast_forward.xml b/compose/snippets/src/main/res/drawable/fast_forward.xml index d49dffbf3..95cdea9c4 100644 --- a/compose/snippets/src/main/res/drawable/fast_forward.xml +++ b/compose/snippets/src/main/res/drawable/fast_forward.xml @@ -1,3 +1,19 @@ + + + + + + + diff --git a/compose/snippets/src/main/res/drawable/ic_launcher_background.xml b/compose/snippets/src/main/res/drawable/ic_launcher_background.xml index 4c2360d21..2edadf928 100644 --- a/compose/snippets/src/main/res/drawable/ic_launcher_background.xml +++ b/compose/snippets/src/main/res/drawable/ic_launcher_background.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> + Copyright 2023 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. +--> diff --git a/compose/snippets/src/main/res/drawable/ic_moon_24.xml b/compose/snippets/src/main/res/drawable/ic_moon_24.xml index 527559f3f..b1c551d5e 100644 --- a/compose/snippets/src/main/res/drawable/ic_moon_24.xml +++ b/compose/snippets/src/main/res/drawable/ic_moon_24.xml @@ -1,3 +1,19 @@ + + diff --git a/compose/snippets/src/main/res/drawable/ic_sun_24.xml b/compose/snippets/src/main/res/drawable/ic_sun_24.xml index a63c6400e..4bac89d3e 100644 --- a/compose/snippets/src/main/res/drawable/ic_sun_24.xml +++ b/compose/snippets/src/main/res/drawable/ic_sun_24.xml @@ -1,3 +1,19 @@ + + diff --git a/compose/snippets/src/main/res/layout/activity_example.xml b/compose/snippets/src/main/res/layout/activity_example.xml index be37892ac..b85c61cf1 100644 --- a/compose/snippets/src/main/res/layout/activity_example.xml +++ b/compose/snippets/src/main/res/layout/activity_example.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/example_layout.xml b/compose/snippets/src/main/res/layout/example_layout.xml index d77d31543..91e49c045 100644 --- a/compose/snippets/src/main/res/layout/example_layout.xml +++ b/compose/snippets/src/main/res/layout/example_layout.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/example_view.xml b/compose/snippets/src/main/res/layout/example_view.xml index 77fff6e8c..86bdacae8 100644 --- a/compose/snippets/src/main/res/layout/example_view.xml +++ b/compose/snippets/src/main/res/layout/example_view.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> diff --git a/compose/snippets/src/main/res/layout/fragment_example.xml b/compose/snippets/src/main/res/layout/fragment_example.xml index fd65463ef..d5517e3c4 100644 --- a/compose/snippets/src/main/res/layout/fragment_example.xml +++ b/compose/snippets/src/main/res/layout/fragment_example.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml b/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml index bb28c5685..b3a672cbe 100644 --- a/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml +++ b/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml b/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml index 2ef7e2dcb..a7bd893b7 100644 --- a/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml +++ b/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml b/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml index ac462f26c..e5ed1b289 100644 --- a/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml +++ b/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/my_container_view.xml b/compose/snippets/src/main/res/layout/my_container_view.xml index 4d0ecb396..cec8feedc 100644 --- a/compose/snippets/src/main/res/layout/my_container_view.xml +++ b/compose/snippets/src/main/res/layout/my_container_view.xml @@ -1,21 +1,21 @@ + + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/my_fragment_layout.xml b/compose/snippets/src/main/res/layout/my_fragment_layout.xml index d2428b777..3b0ec2765 100644 --- a/compose/snippets/src/main/res/layout/my_fragment_layout.xml +++ b/compose/snippets/src/main/res/layout/my_fragment_layout.xml @@ -1,24 +1,23 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/layout/touchinput_gestures_nested_scroll_interop.xml b/compose/snippets/src/main/res/layout/touchinput_gestures_nested_scroll_interop.xml index bbc6128e7..229f37a13 100644 --- a/compose/snippets/src/main/res/layout/touchinput_gestures_nested_scroll_interop.xml +++ b/compose/snippets/src/main/res/layout/touchinput_gestures_nested_scroll_interop.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 0b0b5355c..05f11b4d6 100644 --- a/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,22 +1,21 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 0b0b5355c..05f11b4d6 100644 --- a/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/compose/snippets/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,22 +1,21 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values-es/strings.xml b/compose/snippets/src/main/res/values-es/strings.xml index 3ba327f49..7287beb35 100644 --- a/compose/snippets/src/main/res/values-es/strings.xml +++ b/compose/snippets/src/main/res/values-es/strings.xml @@ -1,19 +1,19 @@ + + Copyright 2023 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. +--> snippets Golden Retriever in fall leaves @@ -53,4 +53,4 @@ Compras Perfil Esto es sólo un texto de marcador de posición. - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values/colors.xml b/compose/snippets/src/main/res/values/colors.xml index 0205675f4..55242e03e 100644 --- a/compose/snippets/src/main/res/values/colors.xml +++ b/compose/snippets/src/main/res/values/colors.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> #FFBB86FC #FF6200EE @@ -25,4 +24,4 @@ #FFFFFFFF #FFF #FFF - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values/dimens.xml b/compose/snippets/src/main/res/values/dimens.xml index d8dec0639..99c6f584a 100644 --- a/compose/snippets/src/main/res/values/dimens.xml +++ b/compose/snippets/src/main/res/values/dimens.xml @@ -1,20 +1,19 @@ + Copyright 2023 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. +--> 8dp - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values/ids.xml b/compose/snippets/src/main/res/values/ids.xml index f97cf8a5b..6cc94fcf1 100644 --- a/compose/snippets/src/main/res/values/ids.xml +++ b/compose/snippets/src/main/res/values/ids.xml @@ -1,23 +1,22 @@ + Copyright 2023 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. +--> - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values/strings.xml b/compose/snippets/src/main/res/values/strings.xml index 02254e29a..d4b9a63a6 100644 --- a/compose/snippets/src/main/res/values/strings.xml +++ b/compose/snippets/src/main/res/values/strings.xml @@ -1,19 +1,19 @@ + + Copyright 2023 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. +--> snippets Golden Retriever in fall leaves @@ -55,4 +55,4 @@ This is just a placeholder. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - \ No newline at end of file + diff --git a/compose/snippets/src/main/res/values/themes.xml b/compose/snippets/src/main/res/values/themes.xml index 3d9f463cd..9e7318559 100644 --- a/compose/snippets/src/main/res/values/themes.xml +++ b/compose/snippets/src/main/res/values/themes.xml @@ -1,21 +1,20 @@ + Copyright 2023 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. +--> + + diff --git a/wear/src/main/res_other/values/dimens.xml b/wear/src/main/res_other/values/dimens.xml new file mode 100644 index 000000000..2d1e73370 --- /dev/null +++ b/wear/src/main/res_other/values/dimens.xml @@ -0,0 +1,22 @@ + + + + + + 36dp + + diff --git a/wear/src/main/res_other/values/styles.xml b/wear/src/main/res_other/values/styles.xml new file mode 100644 index 000000000..d0bccfea6 --- /dev/null +++ b/wear/src/main/res_other/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + From c54efa02c13681ae89640b357164fd81b522df45 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 14 Nov 2025 14:37:56 +0000 Subject: [PATCH 112/120] Attempt to fix rendering error (#693) --- wear/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 6b977a557..8e12f5ec0 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -36,10 +36,10 @@ android:exported="true" android:taskAffinity="" android:theme="@style/Theme.App.Starting"> - + + - From 07fc3540fe179a766d0b0a9e54a1898b526c4153 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 14 Nov 2025 17:34:36 +0000 Subject: [PATCH 113/120] Add snippets for /training/wearables/tiles/debug (#694) * Add snippets for /training/wearables/tiles/debug * Create dynamic preview * Suppress lint warnings for code sample --- .../com/example/wear/snippets/tile/Preview.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/tile/Preview.kt diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Preview.kt b/wear/src/main/java/com/example/wear/snippets/tile/Preview.kt new file mode 100644 index 000000000..dc259b741 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/tile/Preview.kt @@ -0,0 +1,125 @@ +/* + * 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. + */ + +@file:Suppress("MissingPermission") + +package com.example.wear.snippets.tile + +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters +import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.material3.materialScope +import androidx.wear.protolayout.material3.primaryLayout +import androidx.wear.protolayout.material3.text +import androidx.wear.protolayout.types.stringLayoutConstraint +import androidx.wear.tiles.tooling.preview.Preview +import androidx.wear.tiles.tooling.preview.TilePreviewData +import androidx.wear.tiles.tooling.preview.TilePreviewHelper +import androidx.wear.tooling.preview.devices.WearDevices +import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.expression.DynamicDataBuilders +import androidx.wear.protolayout.expression.PlatformDataValues +import androidx.wear.protolayout.expression.PlatformHealthSources +import com.example.wear.R +import androidx.wear.protolayout.types.layoutString +import androidx.wear.protolayout.types.asLayoutString + +// [START android_wear_tile_preview_simple] +@Preview(device = WearDevices.SMALL_ROUND) +@Preview(device = WearDevices.LARGE_ROUND) +fun tilePreview(context: Context) = TilePreviewData { request -> + TilePreviewHelper.singleTimelineEntryTileBuilder( + buildMyTileLayout(context, request.deviceConfiguration) + ).build() +} +// [END android_wear_tile_preview_simple] + +fun buildMyTileLayout( + context: Context, + deviceParameters: DeviceParameters, +) = materialScope(context = context, deviceConfiguration = deviceParameters) { + primaryLayout( + mainSlot = { + text("Hello world!".layoutString) + } + ) + } + +private const val RESOURCES_VERSION = "1" +private const val myImageId = "myImageId" + +// [START android_wear_tile_preview_resources] +@Preview(device = WearDevices.SMALL_ROUND) +fun previewWithResources(context: Context) = TilePreviewData( + onTileResourceRequest = { request -> + Resources.Builder() + .setVersion(RESOURCES_VERSION) + .addIdToImageMapping( + myImageId, + getImageById(R.drawable.animated_walk) + ) + .build() + }, + onTileRequest = { request -> + TilePreviewHelper.singleTimelineEntryTileBuilder( + buildMyTileLayout(context, request.deviceConfiguration) + ).build() + } +) +// [END android_wear_tile_preview_resources] + +fun getImageById( + @DrawableRes id: Int, +): ResourceBuilders.ImageResource = + ResourceBuilders.ImageResource.Builder() + .setAndroidResourceByResId( + ResourceBuilders.AndroidImageResourceByResId.Builder() + .setResourceId(id) + .build(), + ) + .build() + +fun buildMyTileLayoutDynamic( + context: Context, + deviceParameters: DeviceParameters, +) = materialScope(context = context, deviceConfiguration = deviceParameters) { + primaryLayout( + mainSlot = { + text( + text = + PlatformHealthSources.heartRateBpm() + .format() + .asLayoutString("--", stringLayoutConstraint("999")) + ) + } + ) +} + +// [START android_wear_tile_preview_platform] +@Preview(device = WearDevices.SMALL_ROUND) +fun previewWithPlatformOverride(context: Context) = TilePreviewData( + platformDataValues = PlatformDataValues.of( + PlatformHealthSources.Keys.HEART_RATE_BPM, + DynamicDataBuilders.DynamicDataValue.fromFloat(160f) + ), + onTileRequest = { request -> + TilePreviewHelper.singleTimelineEntryTileBuilder( + buildMyTileLayoutDynamic(context, request.deviceConfiguration) + ).build() + } +) +// [END android_wear_tile_preview_platform] From c3472d3aaf4621926fb389fa03681e183e9424bd Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 18 Nov 2025 22:34:20 -0500 Subject: [PATCH 114/120] Snippets for /training/wearables/data/data-items (#696) --- .../snippets/datalayer/DataLayerActivity.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt diff --git a/wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt b/wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt new file mode 100644 index 000000000..a509af721 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt @@ -0,0 +1,82 @@ +/* + * 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.com.example.wear.snippets.datalayer + +import androidx.activity.ComponentActivity +import com.google.android.gms.tasks.Task +import com.google.android.gms.wearable.DataClient +import com.google.android.gms.wearable.DataEvent +import com.google.android.gms.wearable.DataEventBuffer +import com.google.android.gms.wearable.DataItem +import com.google.android.gms.wearable.DataMapItem +import com.google.android.gms.wearable.PutDataMapRequest +import com.google.android.gms.wearable.PutDataRequest +import com.google.android.gms.wearable.Wearable + +class DataLayerActivity : ComponentActivity(), DataClient.OnDataChangedListener { + private val dataClient by lazy { Wearable.getDataClient(this) } + private val messageClient by lazy { Wearable.getMessageClient(this) } + private val capabilityClient by lazy { Wearable.getCapabilityClient(this) } + + private var count = 0 + + override fun onResume() { + super.onResume() + Wearable.getDataClient(this).addListener(this) + } + + override fun onPause() { + super.onPause() + Wearable.getDataClient(this).removeListener(this) + } + + // [START android_wear_datalayer_increasecounter] + private fun increaseCounter() { + val putDataReq: PutDataRequest = PutDataMapRequest.create("/count").run { + dataMap.putInt(COUNT_KEY, count++) + asPutDataRequest() + } + val putDataTask: Task = dataClient.putDataItem(putDataReq) + } + // [END android_wear_datalayer_increasecounter] + + // [START android_wear_datalayer_ondatachangedlistener] + override fun onDataChanged(dataEvents: DataEventBuffer) { + + dataEvents.forEach { event -> + // DataItem changed + if (event.type == DataEvent.TYPE_CHANGED) { + event.dataItem.also { item -> + if (item.uri.path?.compareTo("/count") == 0) { + DataMapItem.fromDataItem(item).dataMap.apply { + updateCount(getInt(COUNT_KEY)) + } + } + } + } else if (event.type == DataEvent.TYPE_DELETED) { + // DataItem deleted + } + } + } + // [END android_wear_datalayer_ondatachangedlistener] + + private fun updateCount(int: Int) { + } + companion object { + private const val COUNT_KEY = "com.example.key.count" + } +} From afe04dd9e728503cd889c071da1cac401af51cdf Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 19 Nov 2025 06:52:52 -0500 Subject: [PATCH 115/120] Snippets for /training/wearables/tiles/dynamic (#695) --- wear/src/main/AndroidManifest.xml | 15 +++ .../com/example/wear/snippets/m3/tile/Tile.kt | 99 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 8e12f5ec0..5d6427abe 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -345,6 +345,21 @@ + + + + + + + + diff --git a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt index 5299b685d..569617ff8 100644 --- a/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt @@ -17,16 +17,29 @@ package com.example.wear.snippets.m3.tile import android.content.Context +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.LayoutElementBuilders.Column import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.StateBuilders import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.expression.DynamicBuilders +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.Typography.BODY_MEDIUM import androidx.wear.protolayout.material3.button import androidx.wear.protolayout.material3.buttonGroup 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.asLayoutConstraint +import androidx.wear.protolayout.types.asLayoutString import androidx.wear.protolayout.types.layoutString import androidx.wear.tiles.RequestBuilders import androidx.wear.tiles.RequestBuilders.ResourcesRequest @@ -37,6 +50,7 @@ import androidx.wear.tiles.tooling.preview.TilePreviewData import androidx.wear.tiles.tooling.preview.TilePreviewHelper import androidx.wear.tooling.preview.devices.WearDevices import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture private const val RESOURCES_VERSION = "1" @@ -139,3 +153,88 @@ fun smallPreview(context: Context) = TilePreviewData { .build() } // [END android_wear_tile_preview] + +class StateTile : TileService() { + // [START android_wear_tile_dynamic_ontilerequest] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + // If the tile hasn't had any state set yet, use the default values + val state = + if (requestParams.currentState.keyToValueMapping.isNotEmpty()) + requestParams.currentState + else + StateBuilders.State.Builder() + .setStateMap( + dynamicDataMapOf( + KEY_WATER_INTAKE mapTo 200, + KEY_NOTE mapTo "Good" + ) + ) + .build() + + return Futures.immediateFuture( + Tile.Builder() + // Set resources, timeline, and other tile properties. + // [START_EXCLUDE silent] + .setTileTimeline( + Timeline.fromLayoutElement( + materialScope(this, requestParams.deviceConfiguration) { + tileLayout() + } + ) + ) + // [END_EXCLUDE] + .setState(state) + .build() + ) + } + // [END android_wear_tile_dynamic_ontilerequest] + + fun MaterialScope.tileLayout(): LayoutElementBuilders.LayoutElement { + return primaryLayout( + mainSlot = { + // [START android_wear_tile_dynamic_showdata] + val waterIntakeValue = + DynamicBuilders.DynamicInt32.from(KEY_WATER_INTAKE) + // [END android_wear_tile_dynamic_showdata] + .format() + .asLayoutString("0", "000".asLayoutConstraint()) + val noteValue = + DynamicBuilders.DynamicString.from(KEY_NOTE) + .asLayoutString( + "", + "Note about day".asLayoutConstraint() + ) + + // [START android_wear_tile_dynamic_loadaction] + val loadAction = + loadAction( + dynamicDataMapOf( + KEY_WATER_INTAKE mapTo 400, + KEY_NOTE mapTo "Outstanding" + ) + ) + // [END android_wear_tile_dynamic_loadaction] + + Column.Builder() + .addContent(text(waterIntakeValue, typography = BODY_LARGE)) + .addContent(text(noteValue, typography = BODY_MEDIUM)) + .addContent( + textButton( + onClick = clickable(loadAction), + labelContent = { text("Load".layoutString) } + ) + ) + .build() + } + ) + } + + // [START android_wear_tile_dynamic_companion] + companion object { + val KEY_WATER_INTAKE = intAppDataKey("key_water_intake") + val KEY_NOTE = stringAppDataKey("key_note") + } + // [END android_wear_tile_dynamic_companion] +} From 89b0781c2e45f9985ce0bd5134e7c653545b8409 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 19 Nov 2025 08:24:43 -0500 Subject: [PATCH 116/120] Add snippets for /training/wearables/data/{sync,events} (#697) * Add snippets for /training/wearables/data/sync * Fix file location * Snippets for /training/wearables/data/events --- wear/src/main/AndroidManifest.xml | 18 +++- .../datalayer/DataLayerActivity.kt | 75 ++++++++++++++++- .../snippets/datalayer/DataLayerService.kt | 84 +++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) rename wear/src/main/java/com/example/wear/snippets/{com/example/wear/snippets => }/datalayer/DataLayerActivity.kt (50%) create mode 100644 wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 5d6427abe..ea4f18966 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - + @@ -360,6 +361,21 @@ android:resource="@drawable/tile_preview" /> + + + + + + + + + diff --git a/wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt similarity index 50% rename from wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt rename to wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt index a509af721..ecf3ff7e3 100644 --- a/wear/src/main/java/com/example/wear/snippets/com/example/wear/snippets/datalayer/DataLayerActivity.kt +++ b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerActivity.kt @@ -14,10 +14,16 @@ * limitations under the License. */ -package com.example.wear.snippets.com.example.wear.snippets.datalayer +package com.example.wear.snippets.datalayer +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import androidx.activity.ComponentActivity +import com.example.wear.R import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.android.gms.wearable.Asset import com.google.android.gms.wearable.DataClient import com.google.android.gms.wearable.DataEvent import com.google.android.gms.wearable.DataEventBuffer @@ -26,6 +32,8 @@ import com.google.android.gms.wearable.DataMapItem import com.google.android.gms.wearable.PutDataMapRequest import com.google.android.gms.wearable.PutDataRequest import com.google.android.gms.wearable.Wearable +import java.io.ByteArrayOutputStream +import java.io.InputStream class DataLayerActivity : ComponentActivity(), DataClient.OnDataChangedListener { private val dataClient by lazy { Wearable.getDataClient(this) } @@ -80,3 +88,68 @@ class DataLayerActivity : ComponentActivity(), DataClient.OnDataChangedListener private const val COUNT_KEY = "com.example.key.count" } } + +// [START android_wear_sync_createasset] +private fun createAssetFromBitmap(bitmap: Bitmap): Asset = + ByteArrayOutputStream().let { byteStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream) + Asset.createFromBytes(byteStream.toByteArray()) + } +// [END android_wear_sync_createasset] + +// [START android_wear_datalayer_imageputdata] +private fun Context.sendImagePutDataRequest(): Task { + + val asset: Asset = createAssetFromBitmap(BitmapFactory.decodeResource(resources, R.drawable.ic_walk)) + val request: PutDataRequest = PutDataRequest.create("/image").apply { + putAsset("profileImage", asset) + } + val putTask: Task = Wearable.getDataClient(this).putDataItem(request) + + return putTask +} +// [END android_wear_datalayer_imageputdata] + +// [START android_wear_datalayer_imageputdatamap] +private fun Context.sendImagePutDataMapRequest(): Task { + + val asset: Asset = createAssetFromBitmap(BitmapFactory.decodeResource(resources, R.drawable.ic_walk)) + val request: PutDataRequest = PutDataMapRequest.create("/image").run { + dataMap.putAsset("profileImage", asset) + asPutDataRequest() + } + val putTask: Task = Wearable.getDataClient(this).putDataItem(request) + + return putTask +} +// [END android_wear_datalayer_imageputdatamap] + +class DataLayerActivity2 : ComponentActivity(), DataClient.OnDataChangedListener { + // [START android_wear_datalayer_ondatachanged_assetextract] + override fun onDataChanged(dataEvents: DataEventBuffer) { + dataEvents + .filter { it.type == DataEvent.TYPE_CHANGED && it.dataItem.uri.path == "/image" } + .forEach { event -> + val bitmap: Bitmap? = DataMapItem.fromDataItem(event.dataItem) + .dataMap.getAsset("profileImage") + ?.let { asset -> loadBitmapFromAsset(asset) } + // Do something with the bitmap + } + } + + fun loadBitmapFromAsset(asset: Asset): Bitmap? { + // Convert asset into a file descriptor and block until it's ready + val assetInputStream: InputStream? = + Tasks.await(Wearable.getDataClient(this).getFdForAsset(asset)) + ?.inputStream + + return assetInputStream?.let { inputStream -> + // Decode the stream into a bitmap + BitmapFactory.decodeStream(inputStream) + } ?: run { + // Requested an unknown asset + null + } + } + // [END android_wear_datalayer_ondatachanged_assetextract] +} diff --git a/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt new file mode 100644 index 000000000..c69d5114b --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/datalayer/DataLayerService.kt @@ -0,0 +1,84 @@ +/* + * 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.datalayer + +import android.app.Activity +import android.util.Log +import com.google.android.gms.wearable.DataClient +import com.google.android.gms.wearable.DataEvent +import com.google.android.gms.wearable.DataEventBuffer +import com.google.android.gms.wearable.Wearable +import com.google.android.gms.wearable.WearableListenerService + +private const val TAG = "DataLayerSample" +private const val START_ACTIVITY_PATH = "/start-activity" +private const val DATA_ITEM_RECEIVED_PATH = "/data-item-received" + +// [START android_wear_datalayer_datalayerlistenerservice] +class DataLayerListenerService : WearableListenerService() { + + override fun onDataChanged(dataEvents: DataEventBuffer) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onDataChanged: $dataEvents") + } + + // Loop through the events and send a message + // to the node that created the data item. + dataEvents + .map { it.dataItem.uri } + .forEach { uri -> + // Get the node ID from the host value of the URI. + val nodeId: String = uri.host!! + // Set the data of the message to be the bytes of the URI. + val payload: ByteArray = uri.toString().toByteArray() + + // Send the RPC. + Wearable.getMessageClient(this) + .sendMessage( + nodeId, + DATA_ITEM_RECEIVED_PATH, + payload + ) + } + } +} +// [END android_wear_datalayer_datalayerlistenerservice] + +// [START android_wear_datalayer_ondatachangedlisteneer] +class MainActivity : Activity(), DataClient.OnDataChangedListener { + + public override fun onResume() { + super.onResume() + Wearable.getDataClient(this).addListener(this) + } + + override fun onPause() { + super.onPause() + Wearable.getDataClient(this).removeListener(this) + } + + override fun onDataChanged(dataEvents: DataEventBuffer) { + dataEvents.forEach { event -> + if (event.type == DataEvent.TYPE_DELETED) { + Log.d(TAG, "DataItem deleted: " + event.dataItem.uri) + } else if (event.type == DataEvent.TYPE_CHANGED) { + Log.d(TAG, "DataItem changed: " + event.dataItem.uri) + } + } + } +} +// [END android_wear_datalayer_ondatachangedlisteneer] From 906dee1d58555872a724e769ff23a63b9c029870 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Wed, 19 Nov 2025 19:30:14 -0500 Subject: [PATCH 117/120] Add snippets for /training/wearables/tiles/get_started (#698) * Add snippets for /training/wearables/tiles/get_started --- .../com/example/wear/snippets/tile/Tile.kt | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt index e0a39de99..2d3fe3fb4 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -27,6 +27,16 @@ import androidx.wear.protolayout.LayoutElementBuilders import androidx.wear.protolayout.LayoutElementBuilders.Arc import androidx.wear.protolayout.LayoutElementBuilders.ArcLine import androidx.wear.protolayout.LayoutElementBuilders.DashedArcLine +import androidx.wear.protolayout.LayoutElementBuilders.FontStyle +import androidx.wear.protolayout.LayoutElementBuilders.Image +import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement +import androidx.wear.protolayout.LayoutElementBuilders.SpanImage +import androidx.wear.protolayout.LayoutElementBuilders.SpanText +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.ModifiersBuilders.Background +import androidx.wear.protolayout.ModifiersBuilders.Modifiers +import androidx.wear.protolayout.ModifiersBuilders.Semantics +import androidx.wear.protolayout.ResourceBuilders import androidx.wear.protolayout.ResourceBuilders.Resources import androidx.wear.protolayout.TimelineBuilders import androidx.wear.protolayout.TimelineBuilders.Timeline @@ -41,6 +51,7 @@ import androidx.wear.tiles.RequestBuilders import androidx.wear.tiles.RequestBuilders.ResourcesRequest import androidx.wear.tiles.TileBuilders.Tile import androidx.wear.tiles.TileService +import com.example.wear.R import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -248,3 +259,76 @@ class FeatureFallback : TileService() { ) } } + +// [START android_wear_tile_get_started_modifiers] +private fun myImage(): LayoutElement = + Image.Builder() + .setWidth(dp(24f)) + .setHeight(dp(24f)) + .setResourceId("image_id") + .setModifiers( + Modifiers.Builder() + .setBackground(Background.Builder().setColor(argb(0xFFFF0000.toInt())).build()) + .setPadding(ModifiersBuilders.Padding.Builder().setStart(dp(12f)).build()) + .setSemantics(Semantics.Builder().setContentDescription("Image description").build()) + .build() + ) + .build() +// [END android_wear_tile_get_started_modifiers] + +// [START android_wear_tile_get_started_spannables] +private fun mySpannable(): LayoutElement = + LayoutElementBuilders.Spannable.Builder() + .addSpan(SpanText.Builder().setText("Hello ").build()) + .addSpan(SpanImage.Builder().setWidth(dp(24f)).setHeight(dp(24f)).setResourceId("image_id").build()) + .addSpan( + SpanText.Builder() + .setText("world") + .setFontStyle(FontStyle.Builder().setItalic(true).build()) + .build() + ) + .build() +// [END android_wear_tile_get_started_spannables] + +class ResourcesTileService : TileService() { + + private val imageAsByteArray = byteArrayOf() + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(Timeline.fromLayoutElement(simpleLayout(this))) + .build() + ) + + // [START android_wear_tile_get_started_resources] + override fun onTileResourcesRequest( + requestParams: ResourcesRequest + ) = Futures.immediateFuture( + Resources.Builder() + .setVersion("1") + .addIdToImageMapping( + "image_from_resource", + ResourceBuilders.ImageResource.Builder() + .setAndroidResourceByResId( + ResourceBuilders.AndroidImageResourceByResId.Builder() + .setResourceId(R.drawable.ic_walk) + .build() + ).build() + ) + .addIdToImageMapping( + "image_inline", + ResourceBuilders.ImageResource.Builder() + .setInlineResource( + ResourceBuilders.InlineImageResource.Builder() + .setData(imageAsByteArray) + .setWidthPx(48) + .setHeightPx(48) + .setFormat(ResourceBuilders.IMAGE_FORMAT_RGB_565) + .build() + ).build() + ).build() + ) + // [END android_wear_tile_get_started_resources] +} From a015aef3fa63e7a9fed433a06b264c952060c8cc Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 20 Nov 2025 08:54:21 -0500 Subject: [PATCH 118/120] Add snippets for /training/wearables/tiles/versioning (#699) --- .../example/wear/snippets/tile/Versioning.kt | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt b/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt new file mode 100644 index 000000000..0bfdf6ec2 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt @@ -0,0 +1,112 @@ +/* + * 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.tile + +import android.content.Context +import androidx.wear.protolayout.DeviceParametersBuilders +import androidx.wear.protolayout.DimensionBuilders.expand +import androidx.wear.protolayout.LayoutElementBuilders.Column +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.TypeBuilders +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.Typography +import androidx.wear.protolayout.material.layouts.PrimaryLayout +import androidx.wear.protolayout.material3.MaterialScope +import androidx.wear.protolayout.material3.materialScope +import androidx.wear.protolayout.material3.primaryLayout +import androidx.wear.protolayout.material3.text +import androidx.wear.protolayout.modifiers.LayoutModifier +import androidx.wear.protolayout.modifiers.opacity +import androidx.wear.protolayout.types.layoutString + +class LayoutM2 { + // [START android_wear_tile_version_layoutm2] + fun myLayout( + context: Context, + deviceConfiguration: DeviceParametersBuilders.DeviceParameters + ) = + PrimaryLayout.Builder(deviceConfiguration) + .setResponsiveContentInsetEnabled(true) + .setContent( + Text.Builder(context, "Hello World!") + .setTypography(Typography.TYPOGRAPHY_BODY1) + .build() + ) + .build() + // [END android_wear_tile_version_layoutm2] + + // [START android_wear_tile_version_modifierm2] + // Uses Builder-style modifier to set opacity + fun myModifier(): ModifiersBuilders.Modifiers = + ModifiersBuilders.Modifiers.Builder() + .setOpacity(TypeBuilders.FloatProp.Builder(0.5F).build()) + .build() + // [END android_wear_tile_version_modifierm2] +} + +class LayoutM3 { + // [START android_wear_tile_version_layoutm3] + fun myLayout( + context: Context, + deviceConfiguration: DeviceParametersBuilders.DeviceParameters, + ) = + materialScope(context, deviceConfiguration) { + primaryLayout(mainSlot = { text("Hello, World!".layoutString) }) // HELPME + } + // [END android_wear_tile_version_layoutm3] + + // [START android_wear_tile_version_modifierm3] + // Uses Compose-like modifiers to set opacity + fun myModifier(): LayoutModifier = LayoutModifier.opacity(0.5F) + // [END android_wear_tile_version_modifierm3] +} + +private fun MaterialScope.withoutHelpers() { + // [START android_wear_tile_version_layoutwithouthelpers] + primaryLayout( + mainSlot = { + Column.Builder() + .setWidth(expand()) + .setHeight(expand()) + .addContent(text("A".layoutString)) + .addContent(text("B".layoutString)) + .addContent(text("C".layoutString)) + .build() + } + ) + // [END android_wear_tile_version_layoutwithouthelpers] +} + +private fun MaterialScope.withHelpers() { + // [START android_wear_tile_version_layoutwithhelpers] + // Function literal with receiver helper function + fun column(builder: Column.Builder.() -> Unit) = + Column.Builder().apply(builder).build() + + primaryLayout( + mainSlot = { + column { + setWidth(expand()) + setHeight(expand()) + addContent(text("A".layoutString)) + addContent(text("B".layoutString)) + addContent(text("C".layoutString)) + } + } + ) + // [END android_wear_tile_version_layoutwithhelpers] +} From d60730e17c1b263417c82b62f047907b50578a01 Mon Sep 17 00:00:00 2001 From: Edgar Arriaga Date: Thu, 20 Nov 2025 19:03:01 +0000 Subject: [PATCH 119/120] Add snippets for trigger based profiling section of ProfilingManager docs (#688) * Add snippets for trigger based profiling section of ProfilingManager docs * Apply Spotless * Fix tag name * Bump mindSdk so that new ProfilingManager snippets compile * add extra handling logic for trigger callback and exclude some sections * update kotlin snippet for handling callback * Apply Spotless * rename upload job method --------- Co-authored-by: edgararriagag <211581399+edgararriagag@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- .../ProfilingManagerJavaSnippets.java | 56 +++++++++++++++++++ .../ProfilingManagerKotlinSnippets.kt | 55 ++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d108273d..40c26140b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ material3-adaptive-navigation-suite = "1.4.0" media3 = "1.8.0" media3Ui = "1.8.0" # @keep -minSdk = "35" +minSdk = "36" okHttp = "5.2.1" playServicesWearable = "19.0.0" protobuf = "4.32.1" diff --git a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java index 4dae4d353..4c2d2d23b 100644 --- a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java +++ b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerJavaSnippets.java @@ -2,12 +2,18 @@ import android.app.Activity; import android.os.Bundle; +import android.os.ProfilingManager; +import android.os.ProfilingTrigger; import android.util.Log; +import java.util.List; +import java.util.ArrayList; import java.util.function.Consumer; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import android.os.ProfilingResult; import java.util.concurrent.Executors; import android.os.CancellationSignal; +import android.view.Choreographer; import androidx.tracing.Trace; import androidx.core.os.Profiling; import androidx.core.os.SystemTraceRequestBuilder; @@ -16,6 +22,8 @@ public class ProfilingManagerJavaSnippets { public class MainActivityJava extends Activity { + public static final String TAG = "ProfilingManager"; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -69,5 +77,53 @@ public void accept(ProfilingResult profilingResult) { stopSignal.cancel(); } // [END android_profiling_manager_record_system_trace_java] + + // [START android_profiling_manager_triggered_trace_java] + public void recordWithTrigger() { + ProfilingManager profilingManager = getApplicationContext().getSystemService( + ProfilingManager.class); + List triggers = new ArrayList<>(); + ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder( + ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN); + triggerBuilder.setRateLimitingPeriodHours(1); + triggers.add(triggerBuilder.build()); + + Executor mainExecutor = Executors.newSingleThreadExecutor(); + Consumer resultCallback = + new Consumer() { + @Override + public void accept(ProfilingResult profilingResult) { + if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) { + Log.d( + "ProfileTest", + "Received profiling result file=" + profilingResult.getResultFilePath()); + setupProfileUploadWorker(profilingResult.getResultFilePath()); + } else { + Log.e( + "ProfileTest", + "Profiling failed errorcode=" + + profilingResult.getErrorCode() + + " errormsg=" + + profilingResult.getErrorMessage()); + } + } + }; + profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback); + profilingManager.addProfilingTriggers(triggers); + + // [START_EXCLUDE silent] + Choreographer.getInstance().postFrameCallback((f) -> { + // This will cause the TRIGGER_TYPE_APP_FULLY_DRAWN to be emitted. + reportFullyDrawn(); + }); + // [END_EXCLUDE silent] + } + // [END android_profiling_manager_triggered_trace_java] + + // [START android_profiling_manager_triggered_trace_setup_upload_job_java] + public void setupProfileUploadWorker(String resultFilePath) { + // Setup job to upload the profiling result file. + } + // [END android_profiling_manager_triggered_trace_setup_upload_job_java] } } diff --git a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt index f3f0ad99c..b54ba67b0 100644 --- a/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt +++ b/misc/src/main/java/com/example/snippets/profiling/ProfilingManagerKotlinSnippets.kt @@ -20,20 +20,29 @@ import android.app.Activity import android.os.Build import android.os.Bundle import android.os.CancellationSignal +import android.os.ProfilingManager import android.os.ProfilingResult +import android.os.ProfilingTrigger import android.util.Log +import android.view.Choreographer import androidx.annotation.RequiresApi import androidx.core.os.BufferFillPolicy import androidx.core.os.SystemTraceRequestBuilder import androidx.core.os.requestProfiling import androidx.tracing.Trace +import java.util.ArrayList import java.util.concurrent.Executor +import java.util.concurrent.Executors import java.util.function.Consumer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor class ProfilingManagerKotlinSnippets { class MainActivity : Activity() { + companion object { + const val TAG = "MyApp" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sampleRecordSystemTrace() @@ -81,5 +90,51 @@ class ProfilingManagerKotlinSnippets { // Computations you want to profile } // [END android_profiling_manager_record_system_trace_kotlin] + + // [START android_profiling_manager_triggered_trace] + fun recordWithTrigger() { + val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java) + + val triggers = ArrayList() + + val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN) + .setRateLimitingPeriodHours(1) + + triggers.add(triggerBuilder.build()) + + val mainExecutor: Executor = Executors.newSingleThreadExecutor() + + val resultCallback = Consumer { profilingResult -> + if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) { + Log.d( + "ProfileTest", + "Received profiling result file=" + profilingResult.resultFilePath + ) + setupProfileUploadWorker(profilingResult.resultFilePath) + } else { + Log.e( + "ProfileTest", + "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage + ) + } + } + + profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback) + profilingManager.addProfilingTriggers(triggers) + + // [START_EXCLUDE silent] + Choreographer.getInstance().postFrameCallback { frameTimeNanos -> + // This will cause the TRIGGER_TYPE_APP_FULLY_DRAWN to be emitted. + reportFullyDrawn() + } + // [END_EXCLUDE silent] + } + // [END android_profiling_manager_triggered_trace] + + // [START android_profiling_manager_triggered_trace_setup_upload_job] + fun setupProfileUploadWorker(resultFilePath: String?) { + // Setup job to upload the profiling result file. + } + // [END android_profiling_manager_triggered_trace_setup_upload_job] } } From 4b94445529e0636333de63bbeddfd503b4c9df94 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Fri, 21 Nov 2025 07:30:00 -0500 Subject: [PATCH 120/120] Fix typo (#701) --- wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt b/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt index 0bfdf6ec2..9a3f772a0 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Versioning.kt @@ -65,7 +65,7 @@ class LayoutM3 { deviceConfiguration: DeviceParametersBuilders.DeviceParameters, ) = materialScope(context, deviceConfiguration) { - primaryLayout(mainSlot = { text("Hello, World!".layoutString) }) // HELPME + primaryLayout(mainSlot = { text("Hello, World!".layoutString) }) } // [END android_wear_tile_version_layoutm3]