diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml
index 5ba1f6fe5..3727da63a 100644
--- a/.github/workflows/apply_spotless.yml
+++ b/.github/workflows/apply_spotless.yml
@@ -42,13 +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
+ run: ./gradlew spotlessApply --stacktrace
- name: Auto-commit if spotlessApply has changes
uses: stefanzweifel/git-auto-commit-action@v5
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/.github/workflows/build.yml b/.github/workflows/build.yml
index 97b36f468..1d3c201ca 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,18 +32,12 @@ jobs:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT || github.token }}
- - name: set up Java 17
+ - name: set up Java 25
uses: actions/setup-java@v4
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
+ java-version: '25'
+ - name: Build All
+ run: ./gradlew build --stacktrace
+ - name: Build Watch Face Push validation snippets
+ run: ./gradlew :watchfacepush:validator:run --stacktrace
diff --git a/.gitignore b/.gitignore
index 30e5f7bdc..7b190dc83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,22 @@
*.iml
.gradle
/local.properties
-/.idea/caches/build_file_checksums.ser
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
+.idea/
.DS_Store
-/build
+build
/captures
.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/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index ae78c113f..000000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/bluetoothle/src/main/AndroidManifest.xml b/bluetoothle/src/main/AndroidManifest.xml
index 1661e746b..b42865576 100644
--- a/bluetoothle/src/main/AndroidManifest.xml
+++ b/bluetoothle/src/main/AndroidManifest.xml
@@ -1,6 +1,20 @@
-
+
+
@@ -23,6 +37,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/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 6b4ec9ad0..d4ea18313 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,6 +10,96 @@ 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
+ alias(libs.plugins.spotless) apply false
+}
+
+allprojects {
+ apply(plugin = "com.diffplug.spotless")
+ extensions.configure {
+ kotlin {
+ target("**/*.kt")
+ targetExclude("**/build/**/*.kt", "spotless/**/*.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", "spotless/**/*.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",
+ "spotless/**/*.xml",
+ ".idea/**",
+ )
+ // 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/build.gradle.kts b/compose/snippets/build.gradle.kts
index 8b644b694..7a5062f65 100644
--- a/compose/snippets/build.gradle.kts
+++ b/compose/snippets/build.gradle.kts
@@ -25,7 +25,9 @@ plugins {
}
android {
- compileSdk = libs.versions.compileSdk.get().toInt()
+ compileSdk {
+ version = release(libs.versions.compileSdk.get().toInt())
+ {minorApiLevel = 1}} // Android 16 QPR 2
namespace = "com.example.compose.snippets"
defaultConfig {
@@ -160,6 +162,9 @@ 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)
+ androidTestImplementation(libs.androidx.glance.testing)
+ androidTestImplementation(libs.androidx.glance.appwidget.testing)
}
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/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/glance/GlanceUnitTest.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/glance/GlanceUnitTest.kt
new file mode 100644
index 000000000..05698d1a3
--- /dev/null
+++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/glance/GlanceUnitTest.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.compose.snippets.glance
+
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
+import androidx.glance.layout.Row
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.assertHasText
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.text.Text
+import org.junit.Test
+
+// [START android_compose_glance_unit_test]
+private const val FAKE_HEADLINE = "EXTRA! EXTRA! READ ALL ABOUT IT!"
+
+class MyGlanceComposableTest {
+ @Test
+ fun myNewsItemComposable_largeSize_hasHeadline() = runGlanceAppWidgetUnitTest {
+ // Set the composable to test
+ provideComposable {
+ MyNewsItemComposable(FAKE_HEADLINE)
+ }
+
+ // Perform assertions
+ onNode(hasTestTag("headline"))
+ .assertHasText(FAKE_HEADLINE)
+ }
+
+
+ @Composable
+ fun MyNewsItemComposable(headline: String) {
+ Row {
+ Text(
+ text = headline,
+ modifier = GlanceModifier.semantics { testTag = "headline" },
+ )
+ }
+ }
+}
+// [END android_compose_glance_unit_test]
\ 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/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/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt
index 76006e684..6221902cf 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt
@@ -17,6 +17,7 @@
package com.example.compose.snippets.adaptivelayouts
import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Favorite
@@ -28,6 +29,7 @@ import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItem
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
@@ -39,7 +41,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
-import androidx.window.core.layout.WindowWidthSizeClass
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
import com.example.compose.snippets.R
// [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_destinations]
@@ -154,12 +156,40 @@ fun SampleNavigationSuiteScaffoldColors() {
// [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_item_colors]
}
+@Composable
+fun SampleNavigationSuiteScaffoldIconsAlignment() {
+ var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) }
+
+ // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_icons_alignment]
+ NavigationSuiteScaffold(
+ navigationItems = {
+ AppDestinations.entries.forEach {
+ NavigationSuiteItem(
+ icon = {
+ Icon(
+ it.icon,
+ contentDescription = stringResource(it.contentDescription)
+ )
+ },
+ label = { Text(stringResource(it.label)) },
+ selected = it == currentDestination,
+ onClick = { currentDestination = it },
+ )
+ }
+ },
+ navigationItemVerticalArrangement = Arrangement.Center
+ ) {
+ // TODO: Destination content.
+ }
+ // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_icons_alignment]
+}
+
@Composable
fun SampleNavigationSuiteScaffoldCustomType() {
// [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_layout_type]
val adaptiveInfo = currentWindowAdaptiveInfo()
val customNavSuiteType = with(adaptiveInfo) {
- if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
+ if (windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt
index 7132c5e7f..87b6c62b7 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt
@@ -17,9 +17,15 @@
package com.example.compose.snippets.adaptivelayouts
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
@@ -30,13 +36,17 @@ import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
+import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Devices.TABLET
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -57,6 +67,12 @@ fun SampleNavigableSupportingPaneScaffoldParts() {
// [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_params]
}
+@Composable
+@Preview(device = TABLET)
+fun SampleNavigationSupportingPaneScaffoldFullTabletPreview() {
+ SampleNavigableSupportingPaneScaffoldFull()
+}
+
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
@Preview
@@ -64,6 +80,7 @@ fun SampleNavigableSupportingPaneScaffoldFull() {
// [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_full]
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()
+ val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
NavigableSupportingPaneScaffold(
navigator = scaffoldNavigator,
@@ -92,7 +109,26 @@ fun SampleNavigableSupportingPaneScaffoldFull() {
},
supportingPane = {
AnimatedPane(modifier = Modifier.safeContentPadding()) {
- Text("Supporting pane")
+ Column {
+ // Allow users to dismiss the supporting pane. Use back navigation to
+ // hide an expanded supporting pane.
+ if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
+ // Material design principles promote the usage of a right-aligned
+ // close (X) button.
+ IconButton(
+ modifier = Modifier.align(Alignment.End).padding(16.dp),
+ onClick = {
+ scope.launch {
+ scaffoldNavigator.navigateBack(backNavigationBehavior)
+ }
+ }
+ ) {
+ Icon(Icons.Default.Close, contentDescription = "Close")
+ }
+ }
+ Text("Supporting pane")
+ }
+
}
}
)
@@ -124,11 +160,32 @@ fun ThreePaneScaffoldPaneScope.MainPane(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.SupportingPane(
+ scaffoldNavigator: ThreePaneScaffoldNavigator,
modifier: Modifier = Modifier,
+ backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange,
) {
- AnimatedPane(modifier = modifier.safeContentPadding()) {
- // Supporting pane content
- Text("This is the supporting pane")
+ val scope = rememberCoroutineScope()
+ AnimatedPane(modifier = Modifier.safeContentPadding()) {
+ Column {
+ // Allow users to dismiss the supporting pane. Use back navigation to
+ // hide an expanded supporting pane.
+ if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
+ // Material design principles promote the usage of a right-aligned
+ // close (X) button.
+ IconButton(
+ modifier = modifier.align(Alignment.End).padding(16.dp),
+ onClick = {
+ scope.launch {
+ scaffoldNavigator.navigateBack(backNavigationBehavior)
+ }
+ }
+ ) {
+ Icon(Icons.Default.Close, contentDescription = "Close")
+ }
+ }
+ Text("Supporting pane")
+ }
+
}
}
// [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_extracted_panes]
@@ -152,7 +209,7 @@ fun SampleNavigableSupportingPaneScaffoldSimplified() {
}
)
},
- supportingPane = { SupportingPane() },
+ supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)
// [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified]
}
@@ -182,7 +239,7 @@ fun SampleSupportingPaneScaffoldSimplifiedWithPredictiveBackHandler() {
}
)
},
- supportingPane = { SupportingPane() },
+ supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)
// [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified_with_predictive_back_handler]
}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt
index 2fe06cf06..bdb82790d 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt
@@ -280,14 +280,15 @@ private fun AnimateAsStateSimple() {
// [START android_compose_animations_animate_as_state]
var enabled by remember { mutableStateOf(true) }
- val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
+ val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
Modifier
.fillMaxSize()
- .graphicsLayer(alpha = alpha)
+ .graphicsLayer { alpha = animatedAlpha }
.background(Color.Red)
)
// [END android_compose_animations_animate_as_state]
+ { Button(onClick = { enabled = !enabled }) { Text("Animate me!") } }
}
@Preview
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt
index 92bee9098..e07cbec6f 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt
@@ -157,7 +157,7 @@ fun SharedTransitionScope.SnackItem(
SnackContents(
snack = snack,
modifier = Modifier.sharedElement(
- state = rememberSharedContentState(key = snack.name),
+ sharedContentState = rememberSharedContentState(key = snack.name),
animatedVisibilityScope = this@AnimatedVisibility,
boundsTransform = boundsTransition,
),
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt
index 955dbe460..5a51b8d56 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt
@@ -111,7 +111,7 @@ private fun AnimatedVisibilitySharedElementShortenedExample() {
SnackContents(
snack = snack,
modifier = Modifier.sharedElement(
- state = rememberSharedContentState(key = snack.name),
+ sharedContentState = rememberSharedContentState(key = snack.name),
animatedVisibilityScope = this@AnimatedVisibility
),
onClick = {
@@ -175,7 +175,7 @@ fun SharedTransitionScope.SnackEditDetails(
SnackContents(
snack = targetSnack,
modifier = Modifier.sharedElement(
- state = rememberSharedContentState(key = targetSnack.name),
+ sharedContentState = rememberSharedContentState(key = targetSnack.name),
animatedVisibilityScope = this@AnimatedContent,
),
onClick = {
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/components/AppBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt
index 354efe635..72b4586a0 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt
@@ -218,7 +218,7 @@ fun CenterAlignedTopAppBarExample() {
topBar = {
CenterAlignedTopAppBar(
- colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
@@ -431,6 +431,7 @@ fun AppBarSelectionActions(
}
}
},
+ modifier = modifier
)
}
// [END android_compose_components_appbarselectionactions]
@@ -454,6 +455,7 @@ private fun AppBarMultiSelectionExample(
var selectedItems by rememberSaveable { mutableStateOf(setOf()) }
Scaffold(
+ modifier = modifier,
topBar = { AppBarSelectionActions(selectedItems) }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
@@ -517,7 +519,8 @@ fun LazyListMultiSelection(
) {
var selectedItems by rememberSaveable { mutableStateOf(setOf()) }
- LazyColumn(contentPadding = contentPadding) {
+ LazyColumn(modifier = modifier,
+ contentPadding = contentPadding) {
itemsIndexed(listItems) { _, index ->
val selected = selectedItems.contains(index)
ListItemSelectable(
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/DatePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt
index 692199123..4d59ea4ef 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt
@@ -57,7 +57,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
-import com.example.compose.snippets.touchinput.userinteractions.MyAppTheme
+import com.example.compose.snippets.ui.theme.SnippetsTheme
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -65,7 +65,7 @@ import java.util.Locale
@Preview
@Composable
private fun DatePickerPreview() {
- MyAppTheme {
+ SnippetsTheme {
DatePickerExamples()
}
}
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/java/com/example/compose/snippets/components/Navigation.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Navigation.kt
new file mode 100644
index 000000000..9cad68934
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Navigation.kt
@@ -0,0 +1,214 @@
+/*
+ * 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.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Album
+import androidx.compose.material.icons.filled.MusicNote
+import androidx.compose.material.icons.filled.PlaylistAddCircle
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarDefaults
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.PrimaryTabRow
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+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.graphics.vector.ImageVector
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun SongsScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Songs Screen")
+ }
+}
+
+@Composable
+fun AlbumScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Album Screen")
+ }
+}
+
+@Composable
+fun PlaylistScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Playlist Screen")
+ }
+}
+
+enum class Destination(
+ val route: String,
+ val label: String,
+ val icon: ImageVector,
+ val contentDescription: String
+) {
+ SONGS("songs", "Songs", Icons.Default.MusicNote, "Songs"),
+ ALBUM("album", "Album", Icons.Default.Album, "Album"),
+ PLAYLISTS("playlist", "Playlist", Icons.Default.PlaylistAddCircle, "Playlist")
+}
+
+@Composable
+fun AppNavHost(
+ navController: NavHostController,
+ startDestination: Destination,
+ modifier: Modifier = Modifier
+) {
+ NavHost(
+ navController,
+ startDestination = startDestination.route
+ ) {
+ Destination.entries.forEach { destination ->
+ composable(destination.route) {
+ when (destination) {
+ Destination.SONGS -> SongsScreen()
+ Destination.ALBUM -> AlbumScreen()
+ Destination.PLAYLISTS -> PlaylistScreen()
+ }
+ }
+ }
+ }
+}
+
+@Preview()
+// [START android_compose_components_navigationbarexample]
+@Composable
+fun NavigationBarExample(modifier: Modifier = Modifier) {
+ val navController = rememberNavController()
+ val startDestination = Destination.SONGS
+ var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) }
+
+ Scaffold(
+ modifier = modifier,
+ bottomBar = {
+ NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) {
+ Destination.entries.forEachIndexed { index, destination ->
+ NavigationBarItem(
+ selected = selectedDestination == index,
+ onClick = {
+ navController.navigate(route = destination.route)
+ selectedDestination = index
+ },
+ icon = {
+ Icon(
+ destination.icon,
+ contentDescription = destination.contentDescription
+ )
+ },
+ label = { Text(destination.label) }
+ )
+ }
+ }
+ }
+ ) { contentPadding ->
+ AppNavHost(navController, startDestination, modifier = Modifier.padding(contentPadding))
+ }
+}
+// [END android_compose_components_navigationbarexample]
+
+@Preview()
+// [START android_compose_components_navigationrailexample]
+@Composable
+fun NavigationRailExample(modifier: Modifier = Modifier) {
+ val navController = rememberNavController()
+ val startDestination = Destination.SONGS
+ var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) }
+
+ Scaffold(modifier = modifier) { contentPadding ->
+ NavigationRail(modifier = Modifier.padding(contentPadding)) {
+ Destination.entries.forEachIndexed { index, destination ->
+ NavigationRailItem(
+ selected = selectedDestination == index,
+ onClick = {
+ navController.navigate(route = destination.route)
+ selectedDestination = index
+ },
+ icon = {
+ Icon(
+ destination.icon,
+ contentDescription = destination.contentDescription
+ )
+ },
+ label = { Text(destination.label) }
+ )
+ }
+ }
+ AppNavHost(navController, startDestination)
+ }
+}
+// [END android_compose_components_navigationrailexample]
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview(showBackground = true)
+// [START android_compose_components_navigationtabexample]
+@Composable
+fun NavigationTabExample(modifier: Modifier = Modifier) {
+ val navController = rememberNavController()
+ val startDestination = Destination.SONGS
+ var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) }
+
+ Scaffold(modifier = modifier) { contentPadding ->
+ PrimaryTabRow(selectedTabIndex = selectedDestination, modifier = Modifier.padding(contentPadding)) {
+ Destination.entries.forEachIndexed { index, destination ->
+ Tab(
+ selected = selectedDestination == index,
+ onClick = {
+ navController.navigate(route = destination.route)
+ selectedDestination = index
+ },
+ text = {
+ Text(
+ text = destination.label,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ )
+ }
+ }
+ AppNavHost(navController, startDestination)
+ }
+}
+// [END android_compose_components_navigationtabexample]
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/components/PullToRefreshBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt
index 81280151a..5dbcb9221 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt
@@ -32,11 +32,10 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.PositionalThreshold
import androidx.compose.material3.pulltorefresh.PullToRefreshState
-import androidx.compose.material3.pulltorefresh.pullToRefreshIndicator
+import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -187,11 +186,13 @@ fun MyCustomIndicator(
modifier: Modifier = Modifier,
) {
Box(
- modifier = modifier.pullToRefreshIndicator(
+ modifier = modifier.pullToRefresh(
state = state,
isRefreshing = isRefreshing,
- containerColor = PullToRefreshDefaults.containerColor,
- threshold = PositionalThreshold
+ threshold = PositionalThreshold,
+ onRefresh = {
+
+ }
),
contentAlignment = Alignment.Center
) {
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) }
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 183c19e16..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
) {
- // [START_EXCLUDE]
- val swipeToDismissState = rememberSwipeToDismissBoxState(
- positionalThreshold = { totalDistance -> totalDistance * 0.25f },
+ 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()
+ )
}
}
}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt
index 1066d0cfb..9a034e261 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt
@@ -166,7 +166,6 @@ fun AdvancedRichTooltipExample(
}
}
},
- caretSize = DpSize(32.dp, 16.dp)
) {
Text(richTooltipText)
}
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 46e245a3f..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
@@ -20,11 +20,11 @@ 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
import androidx.compose.foundation.draganddrop.dragAndDropTarget
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -32,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)
@@ -40,40 +41,24 @@ private fun DragAndDropSnippet() {
val url = ""
- // [START android_compose_drag_and_drop_1]
- Modifier.dragAndDropSource {
- detectTapGestures(onLongPress = {
- // Transfer data here.
- })
- }
- // [END android_compose_drag_and_drop_1]
-
// [START android_compose_drag_and_drop_2]
- Modifier.dragAndDropSource {
- detectTapGestures(onLongPress = {
- startTransfer(
- DragAndDropTransferData(
- ClipData.newPlainText(
- "image Url", url
- )
- )
+ Modifier.dragAndDropSource { _ ->
+ DragAndDropTransferData(
+ ClipData.newPlainText(
+ "image Url", url
)
- })
+ )
}
// [END android_compose_drag_and_drop_2]
// [START android_compose_drag_and_drop_3]
- Modifier.dragAndDropSource {
- detectTapGestures(onLongPress = {
- startTransfer(
- DragAndDropTransferData(
- ClipData.newPlainText(
- "image Url", url
- ),
- flags = View.DRAG_FLAG_GLOBAL
- )
- )
- })
+ Modifier.dragAndDropSource { _ ->
+ DragAndDropTransferData(
+ ClipData.newPlainText(
+ "image Url", url
+ ),
+ flags = View.DRAG_FLAG_GLOBAL
+ )
}
// [END android_compose_drag_and_drop_3]
@@ -88,11 +73,27 @@ 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 ->
event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
- }, target = callback
+ }, target = callback // or externalAppCallback
)
// [END android_compose_drag_and_drop_5]
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt
new file mode 100644
index 000000000..3541cceb4
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+private const val TAG = "WidgetMetrics"
+
+class GlanceMetrics {
+
+ // [START android_compose_glance_metrics]
+ @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
+ fun getWidgetEngagementMetrics(context: Context) {
+ val manager = AppWidgetManager.getInstance(context)
+
+ val endTime = System.currentTimeMillis()
+ val startTime = endTime - (24 * 60 * 60 * 1000) // a day ago
+
+ val events = manager.queryAppWidgetEvents(startTime, endTime)
+
+ if (events.isEmpty()) {
+ Log.d(TAG, "No events found for the given time range.")
+ }
+
+ val metrics = hashMapOf(
+ "clicks" to 0L,
+ "scrolls" to 0L,
+ "totalImpressionLength" to 0L
+ )
+
+ for (event in events) {
+
+ Log.d(TAG, "Event Start: ${event.start}")
+ Log.d(TAG, "Event End: ${event.end}")
+
+ val widgetId = event.appWidgetId
+
+ // Tap actions
+ val clickedIds = event.clickedIds
+ if (clickedIds?.isNotEmpty() == true) {
+ metrics["clicks"] = metrics.getValue("clicks") + clickedIds.size
+ // Log or analyze which components were clicked.
+ for (id in clickedIds) {
+ Log.d(TAG, "Widget $widgetId: Tap event on component with ID $id")
+ }
+ }
+
+ // Scroll events
+ val scrolledIds = event.scrolledIds
+ if (scrolledIds?.isNotEmpty() == true) {
+ metrics["scrolls"] = metrics.getValue("scrolls") + scrolledIds.size
+ // Log or analyze which lists were scrolled.
+ for (id in scrolledIds) {
+ Log.d(TAG, "Widget $widgetId: Scroll event in list with ID/tag $id")
+ }
+ }
+
+ // Impressions
+ metrics["totalImpressionLength"] = metrics.getValue("totalImpressionLength") + event.visibleDuration.toMillis()
+ Log.d(
+ TAG,
+ "Widget $widgetId: Impression event with duration " + event.visibleDuration.toMillis() + "ms"
+ )
+
+ // Position
+ val position = event.position
+ if (position != null) {
+ Log.d(
+ TAG,
+ "Widget $widgetId: left=${position.left}, right=${position.right}, top=${position.top}, bottom=${position.bottom}"
+ )
+ }
+ }
+ Log.d("WidgetMetrics", "Metrics: $metrics")
+ }
+ // [END android_compose_glance_metrics]
+}
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]
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..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
@@ -225,7 +226,7 @@ private object ActionLaunchSendBroadcastEvent {
private object ActionLambda {
@Composable
- fun actionLambda() {
+ fun ActionLambda() {
// [START android_compose_glance_lambda01]
Text(
text = "Submit",
@@ -237,7 +238,7 @@ private object ActionLambda {
}
@Composable
- fun actionLambda2() {
+ fun ActionLambda2() {
// [START android_compose_glance_lambda02]
Button(
text = "Submit",
@@ -436,7 +437,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 +449,7 @@ object BuildUIWithGlance {
}
@Composable
- fun example2() {
+ fun Example2() {
// [START android_compose_glance_buildUI02]
// Remember to import Glance Composables
@@ -466,7 +467,7 @@ object BuildUIWithGlance {
}
@Composable
- fun example3() {
+ fun Example3() {
// [START android_compose_glance_buildUI03]
LazyColumn {
item {
@@ -480,7 +481,7 @@ object BuildUIWithGlance {
}
@Composable
- fun example4() {
+ fun Example4() {
val peopleNameList = arrayListOf()
val peopleList = arrayListOf()
@@ -631,7 +632,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,11 +652,11 @@ 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) }
- var isRadioChecked by remember { mutableStateOf(0) }
+ var isRadioChecked by remember { mutableIntStateOf(0) }
CheckBox(
checked = isApplesChecked,
@@ -860,7 +861,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 +899,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 +910,7 @@ object GlanceInteroperability {
}
@Composable
- fun example02() {
+ fun Example02() {
val packageName = null
// [START android_compose_glance_glanceinteroperability02]
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]
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..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,13 +32,14 @@ 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
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
@@ -191,10 +192,32 @@ 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() {
- var selectedItem by remember { mutableStateOf(0) }
+ var selectedItem by remember { mutableIntStateOf(0) }
// Adds view to Compose
AndroidView(
@@ -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]
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt
index 59ac0129d..fec60d697 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt
@@ -58,7 +58,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
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)
+ }
}
}
}
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..8c682a601 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
@@ -21,9 +21,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.ContextualFlowRow
-import androidx.compose.foundation.layout.ContextualFlowRowOverflow
-import androidx.compose.foundation.layout.ContextualFlowRowOverflowScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowColumn
import androidx.compose.foundation.layout.FlowRow
@@ -33,22 +30,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
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.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.graphics.Color
@@ -465,48 +453,6 @@ fun FlowLayout_FractionalSizing() {
// [END android_compose_flow_layout_fractional_sizing]
}
-@OptIn(ExperimentalLayoutApi::class)
-@Preview
-@Composable
-fun ContextualFlowLayoutExample() {
- // [START android_compose_layouts_contextual_flow]
- val totalCount = 40
- var maxLines by remember {
- mutableStateOf(2)
- }
-
- val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope ->
- val remainingItems = totalCount - scope.shownItemCount
- ChipItem(if (remainingItems == 0) "Less" else "+$remainingItems", onClick = {
- if (remainingItems == 0) {
- maxLines = 2
- } else {
- maxLines += 5
- }
- })
- }
- ContextualFlowRow(
- modifier = Modifier
- .safeDrawingPadding()
- .fillMaxWidth(1f)
- .padding(16.dp)
- .wrapContentHeight(align = Alignment.Top)
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- maxLines = maxLines,
- overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator(
- minRowsToShowCollapse = 4,
- expandIndicator = moreOrCollapseIndicator,
- collapseIndicator = moreOrCollapseIndicator
- ),
- itemCount = totalCount
- ) { index ->
- ChipItem("Item $index")
- }
- // [END android_compose_layouts_contextual_flow]
-}
-
@OptIn(ExperimentalLayoutApi::class)
@Preview
@Composable
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..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
@@ -28,9 +28,14 @@ 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.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
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
@@ -44,10 +49,19 @@ 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
+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
import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
class InsetSnippetActivity : ComponentActivity() {
@@ -147,3 +161,105 @@ 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]
+
+// [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]
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt
index 899163367..e09fe456d 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt
@@ -50,8 +50,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -239,7 +239,7 @@ fun PagerWithTabsExample() {
pages.size
})
- TabRow(
+ PrimaryTabRow(
// Our selected tab is our current page
selectedTabIndex = pagerState.currentPage,
) {
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..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
@@ -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
@@ -141,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() {
@@ -158,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)
@@ -180,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
@@ -259,7 +263,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 +271,7 @@ object CustomModifierSnippets14 {
}
override fun onAttach() {
+ alpha = Animatable(1f)
coroutineScope.launch {
alpha.animateTo(
0f,
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt
new file mode 100644
index 000000000..dbad0cc1b
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.modifiers
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Card
+import androidx.compose.material3.Text
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onFirstVisible
+import androidx.compose.ui.layout.onVisibilityChanged
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+
+private object OnVisibilityChangedSample {
+
+ @Composable
+ fun OnVisibilityChangedExample(modifier: Modifier = Modifier) {
+ // [START android_compose_modifiers_onVisibilityChanged]
+ Text(
+ text = "Some text",
+ modifier = Modifier
+ .onVisibilityChanged { visible ->
+ if (visible) {
+ // Do something if visible
+ } else {
+ // Do something if not visible
+ }
+ }
+ .padding(vertical = 8.dp)
+ )
+ // [END android_compose_modifiers_onVisibilityChanged]
+ }
+}
+
+private object OnVisibilityChangedMinFractionVisible {
+
+ @Composable
+ fun OnVisibilityChangedModifierMinFractionExample(modifier: Modifier = Modifier) {
+ // [START android_compose_modifiers_onVisibilityChangedMinFraction]
+ LazyColumn(
+ modifier = modifier.fillMaxSize()
+ ) {
+ item {
+ Box(
+ modifier = Modifier
+ // [START_EXCLUDE]
+ .fillParentMaxWidth()
+ .border(0.5.dp, Color.Gray)
+ // [END_EXCLUDE]
+ // Here the visible callback gets triggered when 20% of the composable is visible
+ .onVisibilityChanged(
+ minFractionVisible = 0.2f,
+ ) { visible ->
+ if (visible) {
+ // Call specific logic here
+ // viewModel.fetchDataFromNetwork()
+ }
+ }
+ .padding(vertical = 16.dp)
+ ) {
+ Text(
+ text = "Sample Text",
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ // [END android_compose_modifiers_onVisibilityChangedMinFraction]
+ }
+}
+
+private object onVisibilityChangedMinDuration {
+
+ val MutedPlum = Color(0xFF7B4B6B)
+ val PalePink = Color(0xFFF3E9EB)
+
+ @Composable
+ fun OnVisibilityChangedMinDurationExample(
+ @DrawableRes imageRes: Int,
+ modifier: Modifier = Modifier,
+ ) {
+ // [START android_compose_modifiers_onVisibilityChangedMinDuration]
+ var background by remember { mutableStateOf(PalePink) }
+ Card(
+ modifier = modifier
+ // [START_EXCLUDE]
+ .height(300.dp)
+ .fillMaxWidth()
+ // [END_EXCLUDE]
+ .onVisibilityChanged(minDurationMs = 3000) {
+ if (it) {
+ background = MutedPlum
+ }
+ }
+ ) {
+
+ Box(
+ modifier = Modifier
+ // [START_EXCLUDE]
+ .fillMaxSize()
+ // [END_EXCLUDE]
+ .background(background),
+ contentAlignment = Alignment.Center,
+ ) {
+ // [START_EXCLUDE]
+ Image(
+ painter = painterResource(id = imageRes),
+ contentDescription = "Androidify Bot",
+ )
+ // [END_EXCLUDE]
+ }
+ }
+ // [END android_compose_modifiers_onVisibilityChangedMinDuration]
+ }
+}
+
+
+private object OnFirstVisibleSample {
+
+ @Composable
+ fun OnFirstVisibleExample(id: Int, modifier: Modifier = Modifier) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ // [START android_compose_modifiers_onFirstVisible]
+ Text(
+ modifier = Modifier
+ .fillMaxSize()
+ .onFirstVisible {
+ println("OnFirstVisible : ProductCard: $id is visible")
+ }
+ .padding(8.dp),
+ text = "Product $id"
+ )
+ // [END android_compose_modifiers_onFirstVisible]
+ }
+ }
+}
\ No newline at end of file
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/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())
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]
}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt
index 0a2f34a56..f964e352d 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -109,7 +110,7 @@ fun Colors() {
// #FFBB86FC
// In your Compose code
- Divider(color = colorResource(R.color.purple_200))
+ HorizontalDivider(color = colorResource(R.color.purple_200))
// [END android_compose_resources_colors]
}
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/state/StateHoistingSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt
index 2a4f0c949..2e607fde9 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt
@@ -18,6 +18,8 @@
package com.example.compose.snippets.state
+import android.R.id.message
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@@ -34,7 +36,13 @@ import androidx.compose.runtime.rememberCoroutineScope
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.LinkAnnotation
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withLink
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
@@ -60,9 +68,11 @@ private object StateHoistingSnippets1 {
) {
var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state
- ClickableText(
+ Text(
text = AnnotatedString(message.content),
- onClick = { showDetails = !showDetails } // Apply simple UI logic
+ modifier = Modifier.clickable {
+ showDetails = !showDetails // Apply UI logic
+ }
)
if (showDetails) {
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]
+}
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..e4e901bd8
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.layout.padding
+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
+import androidx.compose.material.OutlinedTextField
+//noinspection UsingMaterialAndMaterial3Libraries
+import androidx.compose.material.TextField
+//noinspection UsingMaterialAndMaterial3Libraries
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.text.isDigitsOnly
+import androidx.lifecycle.ViewModel
+
+@Preview
+@Composable
+fun StateBasedTextSnippets() {
+ Column() {
+ // [START android_compose_state_text_0]
+ TextField(
+ state = rememberTextFieldState(initialText = "Hello"),
+ label = { Text("Label") }
+ )
+ // [END android_compose_state_text_0]
+
+ // [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("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]
+}
+
+@Composable
+fun ConfigureLineLimits() {
+ // [START android_compose_state_text_3]
+ TextField(
+ state = rememberTextFieldState(),
+ 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("Hello\nWorld\nHello\nWorld"),
+ 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]
+}
+
+@Preview(showBackground = true)
+@Composable
+fun TextFieldBuffer() {
+ // [START android_compose_state_text_8]
+ val phoneNumberState = rememberTextFieldState("1234567890")
+
+ TextField(
+ state = phoneNumberState,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Phone
+ ),
+ inputTransformation = InputTransformation.maxLength(10).then {
+ 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() {
+ 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)
+ textFieldState.edit { insert(14, "!") }
+ // textFieldState.text : I love Android!
+ // textFieldState.selection: TextRange(15, 15)
+ textFieldState.edit { replace(7, 14, "Compose") }
+ // textFieldState.text : I love Compose!
+ // textFieldState.selection: TextRange(15, 15)
+ textFieldState.edit { append("!!!") }
+ // textFieldState.text : I love Compose!!!!
+ // textFieldState.selection: TextRange(18, 18)
+ 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]
+ 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]
+ textFieldState.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 (!asCharSequence().isDigitsOnly()) {
+ 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]
+}
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]
+}
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/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
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt
index 07248a6f9..6b9fa3fda 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt
@@ -30,13 +30,13 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalRippleConfiguration
-import androidx.compose.material.LocalUseFallbackRippleImplementation
import androidx.compose.material.RippleConfiguration
import androidx.compose.material.ripple
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -239,28 +239,6 @@ private class ScaleIndicationNode(
fun App() {
}
-@OptIn(ExperimentalMaterialApi::class)
-@Composable
-private fun LocalUseFallbackRippleImplementationExample() {
-// [START android_compose_userinteractions_localusefallbackrippleimplementation]
- CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
- MaterialTheme {
- App()
- }
- }
-// [END android_compose_userinteractions_localusefallbackrippleimplementation]
-}
-
-// [START android_compose_userinteractions_localusefallbackrippleimplementation_app_theme]
-@OptIn(ExperimentalMaterialApi::class)
-@Composable
-fun MyAppTheme(content: @Composable () -> Unit) {
- CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
- MaterialTheme(content = content)
- }
-}
-// [END android_compose_userinteractions_localusefallbackrippleimplementation_app_theme]
-
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun MyComposableDisabledRippleConfig() {
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
new file mode 100644
index 000000000..95cdea9c4
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/fast_forward.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
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..84a42ad80
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/fast_forward_filled.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
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..4564c5ba7
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/fast_rewind.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
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..f6f8cebb1
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
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..e6859af3e
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/favorite.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
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..d49f76c61
--- /dev/null
+++ b/compose/snippets/src/main/res/drawable/favorite_filled.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml b/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml
index ad4c4a39f..c30879c0e 100644
--- a/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml
+++ b/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml
@@ -1,18 +1,18 @@
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.
+-->
-
\ No newline at end of file
+
diff --git a/compose/snippets/src/main/res/xml/my_app_widget_info.xml b/compose/snippets/src/main/res/xml/my_app_widget_info.xml
index a1a7f5da2..75a4b70f6 100644
--- a/compose/snippets/src/main/res/xml/my_app_widget_info.xml
+++ b/compose/snippets/src/main/res/xml/my_app_widget_info.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/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
diff --git a/gradle/init.gradle.kts b/gradle/init.gradle.kts
deleted file mode 100644
index 8a852469a..000000000
--- a/gradle/init.gradle.kts
+++ /dev/null
@@ -1,52 +0,0 @@
-val ktlintVersion = "0.43.0"
-
-initscript {
- val spotlessVersion = "6.11.0"
-
- repositories {
- mavenCentral()
- }
-
- dependencies {
- classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
- }
-}
-
-rootProject {
- subprojects {
- apply()
- extensions.configure {
- kotlin {
- target("**/*.kt")
- targetExclude("**/build/**/*.kt")
- ktlint(ktlintVersion).userData(
- mapOf(
- "android" to "true",
- "ktlint_code_style" to "android",
- "ij_kotlin_allow_trailing_comma" to "true",
- // 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
- "disabled_rules" to
- "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"
- )
- )
- licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5482e9f16..2fc556ece 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,74 +1,99 @@
[versions]
accompanist = "0.36.0"
-androidGradlePlugin = "8.8.1"
-androidx-activity-compose = "1.10.0"
+activityKtx = "1.11.0"
+android-googleid = "1.1.1"
+androidGradlePlugin = "8.13.0"
+androidx-activity-compose = "1.11.0"
androidx-appcompat = "1.7.0"
-androidx-compose-bom = "2025.02.00"
+androidx-compose-bom = "2025.10.00"
androidx-compose-ui-test = "1.7.0-alpha08"
+androidx-compose-ui-test-junit4-accessibility = "1.10.0-alpha05"
androidx-constraintlayout = "2.2.1"
-androidx-constraintlayout-compose = "1.1.0"
-androidx-coordinator-layout = "1.2.0"
-androidx-corektx = "1.16.0-beta01"
-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-constraintlayout-compose = "1.1.1"
+androidx-coordinator-layout = "1.3.0"
+androidx-corektx = "1.17.0"
+androidx-credentials = "1.6.0-beta03"
+androidx-credentials-play-services-auth = "1.6.0-beta03"
+androidx-emoji2-views = "1.6.0"
+androidx-fragment-ktx = "1.8.9"
androidx-glance-appwidget = "1.1.1"
-androidx-lifecycle-compose = "2.8.7"
-androidx-lifecycle-runtime-compose = "2.8.7"
-androidx-navigation = "2.8.7"
+androidx-lifecycle-compose = "2.9.4"
+androidx-lifecycle-runtime-compose = "2.9.4"
+androidx-navigation = "2.9.5"
androidx-paging = "3.3.6"
androidx-startup-runtime = "1.2.0"
-androidx-test = "1.6.1"
-androidx-test-espresso = "3.6.1"
-androidx-window = "1.4.0-rc01"
-androidx-window-core = "1.4.0-beta02"
-androidx-window-java = "1.3.0"
-androidxHiltNavigationCompose = "1.2.0"
-appcompat = "1.7.0"
+androidx-test = "1.7.0"
+androidx-test-espresso = "3.7.0"
+androidx-test-junit = "1.3.0"
+androidx-window = "1.5.0"
+androidx-window-core = "1.5.0"
+androidx-window-java = "1.5.0"
+androidx-xr-arcore = "1.0.0-alpha07"
+androidx-xr-compose = "1.0.0-alpha08"
+androidx-xr-scenecore = "1.0.0-alpha08"
+androidxHiltNavigationCompose = "1.3.0"
+appcompat = "1.7.1"
coil = "2.7.0"
# @keep
-compileSdk = "35"
-compose-latest = "1.7.8"
-composeUiTooling = "1.4.1"
+compileSdk = "36"
+compose-latest = "1.9.3"
+composeUiTooling = "1.5.3"
coreSplashscreen = "1.0.1"
-coroutines = "1.10.1"
-glide = "1.0.0-beta01"
-google-maps = "19.0.0"
-gradle-versions = "0.52.0"
-guava = "33.4.0-jre"
-hilt = "2.55"
-horologist = "0.6.22"
+coroutines = "1.10.2"
+firebase-bom = "34.4.0"
+glide = "1.0.0-beta08"
+google-maps = "19.2.0"
+gradle-versions = "0.53.0"
+guava = "33.5.0-jre"
+guava-android = "31.0.1-android"
+reactive-streams = "1.0.4"
+hilt = "2.57.2"
+horologist = "0.8.2-alpha"
junit = "4.13.2"
-kotlin = "2.1.10"
-kotlinxSerializationJson = "1.8.0"
-ksp = "2.1.10-1.0.30"
-maps-compose = "6.4.4"
-material = "1.13.0-alpha10"
+kotlin = "2.2.20"
+kotlinCoroutinesOkhttp = "1.0"
+kotlinxCoroutinesGuava = "1.10.2"
+kotlinxSerializationJson = "1.9.0"
+ksp = "2.2.20-2.0.4"
+ktlint = "1.5.0"
+lifecycleService = "2.9.4"
+maps-compose = "6.12.1"
+material = "1.14.0-alpha05"
material3-adaptive = "1.1.0"
-material3-adaptive-navigation-suite = "1.3.1"
-media3 = "1.5.1"
+material3-adaptive-navigation-suite = "1.4.0"
+media3 = "1.8.0"
+media3Ui = "1.8.0"
# @keep
-minSdk = "21"
+minSdk = "35"
+okHttp = "5.2.0"
playServicesWearable = "19.0.0"
-protolayout = "1.2.1"
+protolayout = "1.3.0"
recyclerview = "1.4.0"
-# @keep
-targetSdk = "34"
-tiles = "1.4.1"
-version-catalog-update = "0.8.5"
+spotless = "8.0.0"
+targetSdk = "36"
+tiles = "1.5.0"
+tracing = "1.3.0"
+validatorPush = "1.0.0-alpha08"
+version-catalog-update = "1.0.1"
+watchfaceComplicationsDataSourceKtx = "1.2.1"
wear = "1.3.0"
-wearComposeFoundation = "1.4.1"
-wearComposeMaterial = "1.4.1"
+wearComposeFoundation = "1.5.3"
+wearComposeMaterial = "1.5.3"
+wearComposeMaterial3 = "1.5.3"
+wearOngoing = "1.1.0"
wearToolingPreview = "1.0.0"
+webkit = "1.14.0"
+wearInput = "1.2.0"
[libraries]
-accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
-accompanist-permissions = "com.google.accompanist:accompanist-permissions:0.37.0"
+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" }
+android-identity-googleid = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "android-googleid" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
+androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose-latest" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
@@ -87,14 +112,15 @@ 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 = { 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" }
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" }
-androidx-constraintlayout = {module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
+androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "androidx-constraintlayout-compose" }
androidx-coordinator-layout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "androidx-coordinator-layout" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-corektx" }
@@ -104,66 +130,96 @@ androidx-credentials-play-services-auth = { module = "androidx.credentials:crede
androidx-emoji2-views = { module = "androidx.emoji2:emoji2-views", version.ref = "androidx-emoji2-views" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment-ktx" }
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "androidx-glance-appwidget" }
+androidx-glance-appwidget-testing = { module = "androidx.glance:glance-appwidget-testing", version.ref = "androidx-glance-appwidget" }
androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "androidx-glance-appwidget" }
+androidx-glance-testing = { module = "androidx.glance:glance-testing", version.ref = "androidx-glance-appwidget" }
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 = { 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" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
+androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
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-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup-runtime" }
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-runner = "androidx.test:runner:1.6.2"
+androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
+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 = { 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" }
+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.0"
+androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window-java" }
+androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.5"
+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" }
-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" }
compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "composeUiTooling" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
+firebase-ai = { module = "com.google.firebase:firebase-ai" }
glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }
google-android-material = { module = "com.google.android.material:material", version.ref = "material" }
googlemaps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" }
googlemaps-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "google-maps" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
+guava-android = { module = "com.google.guava:guava", version.ref = "guava-android" }
+reactive-streams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactive-streams" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" }
horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" }
+jetbrains-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
+kotlin-coroutines-okhttp = { module = "ru.gildor.coroutines:kotlin-coroutines-okhttp", version.ref = "kotlinCoroutinesOkhttp" }
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", 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" }
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" }
+androidx-wear-input = { group = "androidx.wear", name = "wear-input", version.ref = "wearInput" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" }
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 = "org.jetbrains.kotlin.android:2.2.20"
+kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
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" }
+spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df97d72b8..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.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
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/identity/credentialmanager/build.gradle.kts b/identity/credentialmanager/build.gradle.kts
index fab41549b..2e35608a7 100644
--- a/identity/credentialmanager/build.gradle.kts
+++ b/identity/credentialmanager/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.application)
+ // [START android_identity_fido2_migration_dependency]
alias(libs.plugins.kotlin.android)
+ // [END android_identity_fido2_migration_dependency]
alias(libs.plugins.compose.compiler)
}
@@ -37,6 +39,13 @@ android {
buildFeatures {
compose = true
}
+ sourceSets {
+ named("main") {
+ java {
+ srcDir("src/main/java")
+ }
+ }
+ }
}
dependencies {
@@ -48,6 +57,11 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
+
+ // [START android_identity_credman_dependency]
+ implementation(libs.androidx.credentials)
+ // [END android_identity_credman_dependency]
+
// [START android_identity_gradle_dependencies]
implementation(libs.androidx.credentials)
@@ -55,6 +69,15 @@ dependencies {
// Android 13 and below.
implementation(libs.androidx.credentials.play.services.auth)
// [END android_identity_gradle_dependencies]
+ // [START android_identity_siwg_gradle_dependencies]
+ implementation(libs.androidx.credentials)
+ implementation(libs.androidx.credentials.play.services.auth)
+ implementation(libs.android.identity.googleid)
+ // [END android_identity_siwg_gradle_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)
-}
\ No newline at end of file
+}
diff --git a/identity/credentialmanager/src/main/AndroidManifest.xml b/identity/credentialmanager/src/main/AndroidManifest.xml
index 09a3c8397..b0f129562 100644
--- a/identity/credentialmanager/src/main/AndroidManifest.xml
+++ b/identity/credentialmanager/src/main/AndroidManifest.xml
@@ -1,5 +1,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
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
new file mode 100644
index 000000000..381bc8fc3
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialManagerHandler.kt
@@ -0,0 +1,67 @@
+/*
+ * 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
+import android.util.Log
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
+import androidx.credentials.CredentialManager
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.exceptions.CreateCredentialException
+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
+ }
+ }
+
+ /**
+ * 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
new file mode 100644
index 000000000..ed65bd831
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/CredentialProviderDummyActivity.kt
@@ -0,0 +1,506 @@
+/*
+ * 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.app.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.os.PersistableBundle
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AuthenticationCallback
+import androidx.biometric.BiometricPrompt.AuthenticationResult
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePasswordResponse
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.provider.BeginCreateCredentialRequest
+import androidx.credentials.provider.BeginCreateCredentialResponse
+import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
+import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
+import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.PendingIntentHandler
+import androidx.credentials.webauthn.AuthenticatorAssertionResponse
+import androidx.credentials.webauthn.AuthenticatorAttestationResponse
+import androidx.credentials.webauthn.FidoPublicKeyCredential
+import androidx.credentials.webauthn.PublicKeyCredentialCreationOptions
+import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions
+import androidx.fragment.app.FragmentActivity
+import java.math.BigInteger
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.MessageDigest
+import java.security.SecureRandom
+import java.security.Signature
+import java.security.interfaces.ECPrivateKey
+import java.security.spec.ECGenParameterSpec
+import java.security.spec.ECParameterSpec
+import java.security.spec.ECPoint
+import java.security.spec.EllipticCurve
+
+class CredentialProviderDummyActivity : FragmentActivity() {
+
+ private val PERSONAL_ACCOUNT_ID: String = ""
+ private val FAMILY_ACCOUNT_ID: String = ""
+ private val CREATE_PASSWORD_INTENT: String = ""
+
+ @RequiresApi(VERSION_CODES.M)
+ // [START android_identity_credential_provider_handle_passkey]
+ override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
+ super.onCreate(savedInstanceState, persistentState)
+ // ...
+
+ val request =
+ PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+ val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
+ if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
+ val publicKeyRequest: CreatePublicKeyCredentialRequest =
+ request.callingRequest as CreatePublicKeyCredentialRequest
+ createPasskey(
+ publicKeyRequest.requestJson,
+ request.callingAppInfo,
+ publicKeyRequest.clientDataHash,
+ accountId
+ )
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ fun createPasskey(
+ requestJson: String,
+ callingAppInfo: CallingAppInfo?,
+ clientDataHash: ByteArray?,
+ accountId: String?
+ ) {
+ val request = PublicKeyCredentialCreationOptions(requestJson)
+
+ val biometricPrompt = BiometricPrompt(
+ this,
+ { }, // Pass in your own executor
+ object : AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ finish()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ finish()
+ }
+
+ @RequiresApi(VERSION_CODES.P)
+ override fun onAuthenticationSucceeded(
+ result: AuthenticationResult
+ ) {
+ super.onAuthenticationSucceeded(result)
+
+ // Generate a credentialId
+ val credentialId = ByteArray(32)
+ SecureRandom().nextBytes(credentialId)
+
+ // Generate a credential key pair
+ val spec = ECGenParameterSpec("secp256r1")
+ val keyPairGen = KeyPairGenerator.getInstance("EC")
+ keyPairGen.initialize(spec)
+ val keyPair = keyPairGen.genKeyPair()
+
+ // Save passkey in your database as per your own implementation
+
+ // Create AuthenticatorAttestationResponse object to pass to
+ // FidoPublicKeyCredential
+
+ val response = AuthenticatorAttestationResponse(
+ requestOptions = request,
+ credentialId = credentialId,
+ credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
+ origin = appInfoToOrigin(callingAppInfo!!),
+ up = true,
+ uv = true,
+ be = true,
+ bs = true,
+ packageName = callingAppInfo.packageName
+ )
+
+ val credential = FidoPublicKeyCredential(
+ rawId = credentialId,
+ response = response,
+ authenticatorAttachment = "", // Add your authenticator attachment
+ )
+ val result = Intent()
+
+ val createPublicKeyCredResponse =
+ CreatePublicKeyCredentialResponse(credential.json())
+
+ // Set the CreateCredentialResponse as the result of the Activity
+ PendingIntentHandler.setCreateCredentialResponse(
+ result,
+ createPublicKeyCredResponse
+ )
+ setResult(RESULT_OK, result)
+ finish()
+ }
+ }
+ )
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle("Use your screen lock")
+ .setSubtitle("Create passkey for ${request.rp.name}")
+ .setAllowedAuthenticators(
+ BiometricManager.Authenticators.BIOMETRIC_STRONG
+ /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
+ )
+ .build()
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+ @RequiresApi(VERSION_CODES.P)
+ fun appInfoToOrigin(info: CallingAppInfo): String {
+ val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
+ val md = MessageDigest.getInstance("SHA-256")
+ val certHash = md.digest(cert)
+ // This is the format for origin
+ return "android:apk-key-hash:${b64Encode(certHash)}"
+ }
+ // [END android_identity_credential_provider_handle_passkey]
+
+ @RequiresApi(VERSION_CODES.M)
+ // [START android_identity_credential_provider_password_creation]
+ fun processCreateCredentialRequest(
+ request: BeginCreateCredentialRequest
+ ): BeginCreateCredentialResponse? {
+ when (request) {
+ is BeginCreatePublicKeyCredentialRequest -> {
+ // Request is passkey type
+ return handleCreatePasskeyQuery(request)
+ }
+
+ is BeginCreatePasswordCredentialRequest -> {
+ // Request is password type
+ return handleCreatePasswordQuery(request)
+ }
+ }
+ return null
+ }
+
+ @RequiresApi(VERSION_CODES.M)
+ private fun handleCreatePasswordQuery(
+ request: BeginCreatePasswordCredentialRequest
+ ): BeginCreateCredentialResponse {
+ val createEntries: MutableList = mutableListOf()
+
+ // Adding two create entries - one for storing credentials to the 'Personal'
+ // account, and one for storing them to the 'Family' account. These
+ // accounts are local to this sample app only.
+ createEntries.add(
+ CreateEntry(
+ PERSONAL_ACCOUNT_ID,
+ createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
+ )
+ )
+ createEntries.add(
+ CreateEntry(
+ FAMILY_ACCOUNT_ID,
+ createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
+ )
+ )
+
+ return BeginCreateCredentialResponse(createEntries)
+ }
+ // [END android_identity_credential_provider_password_creation]
+
+ @RequiresApi(VERSION_CODES.M)
+ fun handleEntrySelectionForPasswordCreation(
+ mDatabase: MyDatabase
+ ) {
+ // [START android_identity_credential_provider_entry_selection_password_creation]
+ val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+ val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
+
+ if (createRequest == null) {
+ return
+ }
+
+ val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest
+
+ // Fetch the ID and password from the request and save it in your database
+ mDatabase.addNewPassword(
+ PasswordInfo(
+ request.id,
+ request.password,
+ createRequest.callingAppInfo.packageName
+ )
+ )
+
+ // Set the final response back
+ val result = Intent()
+ val response = CreatePasswordResponse()
+ PendingIntentHandler.setCreateCredentialResponse(result, response)
+ setResult(Activity.RESULT_OK, result)
+ finish()
+ // [END android_identity_credential_provider_entry_selection_password_creation]
+ }
+
+ @RequiresApi(VERSION_CODES.P)
+ private fun handleUserSelectionForPasskeys(
+ mDatabase: MyDatabase
+ ) {
+ // [START android_identity_credential_provider_user_pk_selection]
+ val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+ val publicKeyRequest = getRequest?.credentialOptions?.first() as GetPublicKeyCredentialOption
+
+ val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
+ val credIdEnc = requestInfo?.getString("credId").orEmpty()
+
+ // Get the saved passkey from your database based on the credential ID from the PublicKeyRequest
+ val passkey = mDatabase.getPasskey(credIdEnc)
+
+ // Decode the credential ID, private key and user ID
+ val credId = b64Decode(credIdEnc)
+ val privateKey = b64Decode(passkey.credPrivateKey)
+ val uid = b64Decode(passkey.uid)
+
+ val origin = appInfoToOrigin(getRequest.callingAppInfo)
+ val packageName = getRequest.callingAppInfo.packageName
+
+ validatePasskey(
+ publicKeyRequest.requestJson,
+ origin,
+ packageName,
+ uid,
+ passkey.username,
+ credId,
+ privateKey
+ )
+ // [END android_identity_credential_provider_user_pk_selection]
+ }
+
+ @SuppressLint("RestrictedApi")
+ @RequiresApi(VERSION_CODES.M)
+ private fun validatePasskey(
+ requestJson: String,
+ origin: String,
+ packageName: String,
+ uid: ByteArray,
+ username: String,
+ credId: ByteArray,
+ privateKeyBytes: ByteArray,
+ ) {
+ // [START android_identity_credential_provider_user_validation_biometric]
+ val request = PublicKeyCredentialRequestOptions(requestJson)
+ val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)
+
+ val biometricPrompt = BiometricPrompt(
+ this,
+ { }, // Pass in your own executor
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(
+ errorCode: Int,
+ errString: CharSequence
+ ) {
+ super.onAuthenticationError(errorCode, errString)
+ finish()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ finish()
+ }
+
+ override fun onAuthenticationSucceeded(
+ result: BiometricPrompt.AuthenticationResult
+ ) {
+ super.onAuthenticationSucceeded(result)
+ val response = AuthenticatorAssertionResponse(
+ requestOptions = request,
+ credentialId = credId,
+ origin = origin,
+ up = true,
+ uv = true,
+ be = true,
+ bs = true,
+ userHandle = uid,
+ packageName = packageName
+ )
+
+ val sig = Signature.getInstance("SHA256withECDSA")
+ sig.initSign(privateKey)
+ sig.update(response.dataToSign())
+ response.signature = sig.sign()
+
+ val credential = FidoPublicKeyCredential(
+ rawId = credId,
+ response = response,
+ authenticatorAttachment = "", // Add your authenticator attachment
+ )
+ val result = Intent()
+ val passkeyCredential = PublicKeyCredential(credential.json())
+ PendingIntentHandler.setGetCredentialResponse(
+ result, GetCredentialResponse(passkeyCredential)
+ )
+ setResult(RESULT_OK, result)
+ finish()
+ }
+ }
+ )
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle("Use your screen lock")
+ .setSubtitle("Use passkey for ${request.rpId}")
+ .setAllowedAuthenticators(
+ BiometricManager.Authenticators.BIOMETRIC_STRONG
+ /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
+ )
+ .build()
+ biometricPrompt.authenticate(promptInfo)
+ // [END android_identity_credential_provider_user_validation_biometric]
+ }
+
+ @RequiresApi(VERSION_CODES.M)
+ private fun handleUserSelectionForPasswordAuthentication(
+ mDatabase: MyDatabase,
+ callingAppInfo: CallingAppInfo,
+ ) {
+ // [START android_identity_credential_provider_user_selection_password]
+ val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ val passwordOption = getRequest?.credentialOptions?.first() as GetPasswordOption
+
+ val username = passwordOption.allowedUserIds.first()
+ // Fetch the credentials for the calling app package name
+ val creds = mDatabase.getCredentials(callingAppInfo.packageName)
+ val passwords = creds.passwords
+ val it = passwords.iterator()
+ var password = ""
+ while (it.hasNext()) {
+ val passwordItemCurrent = it.next()
+ if (passwordItemCurrent.username == username) {
+ password = passwordItemCurrent.password
+ break
+ }
+ }
+ // [END android_identity_credential_provider_user_selection_password]
+
+ // [START android_identity_credential_provider_set_response]
+ // Set the response back
+ val result = Intent()
+ val passwordCredential = PasswordCredential(username, password)
+ PendingIntentHandler.setGetCredentialResponse(
+ result, GetCredentialResponse(passwordCredential)
+ )
+ setResult(Activity.RESULT_OK, result)
+ finish()
+ // [END android_identity_credential_provider_set_response]
+ }
+
+ // [START android_identity_credential_pending_intent]
+ fun createSettingsPendingIntent(): PendingIntent { // [END android_identity_credential_pending_intent]
+ return PendingIntent.getBroadcast(this, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun getPublicKeyFromKeyPair(keyPair: KeyPair): ByteArray {
+ return byteArrayOf()
+ }
+
+ private fun b64Encode(certHash: ByteArray) {}
+
+ private fun handleCreatePasskeyQuery(
+ request: BeginCreatePublicKeyCredentialRequest
+ ): BeginCreateCredentialResponse {
+ return BeginCreateCredentialResponse()
+ }
+
+ private fun createNewPendingIntent(
+ accountId: String,
+ intent: String
+ ): PendingIntent {
+ return PendingIntent.getBroadcast(this, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun b64Decode(encodedString: String): ByteArray {
+ return byteArrayOf()
+ }
+
+ private fun convertPrivateKey(privateKeyBytes: ByteArray): ECPrivateKey {
+ return ECPrivateKeyImpl()
+ }
+}
+
+object CredentialsRepo {
+ const val EXTRA_KEY_ACCOUNT_ID: String = ""
+}
+
+class MyDatabase {
+ fun addNewPassword(passwordInfo: PasswordInfo) {}
+
+ fun getPasskey(credIdEnc: String): PasskeyInfo {
+ return PasskeyInfo()
+ }
+
+ fun getCredentials(packageName: String): CredentialsInfo {
+ return CredentialsInfo()
+ }
+}
+
+data class PasswordInfo(
+ val id: String = "",
+ val password: String = "",
+ val packageName: String = "",
+ val username: String = ""
+)
+
+data class PasskeyInfo(
+ val credPrivateKey: String = "",
+ val uid: String = "",
+ val username: String = ""
+)
+
+data class CredentialsInfo(
+ val passwords: List = listOf()
+)
+
+class ECPrivateKeyImpl : ECPrivateKey {
+ override fun getAlgorithm(): String = ""
+ override fun getFormat(): String = ""
+ override fun getEncoded(): ByteArray = byteArrayOf()
+ override fun getParams(): ECParameterSpec {
+ return ECParameterSpec(
+ EllipticCurve(
+ { 0 },
+ BigInteger.ZERO,
+ BigInteger.ZERO
+ ),
+ ECPoint(
+ BigInteger.ZERO,
+ BigInteger.ZERO
+ ),
+ BigInteger.ZERO,
+ 0
+ )
+ }
+ override fun getS(): BigInteger = BigInteger.ZERO
+}
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
new file mode 100644
index 000000000..05b06cce7
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/Fido2ToCredmanMigration.kt
@@ -0,0 +1,271 @@
+/*
+ * 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
+import android.content.Context
+import android.util.JsonWriter
+import android.util.Log
+import android.widget.Toast
+import androidx.credentials.CreateCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
+import androidx.credentials.CredentialManager
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPasswordOption
+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
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okhttp3.ResponseBody
+import org.json.JSONObject
+import ru.gildor.coroutines.okhttp.await
+
+class Fido2ToCredmanMigration(
+ private val context: Context,
+ private val client: OkHttpClient,
+) {
+ private val BASE_URL = ""
+ private val JSON = "".toMediaTypeOrNull()
+ private val PUBLIC_KEY = ""
+
+ // [START android_identity_fido2_credman_init]
+ val credMan = CredentialManager.create(context)
+ // [END android_identity_fido2_credman_init]
+
+ // [START android_identity_fido2_migration_post_request_body]
+ suspend fun registerRequest() {
+ // ...
+ val call = client.newCall(
+ Builder()
+ .method(
+ "POST",
+ jsonRequestBody {
+ name("attestation").value("none")
+ name("authenticatorSelection").objectValue {
+ name("residentKey").value("required")
+ }
+ }
+ ).build()
+ )
+ // ...
+ }
+ // [END android_identity_fido2_migration_post_request_body]
+
+ // [START android_identity_fido2_migration_register_request]
+ suspend fun registerRequest(sessionId: String): ApiResult {
+ val call = client.newCall(
+ Builder()
+ .url("https://github.com/$BASE_URL/%3Cyour%20api%20url%3E")
+ .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")
+ }
+ }
+ ).build()
+ )
+ val response = call.await()
+ return response.result("Error calling the api") {
+ parsePublicKeyCredentialCreationOptions(
+ body ?: throw ApiException("Empty response from the api call")
+ )
+ }
+ }
+ // [END android_identity_fido2_migration_register_request]
+
+ // [START android_identity_fido2_migration_create_passkey]
+ suspend fun createPasskey(
+ activity: Activity,
+ requestResult: JSONObject
+ ): CreatePublicKeyCredentialResponse? {
+ val request = CreatePublicKeyCredentialRequest(requestResult.toString())
+ var response: CreatePublicKeyCredentialResponse? = null
+ try {
+ response = credMan.createCredential(
+ request = request as CreateCredentialRequest,
+ context = activity
+ ) as CreatePublicKeyCredentialResponse
+ } catch (e: CreateCredentialException) {
+
+ showErrorAlert(activity, e)
+
+ return null
+ }
+ return response
+ }
+ // [END android_identity_fido2_migration_create_passkey]
+
+ // [START android_identity_fido2_migration_auth_with_passkeys]
+ /**
+ * @param sessionId The session ID to be used for the sign-in.
+ * @param credentialId The credential ID of this device.
+ * @return a JSON object.
+ */
+ suspend fun signinRequest(): ApiResult {
+ 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") {
+ parsePublicKeyCredentialRequestOptions(
+ body ?: throw ApiException("Empty response from /signinRequest")
+ )
+ }
+ }
+
+ /**
+ * @param sessionId The session ID to be used for the sign-in.
+ * @param response The JSONObject for signInResponse.
+ * @param credentialId id/rawId.
+ * @return A list of all the credentials registered on the server,
+ * including the newly-registered one.
+ */
+ suspend fun signinResponse(
+ sessionId: String,
+ response: JSONObject,
+ credentialId: String
+ ): ApiResult {
+
+ val call = client.newCall(
+ Builder().url("https://github.com/$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")
+ )
+ }
+ }
+ ).build()
+ )
+ val apiResponse = call.await()
+ return apiResponse.result("Error calling /signingResponse") {
+ }
+ }
+ // [END android_identity_fido2_migration_auth_with_passkeys]
+
+ // [START android_identity_fido2_migration_get_passkeys]
+ suspend fun getPasskey(
+ activity: Activity,
+ creationResult: JSONObject
+ ): GetCredentialResponse? {
+ Toast.makeText(
+ activity,
+ "Fetching previously stored credentials",
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ var result: GetCredentialResponse? = null
+ try {
+ val request = GetCredentialRequest(
+ listOf(
+ GetPublicKeyCredentialOption(
+ creationResult.toString(),
+ null
+ ),
+ GetPasswordOption()
+ )
+ )
+ result = credMan.getCredential(activity, request)
+ if (result.credential is PublicKeyCredential) {
+ val publicKeycredential = result.credential as PublicKeyCredential
+ Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
+ return result
+ }
+ } catch (e: Exception) {
+ showErrorAlert(activity, e)
+ }
+ return result
+ }
+ // [END android_identity_fido2_migration_get_passkeys]
+
+ private fun showErrorAlert(
+ activity: Activity,
+ e: Exception
+ ) {}
+
+ private fun jsonRequestBody(body: JsonWriter.() -> Unit): RequestBody {
+ val output = StringWriter()
+ JsonWriter(output).use { writer ->
+ writer.beginObject()
+ writer.body()
+ writer.endObject()
+ }
+ return output.toString().toRequestBody(JSON)
+ }
+
+ private fun JsonWriter.objectValue(body: JsonWriter.() -> Unit) {
+ beginObject()
+ body()
+ endObject()
+ }
+
+ private fun formatCookie(sessionId: String): String {
+ return ""
+ }
+
+ private fun parsePublicKeyCredentialCreationOptions(body: ResponseBody): JSONObject {
+ return JSONObject()
+ }
+
+ private fun parsePublicKeyCredentialRequestOptions(body: ResponseBody): JSONObject {
+ return JSONObject()
+ }
+
+ private fun Response.result(errorMessage: String, data: Response.() -> T): ApiResult {
+ return Success()
+ }
+}
+
+sealed class ApiResult {
+ class Success : ApiResult()
+}
+
+class ApiException(message: String) : RuntimeException(message)
diff --git a/identity/credentialmanager/src/main/java/com/example/identity/MainActivity.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MainActivity.kt
similarity index 100%
rename from identity/credentialmanager/src/main/java/com/example/identity/MainActivity.kt
rename to identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MainActivity.kt
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
new file mode 100644
index 000000000..77db763d3
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/MyCredentialProviderService.kt
@@ -0,0 +1,327 @@
+/*
+ * 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.app.PendingIntent
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.BeginCreateCredentialRequest
+import androidx.credentials.provider.BeginCreateCredentialResponse
+import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
+import androidx.credentials.provider.BeginGetCredentialRequest
+import androidx.credentials.provider.BeginGetCredentialResponse
+import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CredentialEntry
+import androidx.credentials.provider.CredentialProviderService
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.ProviderClearCredentialStateRequest
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions
+
+@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
+class MyCredentialProviderService : CredentialProviderService() {
+ private val PERSONAL_ACCOUNT_ID: String = ""
+ private val FAMILY_ACCOUNT_ID: String = ""
+ private val CREATE_PASSKEY_INTENT: String = ""
+ private val PACKAGE_NAME: String = ""
+ private val EXTRA_KEY_ACCOUNT_ID: String = ""
+ private val UNIQUE_REQ_CODE: Int = 1
+ private val UNLOCK_INTENT: String = ""
+ private val UNIQUE_REQUEST_CODE: Int = 0
+ private val TAG: String = ""
+ private val GET_PASSWORD_INTENT: String = ""
+
+ // [START android_identity_credential_provider_passkey_creation]
+ override fun onBeginCreateCredentialRequest(
+ request: BeginCreateCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver,
+ ) {
+ val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
+ if (response != null) {
+ callback.onResult(response)
+ } else {
+ callback.onError(CreateCredentialUnknownException())
+ }
+ }
+
+ fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
+ when (request) {
+ is BeginCreatePublicKeyCredentialRequest -> {
+ // Request is passkey type
+ return handleCreatePasskeyQuery(request)
+ }
+ }
+ // Request not supported
+ return null
+ }
+
+ private fun handleCreatePasskeyQuery(
+ request: BeginCreatePublicKeyCredentialRequest
+ ): BeginCreateCredentialResponse {
+
+ // Adding two create entries - one for storing credentials to the 'Personal'
+ // 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(
+ FAMILY_ACCOUNT_ID,
+ createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
+ )
+ )
+
+ return BeginCreateCredentialResponse(createEntries)
+ }
+
+ private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
+ val intent = Intent(action).setPackage(PACKAGE_NAME)
+
+ // Add your local account ID as an extra to the intent, so that when
+ // user selects this entry, the credential can be saved to this
+ // account
+ intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)
+
+ return PendingIntent.getActivity(
+ applicationContext, UNIQUE_REQ_CODE,
+ intent,
+ (
+ PendingIntent.FLAG_MUTABLE
+ or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ }
+ // [END android_identity_credential_provider_passkey_creation]
+
+ private lateinit var response: BeginGetCredentialResponse
+
+ // [START android_identity_credential_provider_sign_in]
+ private val unlockEntryTitle = "Authenticate to continue"
+
+ override fun onBeginGetCredentialRequest(
+ request: BeginGetCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver,
+ ) {
+ if (isAppLocked()) {
+ callback.onResult(
+ BeginGetCredentialResponse(
+ authenticationActions = mutableListOf(
+ AuthenticationAction(
+ unlockEntryTitle, createUnlockPendingIntent()
+ )
+ )
+ )
+ )
+ return
+ }
+ try {
+ response = processGetCredentialRequest(request)
+ callback.onResult(response)
+ } catch (e: GetCredentialException) {
+ callback.onError(GetCredentialUnknownException())
+ }
+ }
+ // [END android_identity_credential_provider_sign_in]
+
+ // [START android_identity_credential_provider_unlock_pending_intent]
+ private fun createUnlockPendingIntent(): PendingIntent {
+ val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
+ return PendingIntent.getActivity(
+ applicationContext, UNIQUE_REQUEST_CODE, intent,
+ (
+ PendingIntent.FLAG_MUTABLE
+ or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ }
+ // [END android_identity_credential_provider_unlock_pending_intent]
+
+ // [START android_identity_credential_provider_process_get_credential_request]
+ companion object {
+ // These intent actions are specified for corresponding activities
+ // 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(
+ request: BeginGetCredentialRequest
+ ): BeginGetCredentialResponse {
+ val callingPackageInfo = request.callingAppInfo
+ val callingPackageName = callingPackageInfo?.packageName.orEmpty()
+ val credentialEntries: MutableList = mutableListOf()
+
+ for (option in request.beginGetCredentialOptions) {
+ when (option) {
+ is BeginGetPasswordOption -> {
+ credentialEntries.addAll(
+ populatePasswordData(
+ callingPackageName,
+ option
+ )
+ )
+ }
+ is BeginGetPublicKeyCredentialOption -> {
+ credentialEntries.addAll(
+ populatePasskeyData(
+ callingPackageInfo,
+ option
+ )
+ )
+ } else -> {
+ Log.i(TAG, "Request not supported")
+ }
+ }
+ }
+ return BeginGetCredentialResponse(credentialEntries)
+ }
+ // [END android_identity_credential_provider_process_get_credential_request]
+
+ @SuppressLint("RestrictedApi")
+ // [START android_identity_credential_provider_populate_pkpw_data]
+ private fun populatePasskeyData(
+ callingAppInfo: CallingAppInfo?,
+ option: BeginGetPublicKeyCredentialOption
+ ): List {
+ val passkeyEntries: MutableList = mutableListOf()
+ val request = PublicKeyCredentialRequestOptions(option.requestJson)
+ // Get your credentials from database where you saved during creation flow
+ val creds = getCredentialsFromInternalDb(request.rpId)
+ val passkeys = creds.passkeys
+ for (passkey in passkeys) {
+ val data = Bundle()
+ data.putString("credId", passkey.credId)
+ passkeyEntries.add(
+ PublicKeyCredentialEntry(
+ context = applicationContext,
+ username = passkey.username,
+ pendingIntent = createNewPendingIntent(
+ GET_PASSKEY_INTENT_ACTION,
+ data
+ ),
+ beginGetPublicKeyCredentialOption = option,
+ displayName = passkey.displayName,
+ icon = passkey.icon
+ )
+ )
+ }
+ return passkeyEntries
+ }
+
+ // Fetch password credentials and create password entries to populate to the user
+ private fun populatePasswordData(
+ callingPackage: String,
+ option: BeginGetPasswordOption
+ ): List {
+ val passwordEntries: MutableList = mutableListOf()
+
+ // Get your password credentials from database where you saved during
+ // creation flow
+ val creds = getCredentialsFromInternalDb(callingPackage)
+ val passwords = creds.passwords
+ for (password in passwords) {
+ passwordEntries.add(
+ PasswordCredentialEntry(
+ context = applicationContext,
+ username = password.username,
+ pendingIntent = createNewPendingIntent(
+ GET_PASSWORD_INTENT
+ ),
+ beginGetPasswordOption = option,
+ displayName = password.username,
+ icon = password.icon
+ )
+ )
+ }
+ return passwordEntries
+ }
+
+ private fun createNewPendingIntent(
+ action: String,
+ extra: Bundle? = null
+ ): PendingIntent {
+ val intent = Intent(action).setPackage(PACKAGE_NAME)
+ if (extra != null) {
+ intent.putExtra("CREDENTIAL_DATA", extra)
+ }
+
+ return PendingIntent.getActivity(
+ applicationContext, UNIQUE_REQUEST_CODE, intent,
+ (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
+ )
+ }
+ // [END android_identity_credential_provider_populate_pkpw_data]
+
+ // [START android_identity_credential_provider_clear_credential]
+ override fun onClearCredentialStateRequest(
+ request: ProviderClearCredentialStateRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver
+ ) {
+ // Delete any maintained state as appropriate.
+ }
+ // [END android_identity_credential_provider_clear_credential]
+
+ private fun isAppLocked(): Boolean {
+ return true
+ }
+
+ private fun getCredentialsFromInternalDb(rpId: String): Creds {
+ return Creds()
+ }
+}
+
+data class Creds(
+ val passkeys: List = listOf(),
+ val passwords: List = listOf()
+)
+
+data class Passkey(
+ val credId: String = "",
+ val username: String = "",
+ val displayName: String = "",
+ val icon: Icon
+)
+
+data class Password(
+ val username: String = "",
+ val icon: Icon
+)
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
new file mode 100644
index 000000000..60ab33d2c
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyAndPasswordFunctions.kt
@@ -0,0 +1,336 @@
+/*
+ * 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.content.Context
+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
+import androidx.credentials.CustomCredential
+import androidx.credentials.GetCredentialRequest
+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.GetCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
+import androidx.credentials.pendingGetCredentialRequest
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.runBlocking
+import org.json.JSONObject
+
+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
+
+ // 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]
+ // Get password logins from the credential provider on the user's device.
+ val getPasswordOption = GetPasswordOption()
+
+ // Get passkeys from the credential provider on the user's device.
+ val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
+ requestJson = requestJson
+ )
+ // [END android_identity_get_password_passkey_options]
+ var result: GetCredentialResponse
+ var preferImmediatelyAvailableCredentials: Boolean = false
+ // [START android_identity_get_credential_request]
+ val credentialRequest = GetCredentialRequest(
+ // Include all the sign-in options that your app supports.
+ listOf(getPasswordOption, getPublicKeyCredentialOption),
+ // Defines whether you prefer to use only immediately available
+ // credentials or hybrid credentials.
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
+ )
+ // [END android_identity_get_credential_request]
+ runBlocking {
+ // getPrepareCredential request
+ // [START android_identity_prepare_get_credential]
+ coroutineScope {
+ val response = credentialManager.prepareGetCredential(
+ GetCredentialRequest(
+ listOf(
+ // Include all the sign-in options that your app supports
+ 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]
+ }
+ }
+
+ 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]
+ }
+
+ 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]
+ 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
+ }
+
+ 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")
+ }
+ }
+ else -> {
+ // Catch any unrecognized credential type here.
+ Log.e(TAG, "Unexpected type of credential")
+ }
+ }
+ }
+ // [END android_identity_launch_sign_in_flow_2]
+
+ // [START android_identity_create_passkey]
+ suspend fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) {
+ var isConditional: Boolean = false
+ val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
+ // Contains the request in JSON format.
+ requestJson = requestJson,
+ // Defines whether you prefer to use only locally-available
+ // credentials or hybrid credentials.
+ preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
+ // Automatically create a passkey if the user does not have one.
+ isConditional = isConditional
+ )
+
+ // Execute createCredential asynchronously to register credentials
+ // for a user account.
+ 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]
+
+ // [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]
+
+ 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)
+
+ // 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]
+}
+
+sealed class ExampleCustomCredential {
+ class ExampleCustomCredentialParsingException : Throwable()
+
+ companion object {
+ fun createFrom(data: Bundle): PublicKeyCredential {
+ return PublicKeyCredential("")
+ }
+
+ 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
new file mode 100644
index 000000000..f13052b49
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/PasskeyWebListener.kt
@@ -0,0 +1,256 @@
+/*
+ * 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
+import android.net.Uri
+import android.util.Log
+import android.webkit.WebView
+import android.widget.Toast
+import androidx.annotation.UiThread
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.webkit.JavaScriptReplyProxy
+import androidx.webkit.WebMessageCompat
+import androidx.webkit.WebViewCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.json.JSONArray
+import org.json.JSONObject
+
+// Placeholder for TAG log value.
+const val TAG = ""
+
+// This class is mostly copied from https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/PasskeyWebListener.kt.
+
+// [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
+) : 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)
+ }
+
+ else -> Log.i(TAG, "Incorrect request json")
+ }
+ }
+ }
+
+ 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)
+ }
+ }
+ }
+
+ // 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]
+}
diff --git a/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/RestoreCredentialsFunctions.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/RestoreCredentialsFunctions.kt
new file mode 100644
index 000000000..8ce9700e7
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/RestoreCredentialsFunctions.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.content.Context
+import androidx.credentials.ClearCredentialStateRequest
+import androidx.credentials.ClearCredentialStateRequest.Companion.TYPE_CLEAR_RESTORE_CREDENTIAL
+import androidx.credentials.CreateRestoreCredentialRequest
+import androidx.credentials.CredentialManager
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetRestoreCredentialOption
+
+class RestoreCredentialsFunctions(
+ private val context: Context,
+ private val credentialManager: CredentialManager,
+) {
+ suspend fun createRestoreKey(
+ createRestoreRequest: CreateRestoreCredentialRequest
+ ) {
+ // [START android_identity_restore_cred_create]
+ val credentialManager = CredentialManager.create(context)
+
+ // On a successful authentication create a Restore Key
+ // Pass in the context and CreateRestoreCredentialRequest object
+ val response = credentialManager.createCredential(context, createRestoreRequest)
+ // [END android_identity_restore_cred_create]
+ }
+
+ suspend fun getRestoreKey(
+ fetchAuthenticationJson: () -> String,
+ ) {
+ // [START android_identity_restore_cred_get]
+ // Fetch the Authentication JSON from server
+ val authenticationJson = fetchAuthenticationJson()
+
+ // Create the GetRestoreCredentialRequest object
+ val options = GetRestoreCredentialOption(authenticationJson)
+ val getRequest = GetCredentialRequest(listOf(options))
+
+ // The restore key can be fetched in two scenarios to
+ // 1. On the first launch of app on the device, fetch the Restore Key
+ // 2. In the onRestore callback (if the app implements the Backup Agent)
+ val response = credentialManager.getCredential(context, getRequest)
+ // [END android_identity_restore_cred_get]
+ }
+
+ suspend fun deleteRestoreKey() {
+ // [START android_identity_restore_cred_delete]
+ // Create a ClearCredentialStateRequest object
+ val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)
+
+ // On user log-out, clear the restore key
+ val response = credentialManager.clearCredentialState(clearRequest)
+ // [END android_identity_restore_cred_delete]
+ }
+}
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
new file mode 100644
index 000000000..28c58ef25
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SignInWithGoogleFunctions.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.content.Context
+import android.util.Log
+import androidx.credentials.CredentialManager
+import androidx.credentials.CustomCredential
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.exceptions.GetCredentialException
+import com.google.android.libraries.identity.googleid.GetGoogleIdOption
+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
+
+const val WEB_CLIENT_ID = ""
+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
+ }
+ }
+ // [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)
+ }
+ } 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]
+ }
+}
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
new file mode 100644
index 000000000..b61ddbe5c
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SingleTap.kt
@@ -0,0 +1,209 @@
+/*
+ * 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
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.BiometricPromptData
+import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.PendingIntentHandler
+
+class SingleTap : ComponentActivity() {
+ private val x: Any? = null
+ private val TAG: String = ""
+
+ private fun passkeyCreation(
+ request: BeginCreatePublicKeyCredentialRequest,
+ passwordCount: Int,
+ passkeyCount: Int
+ ) {
+ val option = null
+ val origin = null
+ val responseBuilder = null
+ val autoSelectEnabled = null
+ val allowedAuthenticator = 0
+
+ val y =
+ // [START android_identity_single_tap_set_biometric_prompt_data]
+ PublicKeyCredentialEntry(
+ // other properties...
+
+ biometricPromptData = BiometricPromptData(
+ allowedAuthenticators = allowedAuthenticator
+ )
+ )
+ // [END android_identity_single_tap_set_biometric_prompt_data]
+
+ when (x) {
+ // [START android_identity_single_tap_pk_creation]
+ is BeginCreatePublicKeyCredentialRequest -> {
+ Log.i(TAG, "Request is passkey type")
+ return handleCreatePasskeyQuery(request, passwordCount, passkeyCount)
+ }
+ // [END android_identity_single_tap_pk_creation]
+
+ // [START android_identity_single_tap_pk_flow]
+ is BeginGetPublicKeyCredentialOption -> {
+ // ... other logic
+
+ populatePasskeyData(
+ origin,
+ option,
+ responseBuilder,
+ autoSelectEnabled,
+ allowedAuthenticator
+ )
+
+ // ... other logic as needed
+ }
+ // [END android_identity_single_tap_pk_flow]
+ }
+ }
+
+ private fun handleCreatePasskeyQuery(
+ request: BeginCreatePublicKeyCredentialRequest,
+ passwordCount: Int,
+ passkeyCount: Int
+ ) {
+ val allowedAuthenticator = 0
+
+ // [START android_identity_single_tap_create_entry]
+ val createEntry = CreateEntry(
+ // Additional properties...
+ biometricPromptData = BiometricPromptData(
+ allowedAuthenticators = allowedAuthenticator
+ ),
+ )
+ // [END android_identity_single_tap_create_entry]
+ }
+
+ @RequiresApi(VERSION_CODES.M)
+ private fun handleCredentialEntrySelection(
+ accountId: String = "",
+ createPasskey: (String, CallingAppInfo, ByteArray?, String) -> Unit
+ ) {
+ // [START android_identity_single_tap_handle_credential_entry]
+ val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+ if (createRequest == null) {
+ Log.i(TAG, "request is null")
+ setUpFailureResponseAndFinish("Unable to extract request from intent")
+ return
+ }
+ // Other logic...
+
+ val biometricPromptResult = createRequest.biometricPromptResult
+
+ // Add your logic based on what needs to be done
+ // after getting biometrics
+
+ if (createRequest.callingRequest is CreatePublicKeyCredentialRequest) {
+ val publicKeyRequest: CreatePublicKeyCredentialRequest =
+ createRequest.callingRequest as CreatePublicKeyCredentialRequest
+
+ if (biometricPromptResult == null) {
+ // Do your own authentication flow, if needed
+ } else if (biometricPromptResult.isSuccessful) {
+ createPasskey(
+ publicKeyRequest.requestJson,
+ createRequest.callingAppInfo,
+ publicKeyRequest.clientDataHash,
+ accountId
+ )
+ } else {
+ val error = biometricPromptResult.authenticationError
+ // Process the error
+ }
+
+ // Other logic...
+ }
+ // [END android_identity_single_tap_handle_credential_entry]
+ }
+
+ @RequiresApi(VERSION_CODES.M)
+ private fun retrieveProviderGetCredentialRequest(
+ validatePasskey: (String, String, String, String, String, String, String) -> Unit,
+ publicKeyRequest: CreatePublicKeyCredentialRequest,
+ origin: String,
+ uid: String,
+ passkey: PK,
+ credId: String,
+ privateKey: String,
+ ) {
+ // [START android_identity_single_tap_get_cred_request]
+ val getRequest =
+ PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+ if (getRequest == null) {
+ Log.i(TAG, "request is null")
+ setUpFailureResponseAndFinish("Unable to extract request from intent")
+ return
+ }
+
+ // Other logic...
+
+ val biometricPromptResult = getRequest.biometricPromptResult
+
+ // Add your logic based on what needs to be done
+ // after getting biometrics
+
+ if (biometricPromptResult == null) {
+ // Do your own authentication flow, if necessary
+ } else if (biometricPromptResult.isSuccessful) {
+
+ Log.i(TAG, "The response from the biometricPromptResult was ${biometricPromptResult.authenticationResult?.authenticationType}")
+
+ validatePasskey(
+ publicKeyRequest.requestJson,
+ origin,
+ packageName,
+ uid,
+ passkey.username,
+ credId,
+ privateKey
+ )
+ } else {
+ val error = biometricPromptResult.authenticationError
+ // Process the error
+ }
+
+ // Other logic...
+ // [END android_identity_single_tap_get_cred_request]
+ }
+
+ private fun CreateEntry(biometricPromptData: BiometricPromptData) {}
+
+ private fun PublicKeyCredentialEntry(biometricPromptData: BiometricPromptData) {}
+
+ private fun populatePasskeyData(
+ origin: Any?,
+ option: Any?,
+ responseBuilder: Any?,
+ autoSelectEnabled: Any?,
+ allowedAuthenticator: Any?
+ ) {}
+
+ private fun setUpFailureResponseAndFinish(str: String) {}
+}
+
+data class PK(
+ val username: String,
+)
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
new file mode 100644
index 000000000..f37f6d9da
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/SmartLockToCredMan.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.content.Context
+import android.util.Log
+import androidx.credentials.CredentialManager
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.PasswordCredential
+import androidx.credentials.exceptions.GetCredentialCancellationException
+import androidx.credentials.exceptions.GetCredentialCustomException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialUnsupportedException
+import androidx.credentials.exceptions.NoCredentialException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class SmartLockToCredMan(
+ private val credentialManager: CredentialManager,
+ private val activityContext: Context,
+ private val coroutineScope: CoroutineScope,
+) {
+ // [START android_identity_init_password_option]
+ // Retrieves the user's saved password for your app from their
+ // password provider.
+ val getPasswordOption = GetPasswordOption()
+ // [END android_identity_init_password_option]
+
+ // [START android_identity_get_cred_request]
+ val getCredRequest = GetCredentialRequest(
+ listOf(getPasswordOption)
+ )
+ // [END android_identity_get_cred_request]
+
+ val TAG: String = "tag"
+
+ // [START android_identity_launch_sign_in_flow]
+ fun launchSignInFlow() {
+ coroutineScope.launch {
+ try {
+ // Attempt to retrieve the credential from the Credential Manager.
+ val result = credentialManager.getCredential(
+ // Use an activity-based context to avoid undefined system UI
+ // launching behavior.
+ context = activityContext,
+ request = getCredRequest
+ )
+
+ // Process the successfully retrieved credential.
+ handleSignIn(result)
+ } catch (e: GetCredentialException) {
+ // Handle any errors that occur during the credential retrieval
+ // process.
+ handleFailure(e)
+ }
+ }
+ }
+
+ private fun handleSignIn(result: GetCredentialResponse) {
+ // Extract the credential from the response.
+ val credential = result.credential
+
+ // Determine the type of credential and handle it accordingly.
+ when (credential) {
+ is PasswordCredential -> {
+ val username = credential.id
+ val password = credential.password
+
+ // Use the extracted username and password to perform
+ // authentication.
+ }
+
+ else -> {
+ // Handle unrecognized credential types.
+ Log.e(TAG, "Unexpected type of credential")
+ }
+ }
+ }
+
+ private fun handleFailure(e: GetCredentialException) {
+ // Handle specific credential retrieval errors.
+ when (e) {
+ is GetCredentialCancellationException -> {
+ /* This exception is thrown when the user intentionally cancels
+ the credential retrieval operation. Update the application's state
+ accordingly. */
+ }
+
+ is GetCredentialCustomException -> {
+ /* This exception is thrown when a custom error occurs during the
+ credential retrieval flow. Refer to the documentation of the
+ third-party SDK used to create the GetCredentialRequest for
+ handling this exception. */
+ }
+
+ is GetCredentialInterruptedException -> {
+ /* This exception is thrown when an interruption occurs during the
+ credential retrieval flow. Determine whether to retry the
+ operation or proceed with an alternative authentication method. */
+ }
+
+ is GetCredentialProviderConfigurationException -> {
+ /* This exception is thrown when there is a mismatch in
+ configurations for the credential provider. Verify that the
+ provider dependency is included in the manifest and that the
+ required system services are enabled. */
+ }
+
+ is GetCredentialUnknownException -> {
+ /* This exception is thrown when the credential retrieval
+ operation fails without providing any additional details. Handle
+ the error appropriately based on the application's context. */
+ }
+
+ is GetCredentialUnsupportedException -> {
+ /* This exception is thrown when the device does not support the
+ Credential Manager feature. Inform the user that credential-based
+ authentication is unavailable and guide them to an alternative
+ authentication method. */
+ }
+
+ is NoCredentialException -> {
+ /* This exception is thrown when there are no viable credentials
+ available for the user. Prompt the user to sign up for an account
+ or provide an alternative authentication method. Upon successful
+ authentication, store the login information using
+ androidx.credentials.CredentialManager.createCredential to
+ facilitate easier sign-in the next time. */
+ }
+
+ else -> {
+ // Handle unexpected exceptions.
+ Log.w(TAG, "Unexpected exception type: ${e::class.java.name}")
+ }
+ }
+ }
+ // [END android_identity_launch_sign_in_flow]
+}
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
new file mode 100644
index 000000000..935da4971
--- /dev/null
+++ b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/WebViewMainActivity.kt
@@ -0,0 +1,101 @@
+/*
+ * 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
+import android.os.Bundle
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.webkit.WebViewCompat
+import androidx.webkit.WebViewFeature
+import kotlinx.coroutines.CoroutineScope
+
+class WebViewMainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // [START android_identity_initialize_the_webview]
+ val credentialManagerHandler = CredentialManagerHandler(this)
+
+ 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
+ )
+ } else {
+ // Fallback routine for unsupported API levels.
+ }
+ loadUrl(url)
+ }
+ }
+ )
+ }
+ // [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)
+
+ 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]
+
+ // [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]
+ }
+}
diff --git a/identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Color.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Color.kt
similarity index 100%
rename from identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Color.kt
rename to identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Color.kt
diff --git a/identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Theme.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Theme.kt
similarity index 100%
rename from identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Theme.kt
rename to identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Theme.kt
diff --git a/identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Type.kt b/identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Type.kt
similarity index 100%
rename from identity/credentialmanager/src/main/java/com/example/identity/ui/theme/Type.kt
rename to identity/credentialmanager/src/main/java/com/example/identity/credentialmanager/ui/theme/Type.kt
diff --git a/identity/credentialmanager/src/main/jsonSnippets.json b/identity/credentialmanager/src/main/jsonSnippets.json
new file mode 100644
index 000000000..bdf7a0f07
--- /dev/null
+++ b/identity/credentialmanager/src/main/jsonSnippets.json
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+
+{
+ "snippets": [
+ {
+ "DigitalAssetLinking":
+ // Digital asset linking
+ // [START android_identity_assetlinks_json]
+ [
+ {
+ "relation": [
+ "delegate_permission/common.handle_all_urls",
+ "delegate_permission/common.get_login_creds"
+ ],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "com.example.android",
+ "sha256_cert_fingerprints": [
+ ""
+ ]
+ }
+ }
+ ]
+ // [END android_identity_assetlinks_json]
+ },
+
+ {
+ "FormatJsonRequestPasskey":
+ // JSON request format
+ // [START android_identity_format_json_request_passkey]
+ {
+ "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
+ "allowCredentials": [],
+ "timeout": 1800000,
+ "userVerification": "required",
+ "rpId": "https://passkeys-codelab.glitch.me/"
+ }
+ // [END android_identity_format_json_request_passkey]
+ },
+
+ {
+ "FormatJsonResponsePasskey":
+ // JSON response format
+ // [START android_identity_format_json_response_passkey]
+ {
+ "id": "",
+ "type": "public-key",
+ "rawId": "",
+ "response": {
+ "clientDataJSON": "",
+ "authenticatorData": "",
+ "signature": "",
+ "userHandle": ""
+ }
+ }
+ // [END android_identity_format_json_response_passkey]
+ },
+ {
+ "CreatePasskeyJsonRequest":
+ // Json request for creating a passkey
+ // [START android_identity_create_passkey_request_json]
+ {
+ "challenge": "",
+ "rp": {
+ "name": "",
+ "id": ""
+ },
+ "user": {
+ "id": "",
+ "name": "",
+ "displayName": ""
+ },
+ "pubKeyCredParams": [
+ {
+ "type": "public-key",
+ "alg": -7
+ }
+ ],
+ "attestation": "none",
+ "excludeCredentials": [
+ {
+ "id": "",
+ "type": "public-key"
+ }
+ ],
+ "authenticatorSelection": {
+ "requireResidentKey": true,
+ "residentKey": "required",
+ "userVerification": "required"
+ }
+ }
+ // [END android_identity_create_passkey_request_json]
+ },
+ {
+ "CreatePasskeyHandleJsonResponse":
+ // Json response when creating a passkey
+ // [START android_identity_create_passkey_response_json]
+ {
+ "id": "",
+ "type": "public-key",
+ "rawId": "",
+ "response": {
+ "clientDataJSON": "",
+ "attestationObject": ""
+ },
+ "authenticatorAttachment": "platform"
+ }
+ // [END android_identity_create_passkey_response_json]
+ },
+ {
+ "SignInJsonRequest":
+ //Json object sent by server when creating sign-in request
+ // [START android_identity_create_sign_in_request_json]
+ {
+ "challenge": "",
+ "allowCredentials": [],
+ "rpId": ""
+ }
+ // [END android_identity_create_sign_in_request_json]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/identity/credentialmanager/src/main/othersnippets b/identity/credentialmanager/src/main/othersnippets
new file mode 100644
index 000000000..3a82843c6
--- /dev/null
+++ b/identity/credentialmanager/src/main/othersnippets
@@ -0,0 +1,18 @@
+// [START android_identity_apk_key_hash]
+android:apk-key-hash:
+// [END android_identity_apk_key_hash]
+
+// [START android_identity_keytool_sign]
+keytool -list -keystore
+// [END android_identity_keytool_sign]
+
+// [START android_identity_fingerprint_decode_python]
+import binascii
+import base64
+fingerprint = '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5'
+print("android:apk-key-hash:" + base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))
+// [END android_identity_fingerprint_decode_python]
+
+// [START android_identity_fingerprint_decoded]
+android:apk-key-hash:kffL-daBUxvHpY-4M8yhTavt5QnFEI2LsexohxrGPYU
+// [END android_identity_fingerprint_decoded]
\ No newline at end of file
diff --git a/identity/credentialmanager/src/main/res/drawable-v24/ic_launcher_foreground.xml b/identity/credentialmanager/src/main/res/drawable-v24/ic_launcher_foreground.xml
index 2b068d114..8760078c8 100644
--- a/identity/credentialmanager/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ b/identity/credentialmanager/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -1,3 +1,19 @@
+
+
-
\ No newline at end of file
+
diff --git a/identity/credentialmanager/src/main/res/drawable/ic_launcher_background.xml b/identity/credentialmanager/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9cb..e6202fbb1 100644
--- a/identity/credentialmanager/src/main/res/drawable/ic_launcher_background.xml
+++ b/identity/credentialmanager/src/main/res/drawable/ic_launcher_background.xml
@@ -1,4 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 6f3b755bf..b8ff0f029 100644
--- a/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,21 @@
+
-
\ No newline at end of file
+
diff --git a/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755bf..b8ff0f029 100644
--- a/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/identity/credentialmanager/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,21 @@
+
-
\ No newline at end of file
+
diff --git a/identity/credentialmanager/src/main/res/values/colors.xml b/identity/credentialmanager/src/main/res/values/colors.xml
index f8c6127d3..732ae206f 100644
--- a/identity/credentialmanager/src/main/res/values/colors.xml
+++ b/identity/credentialmanager/src/main/res/values/colors.xml
@@ -1,4 +1,19 @@
+
#FFBB86FC
#FF6200EE
@@ -7,4 +22,4 @@
#FF018786
#FF000000
#FFFFFFFF
-
\ No newline at end of file
+
diff --git a/identity/credentialmanager/src/main/res/values/strings.xml b/identity/credentialmanager/src/main/res/values/strings.xml
index 3178959b9..cb1497463 100644
--- a/identity/credentialmanager/src/main/res/values/strings.xml
+++ b/identity/credentialmanager/src/main/res/values/strings.xml
@@ -1,3 +1,26 @@
+
+
credentialmanager
-
\ No newline at end of file
+ // [START android_identity_assetlinks_app_association]
+
+ [{
+ \"include\": \"https://signin.example.com/.well-known/assetlinks.json/"
+ }]
+
+ // [END android_identity_assetlinks_app_association]
+
diff --git a/identity/credentialmanager/src/main/res/values/themes.xml b/identity/credentialmanager/src/main/res/values/themes.xml
index 65078ebe0..e2cf423e7 100644
--- a/identity/credentialmanager/src/main/res/values/themes.xml
+++ b/identity/credentialmanager/src/main/res/values/themes.xml
@@ -1,5 +1,20 @@
+
-
\ No newline at end of file
+
diff --git a/identity/credentialmanager/src/main/res/xml/provider.xml b/identity/credentialmanager/src/main/res/xml/provider.xml
new file mode 100644
index 000000000..9d75e5ad1
--- /dev/null
+++ b/identity/credentialmanager/src/main/res/xml/provider.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/identity/credentialmanager/src/main/res/xml/provider_settings.xml b/identity/credentialmanager/src/main/res/xml/provider_settings.xml
new file mode 100644
index 000000000..1983ab623
--- /dev/null
+++ b/identity/credentialmanager/src/main/res/xml/provider_settings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
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..9968c881c
--- /dev/null
+++ b/kmp/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
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/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..2ef25a933
--- /dev/null
+++ b/kmp/shared/build.gradle.kts
@@ -0,0 +1,90 @@
+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.shared"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ minSdk = libs.versions.minSdk.get().toInt()
+
+ withHostTestBuilder {
+ }
+
+ withDeviceTestBuilder {
+ sourceSetTreeName = "test"
+ }.configure {
+ instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ }
+
+ 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
+
+ // 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 {
+
+ }
+ }
+
+ 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..1d4927a52
--- /dev/null
+++ b/kmp/shared/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
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/kotlin/src/main/AndroidManifest.xml b/kotlin/src/main/AndroidManifest.xml
index 8072ee00d..1e772a05e 100644
--- a/kotlin/src/main/AndroidManifest.xml
+++ b/kotlin/src/main/AndroidManifest.xml
@@ -1,2 +1,17 @@
+
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/misc/build.gradle.kts b/misc/build.gradle.kts
index b2619b9e6..6a2dc60d1 100644
--- a/misc/build.gradle.kts
+++ b/misc/build.gradle.kts
@@ -1,3 +1,4 @@
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
@@ -57,6 +58,9 @@ 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)
implementation(libs.androidx.hilt.navigation.compose)
@@ -69,7 +73,13 @@ dependencies {
implementation(libs.androidx.startup.runtime)
implementation(libs.androidx.window.java)
implementation(libs.appcompat)
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.ai)
+ implementation(libs.guava.android)
+ implementation(libs.reactive.streams)
testImplementation(libs.junit)
+ testImplementation(kotlin("test"))
+ androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.runner)
diff --git a/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java
new file mode 100644
index 000000000..ba249b0e6
--- /dev/null
+++ b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestJavaSnippets.java
@@ -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.snippets;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import org.junit.Rule;
+import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+
+public class DeviceCompatibilityModeTestJavaSnippets {
+
+ // [START android_device_compatibility_mode_assert_isLetterboxed_java]
+ @Rule
+ public ActivityScenarioRule rule = new ActivityScenarioRule<>(MainActivity.class);
+
+ @Test
+ public void activity_launched_notLetterBoxed() {
+ try (ActivityScenario scenario =
+ ActivityScenario.launch(MainActivity.class)) {
+ scenario.onActivity( activity -> {
+ assertFalse(activity.isLetterboxed());
+ });
+ }
+ }
+ // [END android_device_compatibility_mode_assert_isLetterboxed_java]
+
+
+ // 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
new file mode 100644
index 000000000..65f9a7f1f
--- /dev/null
+++ b/misc/src/androidTest/java/com/example/snippets/DeviceCompatibilityModeTestKotlinSnippets.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.snippets
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertFalse
+import org.junit.Rule
+import org.junit.Test
+
+class DeviceCompatibilityModeTestKotlinSnippets {
+
+ // [START android_device_compatibility_mode_assert_isLetterboxed_kotlin]
+ @get:Rule
+ val activityRule = ActivityScenarioRule(MainActivity::class.java)
+
+ @Test
+ fun activity_launched_notLetterBoxed() {
+ activityRule.scenario.onActivity {
+ assertFalse(it.isLetterboxed())
+ }
+ }
+ // [END android_device_compatibility_mode_assert_isLetterboxed_kotlin]
+
+ // Class used by snippets.
+
+ class MainActivity : AppCompatActivity() {
+ fun isLetterboxed(): Boolean {
+ return true
+ }
+ }
+}
diff --git a/misc/src/main/AndroidManifest.xml b/misc/src/main/AndroidManifest.xml
index 2ad7db1e7..770f36dcd 100644
--- a/misc/src/main/AndroidManifest.xml
+++ b/misc/src/main/AndroidManifest.xml
@@ -1,4 +1,19 @@
+
diff --git a/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java b/misc/src/main/java/com/example/snippets/ActivityEmbeddingJavaSnippets.java
index 61dd91cee..a0ed0595b 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;
@@ -12,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;
@@ -42,9 +44,7 @@
public class ActivityEmbeddingJavaSnippets {
- static class SnippetsActivity extends Activity {
-
- private Context context;
+ static class SplitAttributesCalculatorSnippetsActivity extends AppCompatActivity {
@RequiresApi(api=VERSION_CODES.N)
@Override
@@ -115,6 +115,17 @@ else if (parentConfiguration.screenWidthDp >= 840) {
}
// [END android_activity_embedding_split_attributes_calculator_tabletop_java]
+ }
+ }
+
+ static class SplitRuleSnippetsActivity extends AppCompatActivity {
+
+ private Context context;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
// [START android_activity_embedding_splitPairFilter_java]
SplitPairFilter splitPairFilter = new SplitPairFilter(
new ComponentName(this, ListActivity.class),
@@ -184,7 +195,7 @@ else if (parentConfiguration.screenWidthDp >= 840) {
// [START android_activity_embedding_expandedActivityFilter_java]
ActivityFilter expandedActivityFilter = new ActivityFilter(
new ComponentName(this, ExpandedActivity.class),
- null
+ null
);
// [END android_activity_embedding_expandedActivityFilter_java]
@@ -204,13 +215,30 @@ else if (parentConfiguration.screenWidthDp >= 840) {
ruleController.addRule(activityRule);
// [END android_activity_embedding_addRuleActivityRule_java]
+ }
+
+
+ // [START android_activity_embedding_isActivityEmbedded_java]
+ boolean isActivityEmbedded(Activity activity) {
+ return ActivityEmbeddingController.getInstance(context).isActivityEmbedded(activity);
+ }
+ // [END android_activity_embedding_isActivityEmbedded_java]
+
+ }
+
+ static class SplitAttributesBuilderSnippetsActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
// [START android_activity_embedding_splitAttributesBuilder_java]
- SplitAttributes.Builder _splitAttributesBuilder = new SplitAttributes.Builder()
+ SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
- _splitAttributesBuilder.setDividerAttributes(
+ splitAttributesBuilder.setDividerAttributes(
new DividerAttributes.DraggableDividerAttributes.Builder()
.setColor(ContextCompat.getColor(this, R.color.divider_color))
.setWidthDp(4)
@@ -218,21 +246,12 @@ else if (parentConfiguration.screenWidthDp >= 840) {
.build()
);
}
- SplitAttributes _splitAttributes = _splitAttributesBuilder.build();
+ SplitAttributes _splitAttributes = splitAttributesBuilder.build();
// [END android_activity_embedding_splitAttributesBuilder_java]
}
-
-
- // [START android_activity_embedding_isActivityEmbedded_java]
- boolean isActivityEmbedded(Activity activity) {
- return ActivityEmbeddingController.getInstance(context).isActivityEmbedded(activity);
- }
- // [END android_activity_embedding_isActivityEmbedded_java]
-
}
-
/** @noinspection InnerClassMayBeStatic */
// [START android_activity_embedding_DetailActivity_class_java]
public class DetailActivity extends AppCompatActivity {
@@ -291,7 +310,7 @@ void onOpenC() {
// [END android_activity_embedding_B_class_java]
- static class SnippetActivity2 extends Activity {
+ static class RuleControllerSnippetsActivity extends Activity {
private Set filterSet = new HashSet<>();
@@ -308,7 +327,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
}
- static class SnippetActivity3 extends AppCompatActivity {
+ static class SplitDeviceActivity extends AppCompatActivity {
@OptIn(markerClass = ExperimentalWindowApi.class)
// [START android_activity_embedding_onCreate_SplitControllerCallbackAdapter_java]
@@ -329,8 +348,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
}
- static class SnippetActivity4 extends Activity {
+ static class ActivityPinningSnippetsActivity extends Activity {
+ @SuppressLint("RequiresWindowSdk")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -349,16 +369,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
.setDefaultSplitAttributes(splitAttributes)
.build();
- SplitController.getInstance(
- getApplicationContext()).pinTopActivityStack(getTaskId(),
- pinSplitRule);
+ SplitController.getInstance(getApplicationContext())
+ .pinTopActivityStack(getTaskId(), pinSplitRule);
});
// [END android_activity_embedding_pinButton_java]
// [START android_activity_embedding_getSplitSupportStatus_java]
if (SplitController.getInstance(this).getSplitSupportStatus() ==
- SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
- // Device supports split activity features.
+ SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
+ // Device supports split activity features.
}
// [END android_activity_embedding_getSplitSupportStatus_java]
diff --git a/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/ActivityEmbeddingKotlinSnippets.kt
index a2c90e0f4..7871e78d5 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
@@ -31,7 +32,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.startup.Initializer
import androidx.window.WindowSdkExtensions
-import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.ActivityEmbeddingController
import androidx.window.embedding.ActivityFilter
import androidx.window.embedding.ActivityRule
@@ -52,11 +52,9 @@ import kotlinx.coroutines.launch
class ActivityEmbeddingKotlinSnippets {
- class SnippetActivity : Activity() {
+ class SplitAttributesCalculatorSnippetsActivity : AppCompatActivity() {
- private val context = this
-
- @RequiresApi(api = VERSION_CODES.N)
+ @SuppressLint("RequiresWindowSdk")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -122,6 +120,16 @@ class ActivityEmbeddingKotlinSnippets {
}
}
// [END android_activity_embedding_split_attributes_calculator_tabletop_kotlin]
+ }
+ }
+
+ class SplitRuleSnippetsActivity : AppCompatActivity() {
+
+ private val context = this
+
+ @RequiresApi(api = VERSION_CODES.N)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
// [START android_activity_embedding_splitPairFilter_kotlin]
val splitPairFilter = SplitPairFilter(
@@ -207,14 +215,29 @@ class ActivityEmbeddingKotlinSnippets {
// [START android_activity_embedding_addRuleActivityRule_kotlin]
ruleController.addRule(activityRule)
// [END android_activity_embedding_addRuleActivityRule_kotlin]
+ }
+
+ // [START android_activity_embedding_isActivityEmbedded_kotlin]
+ fun isActivityEmbedded(activity: Activity): Boolean {
+ return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
+ }
+ // [END android_activity_embedding_isActivityEmbedded_kotlin]
+ }
+
+ class SplitAttributesBuilderSnippetsActivity : AppCompatActivity() {
+
+ @SuppressLint("RequiresWindowSdk")
+ @RequiresApi(VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
// [START android_activity_embedding_splitAttributesBuilder_kotlin]
- val _splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
+ val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
- _splitAttributesBuilder.setDividerAttributes(
+ splitAttributesBuilder.setDividerAttributes(
DividerAttributes.DraggableDividerAttributes.Builder()
.setColor(getColor(R.color.divider_color))
.setWidthDp(4)
@@ -222,14 +245,8 @@ class ActivityEmbeddingKotlinSnippets {
.build()
)
}
- val _splitAttributes: SplitAttributes = _splitAttributesBuilder.build()
+ val splitAttributes: SplitAttributes = splitAttributesBuilder.build()
// [END android_activity_embedding_splitAttributesBuilder_kotlin]
-
- // [START android_activity_embedding_isActivityEmbedded_kotlin]
- fun isActivityEmbedded(activity: Activity): Boolean {
- return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
- }
- // [END android_activity_embedding_isActivityEmbedded_kotlin]
}
}
@@ -259,7 +276,9 @@ class ActivityEmbeddingKotlinSnippets {
/**
* Function used by snippet.
*/
- fun classForItem(item: Int): Class<*> { return Class::class.java }
+ fun classForItem(item: Int): Class<*> {
+ return Class::class.java
+ }
// [START android_activity_embedding_MenuActivity_class_kotlin]
inner class MenuActivity : AppCompatActivity() {
@@ -277,7 +296,7 @@ class ActivityEmbeddingKotlinSnippets {
}
// [END android_activity_embedding_B_class_kotlin]
- class SnippetActivity2 : Activity() {
+ class RuleControllerSnippetsActivity : Activity() {
private val filterSet = HashSet()
@@ -293,9 +312,9 @@ class ActivityEmbeddingKotlinSnippets {
class SplitDeviceActivity : AppCompatActivity() {
- @OptIn(ExperimentalWindowApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
val splitController = SplitController.getInstance(this)
// [START android_activity_embedding_onCreate_SplitControllerCallbackAdapter_kotlin]
val layout = layoutInflater.inflate(R.layout.activity_main, null)
@@ -312,10 +331,12 @@ class ActivityEmbeddingKotlinSnippets {
}
}
- class SnippetActivity3 : AppCompatActivity() {
+ class ActivityPinningSnippetsActivity : AppCompatActivity() {
+ @SuppressLint("RequiresWindowSdk")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
// [START android_activity_embedding_pinButton_kotlin]
val pinButton: Button = findViewById(R.id.pinButton)
pinButton.setOnClickListener {
@@ -329,7 +350,8 @@ class ActivityEmbeddingKotlinSnippets {
.setDefaultSplitAttributes(splitAttributes)
.build()
- SplitController.getInstance(applicationContext).pinTopActivityStack(taskId, pinSplitRule)
+ SplitController.getInstance(applicationContext)
+ .pinTopActivityStack(taskId, pinSplitRule)
}
// [END android_activity_embedding_pinButton_kotlin]
diff --git a/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeJavaSnippets.java b/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeJavaSnippets.java
new file mode 100644
index 000000000..d5e3a3622
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeJavaSnippets.java
@@ -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.snippets;
+
+import android.graphics.Rect;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.window.layout.WindowMetricsCalculator;
+
+public class DeviceCompatibilityModeJavaSnippets {
+
+ @RequiresApi(api=VERSION_CODES.N)
+ // [START android_device_compatibility_mode_isLetterboxed_java]
+ public boolean isLetterboxed(AppCompatActivity activity) {
+ if (activity.isInMultiWindowMode()) {
+ return false;
+ }
+
+ WindowMetricsCalculator wmc = WindowMetricsCalculator.getOrCreate();
+ Rect currentBounds = wmc.computeCurrentWindowMetrics(activity).getBounds();
+ Rect maxBounds = wmc.computeMaximumWindowMetrics(activity).getBounds();
+
+ boolean isScreenPortrait = maxBounds.height() > maxBounds.width();
+
+ return (isScreenPortrait)
+ ? currentBounds.height() < maxBounds.height()
+ : currentBounds.width() < maxBounds.width();
+ }
+ // [END android_device_compatibility_mode_isLetterboxed_java]
+
+}
diff --git a/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeKotlinSnippets.kt b/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeKotlinSnippets.kt
new file mode 100644
index 000000000..7bc6a1971
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/DeviceCompatibilityModeKotlinSnippets.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.window.layout.WindowMetricsCalculator
+
+class DeviceCompatibilityModeKotlinSnippets : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ // [START android_device_compatibility_mode_isLetterboxed_kotlin]
+ fun isLetterboxed(activity: AppCompatActivity): Boolean {
+ if (isInMultiWindowMode) return false
+
+ val wmc = WindowMetricsCalculator.getOrCreate()
+ val currentBounds = wmc.computeCurrentWindowMetrics(this).bounds
+ val maxBounds = wmc.computeMaximumWindowMetrics(this).bounds
+
+ val isScreenPortrait = maxBounds.height() > maxBounds.width()
+
+ return if (isScreenPortrait) {
+ currentBounds.height() < maxBounds.height()
+ } else {
+ currentBounds.width() < maxBounds.width()
+ }
+ }
+ // [END android_device_compatibility_mode_isLetterboxed_kotlin]
+}
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..9a67db280
--- /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
+
+@Suppress("unused_parameter")
+@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]
+
+@Suppress("unused_parameter")
+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]
+
+ // [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()
+ }
+ }
+}
diff --git a/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippets.kt b/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippets.kt
new file mode 100644
index 000000000..f3e3d95e4
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippets.kt
@@ -0,0 +1,206 @@
+/*
+ * 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.ai
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.net.Uri
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagePart
+import com.google.firebase.ai.type.ResponseModality
+import com.google.firebase.ai.type.content
+import com.google.firebase.ai.type.generationConfig
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+object GeminiDeveloperApi25FlashModelConfiguration {
+ // [START android_gemini_developer_api_gemini_25_flash_model]
+ // Start by instantiating a GenerativeModel and specifying the model name:
+ val model = Firebase.ai(backend = GenerativeBackend.googleAI())
+ .generativeModel("gemini-2.5-flash")
+ // [END android_gemini_developer_api_gemini_25_flash_model]
+}
+
+object Gemini25FlashImagePreviewModelConfiguration {
+ // [START android_gemini_developer_api_gemini_25_flash_image_model]
+ val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
+ modelName = "gemini-2.5-flash-image-preview",
+ // Configure the model to respond with text and images (required)
+ generationConfig = generationConfig {
+ responseModalities = listOf(
+ ResponseModality.TEXT,
+ ResponseModality.IMAGE
+ )
+ }
+ )
+ // [END android_gemini_developer_api_gemini_25_flash_image_model]
+}
+
+@Suppress("unused")
+fun textOnlyInput(scope: CoroutineScope) {
+ val model = GeminiDeveloperApi25FlashModelConfiguration.model
+ // [START android_gemini_developer_api_text_only_input]
+ scope.launch {
+ val response = model.generateContent("Write a story about a magic backpack.")
+ }
+ // [END android_gemini_developer_api_text_only_input]
+}
+
+@Suppress("unused")
+fun textAndImageInput(scope: CoroutineScope, bitmap: Bitmap) {
+ val model = GeminiDeveloperApi25FlashModelConfiguration.model
+ // [START android_gemini_developer_api_multimodal_input]
+ scope.launch {
+ val response = model.generateContent(
+ content {
+ image(bitmap)
+ text("what is the object in the picture?")
+ }
+ )
+ }
+ // [END android_gemini_developer_api_multimodal_input]
+}
+
+@Suppress("unused")
+fun textAndAudioInput(
+ scope: CoroutineScope,
+ applicationContext: Application,
+ audioUri: Uri
+) {
+ val model = GeminiDeveloperApi25FlashModelConfiguration.model
+ // [START android_gemini_developer_api_multimodal_audio_input]
+ scope.launch {
+ val contentResolver = applicationContext.contentResolver
+ contentResolver.openInputStream(audioUri).use { stream ->
+ stream?.let {
+ val bytes = it.readBytes()
+
+ val prompt = content {
+ inlineData(bytes, "audio/mpeg") // Specify the appropriate audio MIME type
+ text("Transcribe this audio recording.")
+ }
+
+ val response = model.generateContent(prompt)
+ }
+ }
+ }
+ // [END android_gemini_developer_api_multimodal_audio_input]
+}
+
+@Suppress("unused")
+fun textAndVideoInput(
+ scope: CoroutineScope,
+ applicationContext: Application,
+ videoUri: Uri
+) {
+ val model = GeminiDeveloperApi25FlashModelConfiguration.model
+ // [START android_gemini_developer_api_multimodal_video_input]
+ scope.launch {
+ val contentResolver = applicationContext.contentResolver
+ contentResolver.openInputStream(videoUri).use { stream ->
+ stream?.let {
+ val bytes = it.readBytes()
+
+ val prompt = content {
+ inlineData(bytes, "video/mp4") // Specify the appropriate video MIME type
+ text("Describe the content of this video")
+ }
+
+ val response = model.generateContent(prompt)
+ }
+ }
+ }
+ // [END android_gemini_developer_api_multimodal_video_input]
+}
+
+@Suppress("unused")
+fun multiTurnChat(scope: CoroutineScope) {
+ val model = GeminiDeveloperApi25FlashModelConfiguration.model
+ // [START android_gemini_developer_api_multiturn_chat]
+ val chat = model.startChat(
+ history = listOf(
+ content(role = "user") { text("Hello, I have 2 dogs in my house.") },
+ content(role = "model") { text("Great to meet you. What would you like to know?") }
+ )
+ )
+
+ scope.launch {
+ val response = chat.sendMessage("How many paws are in my house?")
+ }
+ // [END android_gemini_developer_api_multiturn_chat]
+}
+
+@Suppress("unused")
+fun generateImageFromText(scope: CoroutineScope) {
+ val model = Gemini25FlashImagePreviewModelConfiguration.model
+ // [START android_gemini_developer_api_generate_image_from_text]
+ scope.launch {
+ // Provide a text prompt instructing the model to generate an image
+ val prompt =
+ "A hyper realistic picture of a t-rex with a blue bag pack roaming a pre-historic forest."
+ // To generate image output, call `generateContent` with the text input
+ val generatedImageAsBitmap: Bitmap? = model.generateContent(prompt)
+ .candidates.first().content.parts.filterIsInstance()
+ .firstOrNull()?.image
+ }
+ // [END android_gemini_developer_api_generate_image_from_text]
+}
+
+@Suppress("unused")
+fun editImage(scope: CoroutineScope, bitmap: Bitmap) {
+ val model = Gemini25FlashImagePreviewModelConfiguration.model
+ // [START android_gemini_developer_api_edit_image]
+ scope.launch {
+ // Provide a text prompt instructing the model to edit the image
+ val prompt = content {
+ image(bitmap)
+ text("Edit this image to make it look like a cartoon")
+ }
+ // To edit the image, call `generateContent` with the prompt (image and text input)
+ val generatedImageAsBitmap: Bitmap? = model.generateContent(prompt)
+ .candidates.first().content.parts.filterIsInstance().firstOrNull()?.image
+ // Handle the generated text and image
+ }
+ // [END android_gemini_developer_api_edit_image]
+}
+
+@Suppress("unused")
+fun editImageWithChat(scope: CoroutineScope, bitmap: Bitmap) {
+ val model = Gemini25FlashImagePreviewModelConfiguration.model
+ // [START android_gemini_developer_api_edit_image_chat]
+ scope.launch {
+ // Create the initial prompt instructing the model to edit the image
+ val prompt = content {
+ image(bitmap)
+ text("Edit this image to make it look like a cartoon")
+ }
+ // Initialize the chat
+ val chat = model.startChat()
+ // To generate an initial response, send a user message with the image and text prompt
+ var response = chat.sendMessage(prompt)
+ // Inspect the returned image
+ var generatedImageAsBitmap: Bitmap? = response
+ .candidates.first().content.parts.filterIsInstance().firstOrNull()?.image
+ // Follow up requests do not need to specify the image again
+ response = chat.sendMessage("But make it old-school line drawing style")
+ generatedImageAsBitmap = response
+ .candidates.first().content.parts.filterIsInstance().firstOrNull()?.image
+ }
+ // [END android_gemini_developer_api_edit_image_chat]
+}
diff --git a/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippetsJava.java b/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippetsJava.java
new file mode 100644
index 000000000..1ec83594b
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/ai/GeminiDeveloperApiSnippetsJava.java
@@ -0,0 +1,381 @@
+/*
+ * 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.ai;
+
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import com.example.snippets.R;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.firebase.ai.FirebaseAI;
+import com.google.firebase.ai.GenerativeModel;
+import com.google.firebase.ai.java.ChatFutures;
+import com.google.firebase.ai.java.GenerativeModelFutures;
+import com.google.firebase.ai.type.Content;
+import com.google.firebase.ai.type.GenerateContentResponse;
+import com.google.firebase.ai.type.GenerationConfig;
+import com.google.firebase.ai.type.GenerativeBackend;
+import com.google.firebase.ai.type.ImagePart;
+import com.google.firebase.ai.type.Part;
+import com.google.firebase.ai.type.ResponseModality;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SuppressWarnings("unused")
+public final class GeminiDeveloperApiSnippetsJava {
+
+ private static final String TAG = "GeminiDeveloperApiSnippetsJava";
+
+ private GeminiDeveloperApiSnippetsJava() {}
+
+ static final class GeminiDeveloperApi25FlashModelConfigurationJava {
+ public static GenerativeModelFutures model;
+
+ static {
+ // [START android_gemini_developer_api_gemini_25_flash_model_java]
+ GenerativeModel firebaseAI = FirebaseAI.getInstance(GenerativeBackend.googleAI())
+ .generativeModel("gemini-2.5-flash");
+
+ GenerativeModelFutures model = GenerativeModelFutures.from(firebaseAI);
+ // [END android_gemini_developer_api_gemini_25_flash_model_java]
+ GeminiDeveloperApi25FlashModelConfigurationJava.model = model;
+ }
+ }
+
+ static final class Gemini25FlashImagePreviewModelConfigurationJava {
+ public static GenerativeModelFutures model;
+
+ static {
+ // [START android_gemini_developer_api_gemini_25_flash_image_model_java]
+ GenerativeModel ai = FirebaseAI.getInstance(GenerativeBackend.googleAI()).generativeModel(
+ "gemini-2.5-flash-image-preview",
+ // Configure the model to respond with text and images (required)
+ new GenerationConfig.Builder()
+ .setResponseModalities(Arrays.asList(ResponseModality.TEXT, ResponseModality.IMAGE))
+ .build()
+ );
+ GenerativeModelFutures model = GenerativeModelFutures.from(ai);
+ // [END android_gemini_developer_api_gemini_25_flash_image_model_java]
+ Gemini25FlashImagePreviewModelConfigurationJava.model = model;
+ }
+
+ }
+
+ public static void textOnlyInput(Executor executor) {
+ GenerativeModelFutures model = GeminiDeveloperApi25FlashModelConfigurationJava.model;
+ // [START android_gemini_developer_api_text_only_input_java]
+ Content prompt = new Content.Builder()
+ .addText("Write a story about a magic backpack.")
+ .build();
+
+ ListenableFuture response = model.generateContent(prompt);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ String resultText = result.getText();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_text_only_input_java]
+ }
+
+ public static void textAndImageInput(Executor executor, Bitmap bitmap) {
+ GenerativeModelFutures model = GeminiDeveloperApi25FlashModelConfigurationJava.model;
+ // [START android_gemini_developer_api_multimodal_input_java]
+ Content content = new Content.Builder()
+ .addImage(bitmap)
+ .addText("what is the object in the picture?")
+ .build();
+
+ ListenableFuture response = model.generateContent(content);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ String resultText = result.getText();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_multimodal_input_java]
+ }
+
+ public static void textAndAudioInput(Executor executor, Application applicationContext, Uri audioUri) {
+ GenerativeModelFutures model = GeminiDeveloperApi25FlashModelConfigurationJava.model;
+ // [START android_gemini_developer_api_multimodal_audio_input_java]
+ ContentResolver resolver = applicationContext.getContentResolver();
+
+ try (InputStream stream = resolver.openInputStream(audioUri)) {
+ File audioFile = new File(new URI(audioUri.toString()));
+ int audioSize = (int) audioFile.length();
+ byte[] audioBytes = new byte[audioSize];
+ if (stream != null) {
+ stream.read(audioBytes, 0, audioBytes.length);
+ stream.close();
+
+ // Provide a prompt that includes audio specified earlier and text
+ Content prompt = new Content.Builder()
+ .addInlineData(audioBytes, "audio/mpeg") // Specify the appropriate audio MIME type
+ .addText("Transcribe what's said in this audio recording.")
+ .build();
+
+ // To generate text output, call `generateContent` with the prompt
+ ListenableFuture response = model.generateContent(prompt);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ String text = result.getText();
+ Log.d(TAG, (text == null) ? "" : text);
+ }
+ @Override
+ public void onFailure(Throwable t) {
+ Log.e(TAG, "Failed to generate a response", t);
+ }
+ }, executor);
+ } else {
+ Log.e(TAG, "Error getting input stream for file.");
+ // Handle the error appropriately
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read the audio file", e);
+ } catch (URISyntaxException e) {
+ Log.e(TAG, "Invalid audio file", e);
+ }
+ // [END android_gemini_developer_api_multimodal_audio_input_java]
+ }
+
+ public static void textAndVideoInput(Executor executor, Application applicationContext, Uri videoUri) {
+ GenerativeModelFutures model = GeminiDeveloperApi25FlashModelConfigurationJava.model;
+ // [START android_gemini_developer_api_multimodal_video_input_java]
+ ContentResolver resolver = applicationContext.getContentResolver();
+
+ try (InputStream stream = resolver.openInputStream(videoUri)) {
+ File videoFile = new File(new URI(videoUri.toString()));
+ int videoSize = (int) videoFile.length();
+ byte[] videoBytes = new byte[videoSize];
+ if (stream != null) {
+ stream.read(videoBytes, 0, videoBytes.length);
+ stream.close();
+
+ // Provide a prompt that includes video specified earlier and text
+ Content prompt = new Content.Builder()
+ .addInlineData(videoBytes, "video/mp4")
+ .addText("Describe the content of this video")
+ .build();
+
+ // To generate text output, call generateContent with the prompt
+ ListenableFuture response = model.generateContent(prompt);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ String resultText = result.getText();
+ System.out.println(resultText);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ // [END android_gemini_developer_api_multimodal_video_input_java]
+ }
+
+ public static void multiTurnChat(Executor executor) {
+ GenerativeModelFutures model = GeminiDeveloperApi25FlashModelConfigurationJava.model;
+ // [START android_gemini_developer_api_multiturn_chat_java]
+ Content.Builder userContentBuilder = new Content.Builder();
+ userContentBuilder.setRole("user");
+ userContentBuilder.addText("Hello, I have 2 dogs in my house.");
+ Content userContent = userContentBuilder.build();
+
+ Content.Builder modelContentBuilder = new Content.Builder();
+ modelContentBuilder.setRole("model");
+ modelContentBuilder.addText("Great to meet you. What would you like to know?");
+ Content modelContent = modelContentBuilder.build();
+
+ List history = Arrays.asList(userContent, modelContent);
+
+ // Initialize the chat
+ ChatFutures chat = model.startChat(history);
+
+ // Create a new user message
+ Content.Builder messageBuilder = new Content.Builder();
+ messageBuilder.setRole("user");
+ messageBuilder.addText("How many paws are in my house?");
+
+ Content message = messageBuilder.build();
+
+ // Send the message
+ ListenableFuture response = chat.sendMessage(message);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ String resultText = result.getText();
+ System.out.println(resultText);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_multiturn_chat_java]
+ }
+
+ public static void generateImageFromText(Executor executor) {
+ GenerativeModelFutures model = Gemini25FlashImagePreviewModelConfigurationJava.model;
+ // [START android_gemini_developer_api_generate_image_from_text_java]
+ // Provide a text prompt instructing the model to generate an image
+ Content prompt = new Content.Builder()
+ .addText("Generate an image of the Eiffel Tower with fireworks in the background.")
+ .build();
+ // To generate an image, call `generateContent` with the text input
+ ListenableFuture response = model.generateContent(prompt);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ // iterate over all the parts in the first candidate in the result object
+ for (Part part : result.getCandidates().get(0).getContent().getParts()) {
+ if (part instanceof ImagePart) {
+ ImagePart imagePart = (ImagePart) part;
+ // The returned image as a bitmap
+ Bitmap generatedImageAsBitmap = imagePart.getImage();
+ break;
+ }
+ }
+ }
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_generate_image_from_text_java]
+ }
+
+ public static void editImage(Executor executor, Resources resources) {
+ GenerativeModelFutures model = Gemini25FlashImagePreviewModelConfigurationJava.model;
+ // [START android_gemini_developer_api_edit_image_java]
+ // Provide an image for the model to edit
+ Bitmap bitmap = BitmapFactory.decodeResource(resources, R.drawable.scones);
+ // Provide a text prompt instructing the model to edit the image
+ Content promptcontent = new Content.Builder()
+ .addImage(bitmap)
+ .addText("Edit this image to make it look like a cartoon")
+ .build();
+ // To edit the image, call `generateContent` with the prompt (image and text input)
+ ListenableFuture response = model.generateContent(promptcontent);
+ Futures.addCallback(response, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ // iterate over all the parts in the first candidate in the result object
+ for (Part part : result.getCandidates().get(0).getContent().getParts()) {
+ if (part instanceof ImagePart) {
+ ImagePart imagePart = (ImagePart) part;
+ Bitmap generatedImageAsBitmap = imagePart.getImage();
+ break;
+ }
+ }
+ }
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_edit_image_java]
+ }
+
+ public static void editImageWithChat(Executor executor, Resources resources) {
+ GenerativeModelFutures model = Gemini25FlashImagePreviewModelConfigurationJava.model;
+ // [START android_gemini_developer_api_edit_image_chat_java]
+ // Provide an image for the model to edit
+ Bitmap bitmap = BitmapFactory.decodeResource(resources, R.drawable.scones);
+ // Initialize the chat
+ ChatFutures chat = model.startChat();
+ // Create the initial prompt instructing the model to edit the image
+ Content prompt = new Content.Builder()
+ .setRole("user")
+ .addImage(bitmap)
+ .addText("Edit this image to make it look like a cartoon")
+ .build();
+ // To generate an initial response, send a user message with the image and text prompt
+ ListenableFuture response = chat.sendMessage(prompt);
+ // Extract the image from the initial response
+ ListenableFuture initialRequest = Futures.transform(response,
+ result -> {
+ for (Part part : result.getCandidates().get(0).getContent().getParts()) {
+ if (part instanceof ImagePart) {
+ ImagePart imagePart = (ImagePart) part;
+ return imagePart.getImage();
+ }
+ }
+ return null;
+ }, executor);
+ // Follow up requests do not need to specify the image again
+ ListenableFuture modelResponseFuture = Futures.transformAsync(
+ initialRequest,
+ generatedImage -> {
+ Content followUpPrompt = new Content.Builder()
+ .addText("But make it old-school line drawing style")
+ .build();
+ return chat.sendMessage(followUpPrompt);
+ }, executor);
+ // Add a final callback to check the reworked image
+ Futures.addCallback(modelResponseFuture, new FutureCallback() {
+ @Override
+ public void onSuccess(GenerateContentResponse result) {
+ for (Part part : result.getCandidates().get(0).getContent().getParts()) {
+ if (part instanceof ImagePart) {
+ ImagePart imagePart = (ImagePart) part;
+ Bitmap generatedImageAsBitmap = imagePart.getImage();
+ break;
+ }
+ }
+ }
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ }
+ }, executor);
+ // [END android_gemini_developer_api_edit_image_chat_java]
+ }
+}
diff --git a/misc/src/main/java/com/example/snippets/ai/ImagenSnippets.kt b/misc/src/main/java/com/example/snippets/ai/ImagenSnippets.kt
new file mode 100644
index 000000000..864fa339d
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/ai/ImagenSnippets.kt
@@ -0,0 +1,402 @@
+/*
+ * 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(PublicPreviewAPI::class)
+
+package com.example.snippets.ai
+
+import android.graphics.Bitmap
+import android.graphics.Paint
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.asAndroidPath
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.withTransform
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Dimensions
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenAspectRatio
+import com.google.firebase.ai.type.ImagenBackgroundMask
+import com.google.firebase.ai.type.ImagenControlReference
+import com.google.firebase.ai.type.ImagenControlType
+import com.google.firebase.ai.type.ImagenEditMode
+import com.google.firebase.ai.type.ImagenEditingConfig
+import com.google.firebase.ai.type.ImagenGenerationConfig
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenImagePlacement
+import com.google.firebase.ai.type.ImagenInlineImage
+import com.google.firebase.ai.type.ImagenMaskReference
+import com.google.firebase.ai.type.ImagenPersonFilterLevel
+import com.google.firebase.ai.type.ImagenRawImage
+import com.google.firebase.ai.type.ImagenSafetyFilterLevel
+import com.google.firebase.ai.type.ImagenSafetySettings
+import com.google.firebase.ai.type.ImagenStyleReference
+import com.google.firebase.ai.type.ImagenSubjectReference
+import com.google.firebase.ai.type.ImagenSubjectReferenceType
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.toImagenInlineImage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.min
+import android.graphics.Color as AndroidColor
+import androidx.compose.ui.graphics.Color as ComposeColor
+
+private object ImagenModelConfiguration {
+ // [START android_imagen_model_configuration]
+ val config = ImagenGenerationConfig(
+ numberOfImages = 2,
+ aspectRatio = ImagenAspectRatio.LANDSCAPE_16x9,
+ imageFormat = ImagenImageFormat.jpeg(compressionQuality = 100),
+ addWatermark = false,
+ )
+
+ // Initialize the Gemini Developer API backend service
+ // For Vertex AI use Firebase.ai(backend = GenerativeBackend.vertexAI())
+ val model = Firebase.ai(backend = GenerativeBackend.googleAI()).imagenModel(
+ modelName = "imagen-4.0-generate-001",
+ generationConfig = config,
+ safetySettings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ ),
+ )
+ // [END android_imagen_model_configuration]
+}
+
+private object ImagenVertexAIModelConfiguration {
+ // [START android_imagen_vertex_model_configuration]
+ val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI())
+ .imagenModel("imagen-3.0-capability-001")
+ // [END android_imagen_vertex_model_configuration]
+}
+
+private fun generateImagesWithImagen(scope: CoroutineScope) {
+ val model = ImagenModelConfiguration.model
+ scope.launch {
+ // [START android_imagen_generate_images]
+ val imageResponse = model.generateImages(
+ prompt = "A hyper realistic picture of a t-rex with a blue bagpack in a prehistoric forest",
+ )
+ val image = imageResponse.images.first()
+ val bitmapImage = image.asBitmap()
+ // [END android_imagen_generate_images]
+ }
+}
+
+// [START android_imagen_inpaint_insertion]
+suspend fun insertFlowersIntoImage(
+ model: ImagenModel,
+ originalImage: Bitmap,
+ mask: ImagenMaskReference
+): ImagenGenerationResponse {
+ val prompt = "a vase of flowers"
+
+ // Pass the original image, a mask, the prompt, and an editing configuration.
+ val editedImage = model.editImage(
+ referenceImages = listOf(
+ ImagenRawImage(originalImage.toImagenInlineImage()),
+ mask,
+ ),
+ prompt = prompt,
+ // Define the editing configuration for inpainting and insertion.
+ config = ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
+ )
+ return editedImage
+}
+// [END android_imagen_inpaint_insertion]
+
+// [START android_imagen_inpaint_removal]
+suspend fun removeBallFromImage(
+ model: ImagenModel,
+ originalImage: Bitmap,
+ mask: ImagenMaskReference
+): ImagenGenerationResponse {
+
+ // Optional: provide the prompt describing the content to be removed.
+ val prompt = "a ball"
+
+ // Pass the original image, a mask, the prompt, and an editing configuration.
+ val editedImage = model.editImage(
+ referenceImages = listOf(
+ ImagenRawImage(originalImage.toImagenInlineImage()),
+ mask
+ ),
+ prompt = prompt,
+ // Define the editing configuration for inpainting and removal.
+ config = ImagenEditingConfig(ImagenEditMode.INPAINT_REMOVAL)
+ )
+
+ return editedImage
+}
+// [END android_imagen_inpaint_removal]
+
+// [START android_imagen_editing_mask_editor]
+//import androidx.compose.ui.graphics.Color as ComposeColor
+
+@Composable
+fun ImagenEditingMaskEditor(
+ sourceBitmap: Bitmap,
+ onMaskFinalized: (Bitmap) -> Unit,
+) {
+
+ val paths = remember { mutableStateListOf() }
+ var currentPath by remember { mutableStateOf(null) }
+ var scale by remember { mutableFloatStateOf(1f) }
+ var offsetX by remember { mutableFloatStateOf(0f) }
+ var offsetY by remember { mutableFloatStateOf(0f) }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = { startOffset ->
+ val transformedStart = Offset(
+ (startOffset.x - offsetX) / scale,
+ (startOffset.y - offsetY) / scale,
+ )
+ currentPath = Path().apply { moveTo(transformedStart.x, transformedStart.y) }
+ },
+ onDrag = { change, _ ->
+ currentPath?.let {
+ val transformedChange = Offset(
+ (change.position.x - offsetX) / scale,
+ (change.position.y - offsetY) / scale,
+ )
+ it.lineTo(transformedChange.x, transformedChange.y)
+ currentPath = Path().apply { addPath(it) }
+ }
+ change.consume()
+ },
+ onDragEnd = {
+ currentPath?.let { paths.add(it) }
+ currentPath = null
+ },
+ )
+ },
+ ) {
+ Image(
+ bitmap = sourceBitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Fit,
+ )
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ val canvasWidth = size.width
+ val canvasHeight = size.height
+ val bitmapWidth = sourceBitmap.width.toFloat()
+ val bitmapHeight = sourceBitmap.height.toFloat()
+ scale = min(canvasWidth / bitmapWidth, canvasHeight / bitmapHeight)
+ offsetX = (canvasWidth - bitmapWidth * scale) / 2
+ offsetY = (canvasHeight - bitmapHeight * scale) / 2
+ withTransform(
+ {
+ translate(left = offsetX, top = offsetY)
+ scale(scale, scale, pivot = Offset.Zero)
+ },
+ ) {
+ val strokeWidth = 70f / scale
+ val stroke = Stroke(width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round)
+ val pathColor = ComposeColor.White.copy(alpha = 0.5f)
+ paths.forEach { path ->
+ drawPath(path = path, color = pathColor, style = stroke)
+ }
+ currentPath?.let { path ->
+ drawPath(path = path, color = pathColor, style = stroke)
+ }
+ }
+ }
+ }
+ Button(
+ onClick = {
+ val maskBitmap = createMaskBitmap(sourceBitmap, paths)
+ onMaskFinalized(maskBitmap)
+ },
+ ) {
+ Text("Save mask")
+ }
+ }
+}
+// [END android_imagen_editing_mask_editor]
+
+// [START android_imagen_editing_create_mask]
+// import android.graphics.Color as AndroidColor
+// import android.graphics.Paint
+
+private fun createMaskBitmap(
+ sourceBitmap: Bitmap,
+ paths: SnapshotStateList,
+): Bitmap {
+ val maskBitmap = Bitmap.createBitmap(sourceBitmap.width, sourceBitmap.height, Bitmap.Config.ARGB_8888)
+ val canvas = android.graphics.Canvas(maskBitmap)
+ val paint = Paint().apply {
+ color = AndroidColor.RED
+ strokeWidth = 70f
+ style = Paint.Style.STROKE
+ strokeCap = Paint.Cap.ROUND
+ strokeJoin = Paint.Join.ROUND
+ isAntiAlias = true
+ }
+ paths.forEach { path -> canvas.drawPath(path.asAndroidPath(), paint) }
+
+ return maskBitmap
+}
+// [END android_imagen_editing_create_mask]
+
+// [START android_imagen_expand_image]
+suspend fun expandImage(originalImage: Bitmap, imagenModel: ImagenModel): ImagenGenerationResponse {
+
+ // Optionally describe what should appear in the expanded area.
+ val prompt = "a sprawling sandy beach next to the ocean"
+
+ val editedImage = imagenModel.outpaintImage(
+ originalImage.toImagenInlineImage(),
+ Dimensions(1024, 1024),
+ prompt = prompt,
+ newPosition = ImagenImagePlacement.LEFT_CENTER
+ )
+
+
+ return editedImage
+}
+// [END android_imagen_expand_image]
+
+// [START android_imagen_replace_background]
+suspend fun replaceBackground(model: ImagenModel, originalImage: Bitmap): ImagenGenerationResponse {
+ // Provide the prompt describing the new background.
+ val prompt = "space background"
+
+ // Pass the original image, a mask, the prompt, and an editing configuration.
+ val editedImage = model.editImage(
+ referenceImages = listOf(
+ ImagenRawImage(originalImage.toImagenInlineImage()),
+ ImagenBackgroundMask(),
+ ),
+ prompt = prompt,
+ config = ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
+ )
+
+ return editedImage
+}
+// [END android_imagen_replace_background]
+
+// [START android_imagen_customize_subject]
+suspend fun customizeCatImage(model: ImagenModel, referenceCatImage: Bitmap): ImagenGenerationResponse {
+
+ // Define the subject reference using the reference image.
+ val subjectReference = ImagenSubjectReference(
+ image = referenceCatImage.toImagenInlineImage(),
+ referenceId = 1,
+ description = "cat",
+ subjectType = ImagenSubjectReferenceType.ANIMAL
+ )
+
+ // Provide a prompt that describes the final image.
+ // The "[1]" links the prompt to the subject reference with ID 1.
+ val prompt = "A cat[1] flying through outer space"
+
+ // Use the editImage API to perform the subject customization.
+ val editedImage = model.editImage(
+ referenceImages = listOf(subjectReference),
+ prompt = prompt,
+ config = ImagenEditingConfig(
+ editSteps = 50 // Number of editing steps, a higher value can improve quality
+ )
+ )
+
+ return editedImage
+}
+// [END android_imagen_customize_subject]
+
+// [START android_imagen_customize_control]
+suspend fun customizeCatImageByControl(model: ImagenModel, referenceImage: Bitmap): ImagenGenerationResponse {
+
+ // Define the subject reference using the reference image.
+ val controlReference = ImagenControlReference(
+ image = referenceImage.toImagenInlineImage(),
+ referenceId = 1,
+ type = ImagenControlType.SCRIBBLE,
+ )
+
+ val prompt = "A cat flying through outer space arranged like the scribble map[1]"
+
+ val editedImage = model.editImage(
+ referenceImages = listOf(controlReference),
+ prompt = prompt,
+ config = ImagenEditingConfig(
+ editSteps = 50
+ ),
+ )
+
+ return editedImage
+}
+// [END android_imagen_customize_control]
+
+// [START android_imagen_customize_style]
+suspend fun customizeImageByStyle(model: ImagenModel, referenceVanGoghImage: Bitmap): ImagenGenerationResponse {
+
+ // Define the style reference using the reference image.
+ val styleReference = ImagenStyleReference(
+ image = referenceVanGoghImage.toImagenInlineImage(),
+ referenceId = 1,
+ description = "Van Gogh style"
+ )
+
+ // Provide a prompt that describes the final image.
+ // The "1" links the prompt to the style reference with ID 1.
+ val prompt = "A cat flying through outer space, in the Van Gogh style[1]"
+
+ // Use the editImage API to perform the style customization.
+ val editedImage = model.editImage(
+ referenceImages = listOf(styleReference),
+ prompt = prompt,
+ config = ImagenEditingConfig(
+ editSteps = 50 // Number of editing steps, a higher value can improve quality
+ ),
+ )
+
+ return editedImage
+}
+// [END android_imagen_customize_style]
diff --git a/misc/src/main/java/com/example/snippets/ai/ImagenSnippetsJava.java b/misc/src/main/java/com/example/snippets/ai/ImagenSnippetsJava.java
new file mode 100644
index 000000000..2c6a12133
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/ai/ImagenSnippetsJava.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ai;
+
+import android.graphics.Bitmap;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.firebase.ai.FirebaseAI;
+import com.google.firebase.ai.java.ImagenModelFutures;
+import com.google.firebase.ai.type.GenerativeBackend;
+import com.google.firebase.ai.type.ImagenAspectRatio;
+import com.google.firebase.ai.type.ImagenGenerationConfig;
+import com.google.firebase.ai.type.ImagenGenerationResponse;
+import com.google.firebase.ai.type.ImagenImageFormat;
+import com.google.firebase.ai.type.ImagenInlineImage;
+import com.google.firebase.ai.type.ImagenPersonFilterLevel;
+import com.google.firebase.ai.type.ImagenSafetyFilterLevel;
+import com.google.firebase.ai.type.ImagenSafetySettings;
+import com.google.firebase.ai.type.PublicPreviewAPI;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+@SuppressWarnings("unused")
+@PublicPreviewAPI
+final class ImagenSnippetsJava {
+
+ private ImagenSnippetsJava() {}
+
+ static final class ImagenModelConfigurationJava {
+ public static ImagenModelFutures model;
+ }
+
+ static {
+ // [START android_imagen_model_configuration_java]
+ ImagenGenerationConfig config = new ImagenGenerationConfig.Builder()
+ .setNumberOfImages(2)
+ .setAspectRatio(ImagenAspectRatio.LANDSCAPE_16x9)
+ .setImageFormat(ImagenImageFormat.jpeg(100))
+ .setAddWatermark(false)
+ .build();
+
+ // For Vertex AI use Firebase.ai(backend = GenerativeBackend.vertexAI())
+ ImagenModelFutures model = ImagenModelFutures.from(
+ FirebaseAI.getInstance(GenerativeBackend.googleAI()).imagenModel(
+ "imagen-4.0-generate-001",
+ config,
+ new ImagenSafetySettings(
+ ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ ImagenPersonFilterLevel.BLOCK_ALL))
+ );
+ // [END android_imagen_model_configuration_java]
+ ImagenModelConfigurationJava.model = model;
+ }
+
+ public static void generateImagesWithImagen(Executor executor) {
+ ImagenModelFutures model = ImagenModelConfigurationJava.model;
+ // [START android_imagen_generate_images_java]
+ ListenableFuture> futureResponse =
+ model.generateImages(
+ "A hyper realistic picture of a t-rex with a blue bagpack in a prehistoric forest");
+
+ try {
+ ImagenGenerationResponse imageResponse = futureResponse.get();
+ List images = null;
+ if (imageResponse != null) {
+ images = imageResponse.getImages();
+ }
+ if (images != null && !images.isEmpty()) {
+ ImagenInlineImage image = images.get(0);
+ Bitmap bitmapImage = image.asBitmap();
+ // Use bitmapImage
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ // [END android_imagen_generate_images_java]
+ }
+}
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..d7fca9b2e
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsJava.java
@@ -0,0 +1,42 @@
+package com.example.snippets.backgroundwork;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.PowerManager;
+
+import androidx.annotation.Nullable;
+
+@SuppressWarnings("unused")
+public class WakeLockSnippetsJava extends Activity {
+
+ PowerManager.WakeLock wakeLock;
+ final long WAKELOCK_TIMEOUT = 10*60*1000L; // 10 minutes
+
+ @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(WAKELOCK_TIMEOUT);
+ // [END android_backgroundwork_wakelock_create_java]
+
+ super.onCreate(savedInstanceState);
+ }
+
+ @SuppressWarnings("unused")
+ // [START android_backgroundwork_wakelock_release_java]
+ void doSomethingAndRelease() throws MyException {
+ try {
+ wakeLock.acquire(WAKELOCK_TIMEOUT);
+ 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..2a2f3278a
--- /dev/null
+++ b/misc/src/main/java/com/example/snippets/backgroundwork/WakeLockSnippetsKotlin.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.os.Bundle
+import android.os.PowerManager
+
+// Snippets for doc page go here
+@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(POWER_SERVICE) as PowerManager).run {
+ newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyClassName::MyWakelockTag").apply {
+ acquire(WAKELOCK_TIMEOUT)
+ }
+ }
+ // [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(WAKELOCK_TIMEOUT)
+ doTheWork()
+ } finally {
+ release()
+ }
+ }
+ }
+ // [END android_backgroundwork_wakelock_release_kotlin]
+
+ private fun doTheWork() {
+ }
+}
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]
+ }
+}
diff --git a/misc/src/main/java/com/example/snippets/ui/theme/Type.kt b/misc/src/main/java/com/example/snippets/ui/theme/Type.kt
index f383a07ba..db2e63291 100644
--- a/misc/src/main/java/com/example/snippets/ui/theme/Type.kt
+++ b/misc/src/main/java/com/example/snippets/ui/theme/Type.kt
@@ -46,5 +46,5 @@ val Typography = Typography(
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
- */
+ */
)
diff --git a/misc/src/main/res/drawable/ic_launcher_background.xml b/misc/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9cb..e6202fbb1 100644
--- a/misc/src/main/res/drawable/ic_launcher_background.xml
+++ b/misc/src/main/res/drawable/ic_launcher_background.xml
@@ -1,4 +1,19 @@
+
+
-
\ No newline at end of file
+
diff --git a/misc/src/main/res/drawable/scones.xml b/misc/src/main/res/drawable/scones.xml
new file mode 100644
index 000000000..2eafb40af
--- /dev/null
+++ b/misc/src/main/res/drawable/scones.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/misc/src/main/res/layout/activity_main.xml b/misc/src/main/res/layout/activity_main.xml
index 59cbc1b45..dbfa14759 100644
--- a/misc/src/main/res/layout/activity_main.xml
+++ b/misc/src/main/res/layout/activity_main.xml
@@ -1,5 +1,19 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755bf..b8ff0f029 100644
--- a/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,21 @@
+
-
\ No newline at end of file
+
diff --git a/misc/src/main/res/values/colors.xml b/misc/src/main/res/values/colors.xml
index 61944aef2..e1f592422 100644
--- a/misc/src/main/res/values/colors.xml
+++ b/misc/src/main/res/values/colors.xml
@@ -1,4 +1,19 @@
+
#FFBB86FC
#FF6200EE
diff --git a/misc/src/main/res/values/strings.xml b/misc/src/main/res/values/strings.xml
index 870fc4736..681420d2b 100644
--- a/misc/src/main/res/values/strings.xml
+++ b/misc/src/main/res/values/strings.xml
@@ -1,3 +1,19 @@
+
+
Background Snippets
-
\ No newline at end of file
+
diff --git a/misc/src/main/res/values/themes.xml b/misc/src/main/res/values/themes.xml
index 65078ebe0..e2cf423e7 100644
--- a/misc/src/main/res/values/themes.xml
+++ b/misc/src/main/res/values/themes.xml
@@ -1,5 +1,20 @@
+
-
\ No newline at end of file
+
diff --git a/misc/src/main/res/xml/main_split_config.xml b/misc/src/main/res/xml/main_split_config.xml
index 2113dc80d..a7860fa14 100644
--- a/misc/src/main/res/xml/main_split_config.xml
+++ b/misc/src/main/res/xml/main_split_config.xml
@@ -1,5 +1,19 @@
-
+
-
\ No newline at end of file
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3ca8e6446..2a71512aa 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 {
@@ -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 {
@@ -14,7 +17,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()
}
@@ -22,11 +30,15 @@ dependencyResolutionManagement {
rootProject.name = "snippets"
include(
":bluetoothle",
- ":compose:recomposehighlighter",
- ":kotlin",
- ":compose:snippets",
- ":wear",
- ":views",
- ":misc",
- ":identity:credentialmanager",
+ ":compose:recomposehighlighter",
+ ":kotlin",
+ ":compose:snippets",
+ ":wear",
+ ":views",
+ ":misc",
+ ":identity:credentialmanager",
+ ":xr",
+ ":watchfacepush:validator",
+ ":kmp:androidApp",
+ ":kmp:shared"
)
diff --git a/spotless/copyright.kts b/spotless/copyright.kts
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/spotless/copyright.kts
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR 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/spotless/copyright.xml b/spotless/copyright.xml
new file mode 100644
index 000000000..9cb1e5942
--- /dev/null
+++ b/spotless/copyright.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/views/build.gradle.kts b/views/build.gradle.kts
index 80ee4edc8..623add7ab 100644
--- a/views/build.gradle.kts
+++ b/views/build.gradle.kts
@@ -19,10 +19,10 @@ plugins {
android {
namespace = "com.example.example.snippet.views"
- compileSdk = 35
+ compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
- minSdk = 35
+ minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -38,11 +38,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
}
diff --git a/views/src/main/AndroidManifest.xml b/views/src/main/AndroidManifest.xml
index 65ba8e1e0..cb8f943f4 100644
--- a/views/src/main/AndroidManifest.xml
+++ b/views/src/main/AndroidManifest.xml
@@ -1,20 +1,19 @@
-
\ No newline at end of file
+
diff --git a/views/src/main/java/insets/SystemBarProtectionSnippet.kt b/views/src/main/java/insets/SystemBarProtectionSnippet.kt
index d4774fd2f..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,
@@ -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
)
)
)
diff --git a/views/src/main/res/layout/system_bar_protection.xml b/views/src/main/res/layout/system_bar_protection.xml
index f70ccd21f..d230aab10 100644
--- a/views/src/main/res/layout/system_bar_protection.xml
+++ b/views/src/main/res/layout/system_bar_protection.xml
@@ -1,22 +1,19 @@
-
-
\ No newline at end of file
+
diff --git a/views/src/main/res/layout/widget_preview.xml b/views/src/main/res/layout/widget_preview.xml
index c9f449bb1..356458236 100644
--- a/views/src/main/res/layout/widget_preview.xml
+++ b/views/src/main/res/layout/widget_preview.xml
@@ -1,24 +1,22 @@
-
\ No newline at end of file
+
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..7cfb5372d
--- /dev/null
+++ b/watchfacepush/validator/build.gradle.kts
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+
+group = "com.example.validator"
+version = "1.0"
+
+plugins {
+ kotlin("jvm")
+ application
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(17))
+ }
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+application {
+ mainClass.set("com.example.validator.Main")
+
+}
+
+sourceSets {
+ named("main") {
+ java {
+ srcDir("src/main/java")
+ }
+ }
+
+}
+
+dependencies {
+ implementation(libs.validator.push)
+}
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..c1aa20ef0
--- /dev/null
+++ b/watchfacepush/validator/src/main/kotlin/com/example/validator/Main.kt
@@ -0,0 +1,75 @@
+/*
+ * 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
+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
+}
diff --git a/watchfacepush/validator/src/main/resources/watchface.apk b/watchfacepush/validator/src/main/resources/watchface.apk
new file mode 100644
index 000000000..d9bd51c3f
Binary files /dev/null and b/watchfacepush/validator/src/main/resources/watchface.apk differ
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
index 154e7d37e..68d64d8dd 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,16 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
+ kotlinOptions {
+ jvmTarget = "17"
+ }
}
dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.ui)
+ implementation(libs.androidx.wear.input)
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
@@ -57,8 +64,11 @@ 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)
implementation(libs.androidx.protolayout.expression)
debugImplementation(libs.androidx.tiles.renderer)
testImplementation(libs.androidx.tiles.testing)
@@ -69,15 +79,20 @@ 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.androidx.fragment.ktx)
+ implementation(libs.wear.compose.material)
+ implementation(libs.wear.compose.material3)
implementation(libs.compose.foundation)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)
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)
debugImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(libs.junit)
diff --git a/wear/lint.xml b/wear/lint.xml
index 44fac75b8..51fd99c77 100644
--- a/wear/lint.xml
+++ b/wear/lint.xml
@@ -1,8 +1,23 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
index 84c4785a2..9fd910e5d 100644
--- a/wear/src/main/AndroidManifest.xml
+++ b/wear/src/main/AndroidManifest.xml
@@ -1,7 +1,25 @@
+
+
+
+
@@ -24,7 +42,18 @@
android:value="true" />
+
+
+
+
+
+
+
@@ -35,6 +64,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ 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..0f936b5cb
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnActivity.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.Intent
+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.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.wear.compose.foundation.lazy.AutoCenteringParams
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+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 runningService by rememberSaveable { mutableStateOf?>(null) }
+ val listState = rememberScalingLazyListState()
+
+ MaterialTheme(
+ colorScheme = dynamicColorScheme(LocalContext.current) ?: MaterialTheme.colorScheme
+ ) {
+ AmbientAware { ambientState ->
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ autoCentering = AutoCenteringParams(itemIndex = 0)
+ ) {
+ item {
+ Text(text = "Elapsed Time", style = MaterialTheme.typography.titleLarge)
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ item {
+ ElapsedTime(ambientState = ambientState)
+ }
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ val services = listOf(
+ AlwaysOnService1::class.java,
+ AlwaysOnService2::class.java,
+ AlwaysOnService3::class.java
+ )
+
+ items(services.size) { index ->
+ val serviceClass = services[index]
+ val isRunning = runningService == serviceClass
+ SwitchButton(
+ checked = isRunning,
+ onCheckedChange = { newState ->
+ if (newState) {
+ if (runningService != null) {
+ Log.d(TAG, "Stopping ${runningService?.simpleName}")
+ context.stopService(Intent(context, runningService))
+ }
+ Log.d(TAG, "Starting ${serviceClass.simpleName}")
+ val intent = Intent(context, serviceClass)
+ context.startForegroundService(intent)
+ runningService = serviceClass
+ } else {
+ Log.d(TAG, "Stopping ${serviceClass.simpleName}")
+ context.stopService(Intent(context, serviceClass))
+ runningService = null
+ }
+ },
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+ ) {
+ Text(
+ text = "Ongoing Activity ${index + 1}",
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+ }
+ }
+ }
+ }
+}
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..d52db8fd8
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/alwayson/AlwaysOnService.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.SystemClock
+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
+
+abstract class AlwaysOnServiceBase : LifecycleService() {
+
+ private val notificationManager by lazy { getSystemService() }
+
+ companion object {
+ private const val TAG = "AlwaysOnService"
+ const val NOTIFICATION_ID = 1001
+ const val CHANNEL_ID = "always_on_service_channel"
+ private const val CHANNEL_NAME = "Always On Service"
+
+ @Volatile
+ var isRunning = false
+ private set
+ }
+
+ 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")
+
+ createNotification()
+
+ 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")
+ }
+
+ abstract fun createNotification()
+}
+
+class AlwaysOnService1 : AlwaysOnServiceBase() {
+ override fun createNotification() {
+ // Creates an ongoing activity that demonstrates how to link the touch intent to the always-on activity.
+ // [START android_wear_ongoing_activity_create_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]
+ 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)
+
+ val notification = notificationBuilder.build()
+ // [END android_wear_ongoing_activity_create_notification]
+
+ startForeground(NOTIFICATION_ID, notification)
+ }
+}
+
+class AlwaysOnService2 : AlwaysOnServiceBase() {
+ override fun createNotification() {
+ // Creates an ongoing activity with a static status text
+
+ // [START android_wear_ongoing_activity_notification_builder]
+ // Create a PendingIntent to pass to the notification builder
+ val pendingIntent =
+ PendingIntent.getActivity(
+ this,
+ 0,
+ Intent(this, AlwaysOnActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Always On Service")
+ .setContentText("Service is running in background")
+ .setSmallIcon(R.drawable.animated_walk)
+ // Category helps the system prioritize the ongoing activity
+ .setCategory(NotificationCompat.CATEGORY_WORKOUT)
+ .setContentIntent(pendingIntent)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOngoing(true) // Important!
+ // [END android_wear_ongoing_activity_notification_builder]
+
+ // [START android_wear_ongoing_activity_builder]
+ val ongoingActivity =
+ OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
+ // Sets the icon that appears on the watch face in active mode.
+ .setAnimatedIcon(R.drawable.animated_walk)
+ // Sets the icon that appears on the watch face in ambient mode.
+ .setStaticIcon(R.drawable.ic_walk)
+ // Sets the tap target to bring the user back to the app.
+ .setTouchIntent(pendingIntent)
+ .build()
+ // [END android_wear_ongoing_activity_builder]
+
+ // [START android_wear_ongoing_activity_post_notification]
+ // This call modifies notificationBuilder to include the ongoing activity data.
+ ongoingActivity.apply(applicationContext)
+
+ // Post the notification.
+ startForeground(NOTIFICATION_ID, notificationBuilder.build())
+ // [END android_wear_ongoing_activity_post_notification]
+ }
+}
+
+class AlwaysOnService3 : AlwaysOnServiceBase() {
+ override fun createNotification() {
+ // Creates an ongoing activity that demonstrates dynamic status text (a timer)
+ 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)
+ .setContentTitle("Always On Service")
+ .setContentText("Service is running in background")
+ .setSmallIcon(R.drawable.animated_walk)
+ // Category helps the system prioritize the ongoing activity
+ .setCategory(NotificationCompat.CATEGORY_WORKOUT)
+ .setContentIntent(pendingIntent)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setOngoing(true) // Important!
+
+ // [START android_wear_ongoing_activity_create_status]
+ // Define a template with placeholders for the activity type and the timer.
+ val statusTemplate = "#type# for #time#"
+
+ // Set the start time for a stopwatch.
+ // Use SystemClock.elapsedRealtime() for time-based parts.
+ val runStartTime = SystemClock.elapsedRealtime()
+
+ val ongoingActivityStatus = Status.Builder()
+ // Sets the template string.
+ .addTemplate(statusTemplate)
+ // Fills the #type# placeholder with a static text part.
+ .addPart("type", Status.TextPart("Run"))
+ // Fills the #time# placeholder with a stopwatch part.
+ .addPart("time", Status.StopwatchPart(runStartTime))
+ .build()
+ // [END android_wear_ongoing_activity_create_status]
+
+ // [START android_wear_ongoing_activity_set_status]
+ val ongoingActivity =
+ OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder)
+ // [START_EXCLUDE]
+ .setAnimatedIcon(R.drawable.animated_walk)
+ .setStaticIcon(R.drawable.ic_walk)
+ .setTouchIntent(pendingIntent)
+ // [END_EXCLUDE]
+ // Add the status to the OngoingActivity.
+ .setStatus(ongoingActivityStatus)
+ .build()
+ // [END android_wear_ongoing_activity_set_status]
+
+ ongoingActivity.apply(applicationContext)
+ startForeground(NOTIFICATION_ID, notificationBuilder.build())
+ }
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/audio/AudioActivity.kt b/wear/src/main/java/com/example/wear/snippets/audio/AudioActivity.kt
new file mode 100644
index 000000000..5ba03df50
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/audio/AudioActivity.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.audio
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.media.AudioDeviceCallback
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import androidx.activity.ComponentActivity
+import androidx.annotation.OptIn
+import androidx.media3.common.AudioAttributes
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.WearUnsuitableOutputPlaybackSuppressionResolverListener
+
+class AudioActivity : ComponentActivity() {
+
+ // [START android_wear_audio_detect_devices]
+ private val audioManager: AudioManager by lazy {
+ getSystemService(AUDIO_SERVICE) as AudioManager
+ }
+
+ fun audioOutputAvailable(type: Int): Boolean {
+ if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
+ return false
+ }
+ return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).any { it.type == type }
+ }
+ // [END android_wear_audio_detect_devices]
+
+ @OptIn(UnstableApi::class)
+ fun buildExoPlayer(context: Context): ExoPlayer {
+ // [START android_wear_exoplayer_audio_output_suppression]
+ val exoPlayer = ExoPlayer.Builder(context)
+ .setAudioAttributes(AudioAttributes.DEFAULT, true)
+ .setSuppressPlaybackOnUnsuitableOutput(true)
+ .build()
+ // [END android_wear_exoplayer_audio_output_suppression]
+ // [START android_wear_exoplayer_audio_output_suppression_listener]
+ exoPlayer.addListener(WearUnsuitableOutputPlaybackSuppressionResolverListener(context))
+ // [END android_wear_exoplayer_audio_output_suppression_listener]
+ return exoPlayer
+ }
+
+ override fun onCreate(savedInstanceState: android.os.Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // [START android_wear_audio_detect_devices_sample]
+ val hasSpeaker = audioOutputAvailable(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
+ val hasBluetoothHeadset = audioOutputAvailable(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP)
+ val hasBLEBroadcast = audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_BROADCAST)
+ val hasBLEHeadset = audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_HEADSET)
+ val hasBLESpeaker = audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_SPEAKER)
+ // [END android_wear_audio_detect_devices_sample]
+ println("Has speaker: $hasSpeaker")
+ println("Has Bluetooth headset: $hasBluetoothHeadset")
+ println("Has BLE broadcast: $hasBLEBroadcast")
+ println("Has BLE headset: $hasBLEHeadset")
+ println("Has BLE speaker: $hasBLESpeaker")
+
+ // [START android_wear_audio_register_callback]
+ val audioDeviceCallback =
+ object : AudioDeviceCallback() {
+ override fun onAudioDevicesAdded(addedDevices: Array?) {
+ super.onAudioDevicesAdded(addedDevices)
+ if (audioOutputAvailable(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) ||
+ audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_BROADCAST) ||
+ audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_HEADSET) ||
+ audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_SPEAKER)
+ ) {
+ // A Bluetooth or BLE device is connected and available for playback.
+ }
+ }
+ override fun onAudioDevicesRemoved(removedDevices: Array?) {
+ super.onAudioDevicesRemoved(removedDevices)
+ if (!(audioOutputAvailable(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP)) &&
+ !(audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_BROADCAST)) &&
+ !(audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_HEADSET)) &&
+ !(audioOutputAvailable(AudioDeviceInfo.TYPE_BLE_SPEAKER))
+ ) {
+ // No Bluetooth or BLE devices are connected anymore.
+ }
+ }
+ }
+
+ audioManager.registerAudioDeviceCallback(audioDeviceCallback, /*handler=*/ null)
+ // [END android_wear_audio_register_callback]
+ }
+}
diff --git a/wear/src/main/java/com/example/wear/snippets/audio/BluetoothSettings.kt b/wear/src/main/java/com/example/wear/snippets/audio/BluetoothSettings.kt
new file mode 100644
index 000000000..d2ebbd13e
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/audio/BluetoothSettings.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.audio
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+
+object BluetoothSettings {
+ // [START android_wear_bluetooth_settings]
+ fun Context.launchBluetoothSettings(closeOnConnect: Boolean = true) {
+ val intent = with(Intent(Settings.ACTION_BLUETOOTH_SETTINGS)) {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ putExtra("EXTRA_CONNECTION_ONLY", true)
+ if (closeOnConnect) {
+ putExtra("EXTRA_CLOSE_ON_CONNECT", true)
+ }
+ putExtra("android.bluetooth.devicepicker.extra.FILTER_TYPE", FILTER_TYPE_AUDIO)
+ }
+ startActivity(intent)
+ }
+
+ internal const val FILTER_TYPE_AUDIO = 1
+ // [END android_wear_bluetooth_settings]
+}
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..2ff8758fd
--- /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_CANCELED to cancel configuration
+ finish()
+ // [END android_wear_complication_configuration_finish]
+ }) {
+ Text("Done!")
+ }
+ }
+ }
+}
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..1b4263604
--- /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 the 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, and title.
+ .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/java/com/example/wear/snippets/hardwarebuttons/HardwareButtonsActivity.kt b/wear/src/main/java/com/example/wear/snippets/hardwarebuttons/HardwareButtonsActivity.kt
new file mode 100644
index 000000000..e9c8b471c
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/hardwarebuttons/HardwareButtonsActivity.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.hardwarebuttons
+
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import android.view.KeyEvent
+import androidx.activity.ComponentActivity
+import androidx.wear.input.WearableButtons
+
+class HardwareButtonsActivity : ComponentActivity() {
+ // [START android_wear_hardware_buttons_events]
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ return if (event?.repeatCount == 0) {
+ when (keyCode) {
+ KeyEvent.KEYCODE_STEM_1 -> {
+ Log.d(TAG, "KEYCODE_STEM_1 pressed")
+ true
+ }
+ KeyEvent.KEYCODE_STEM_2 -> {
+ Log.d(TAG, "KEYCODE_STEM_2 pressed")
+ true
+ }
+ else -> {
+ super.onKeyDown(keyCode, event)
+ }
+ }
+ } else {
+ super.onKeyDown(keyCode, event)
+ }
+ }
+ // [END android_wear_hardware_buttons_events]
+
+ fun hardwareButtonsCount(context: Context, activity: Activity) {
+ // [START android_wear_hardware_buttons_count]
+ val count = WearableButtons.getButtonCount(context)
+
+ if (count > 1) {
+ Log.d(TAG, "More than one button available")
+ }
+
+ val buttonInfo = WearableButtons.getButtonInfo(
+ activity,
+ KeyEvent.KEYCODE_STEM_1
+ )
+
+ if (buttonInfo == null) {
+ // KEYCODE_STEM_1 is unavailable
+ Log.d(TAG, "KEYCODE_STEM_1 not available")
+ } else {
+ // KEYCODE_STEM_1 is present on the device
+ Log.d(TAG, "KEYCODE_STEM_1 is present on the device")
+ }
+ // [END android_wear_hardware_buttons_count]
+ }
+}
+private const val TAG = "HardwareButtons"
diff --git a/wear/src/main/java/com/example/wear/snippets/location/LocationActivity.kt b/wear/src/main/java/com/example/wear/snippets/location/LocationActivity.kt
new file mode 100644
index 000000000..c51c6ef93
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/location/LocationActivity.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.location
+
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import com.google.android.horologist.compose.layout.AppScaffold
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+
+
+// [START android_wear_location]
+class LocationActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // [START_EXCLUDE]
+ setContent {
+ WearApp(hasGps = { hasGps() })
+ }
+ // [END_EXCLUDE]
+ }
+ fun hasGps(): Boolean =
+ packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS)
+}
+// [END android_wear_location]
+
+@Composable
+fun WearApp(hasGps: () -> Boolean) {
+
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.Button,
+ )
+ AppScaffold {
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ if (!hasGps()) {
+ Text(text = "This hardware doesn't have GPS")
+ // Fall back to functionality that doesn't use location or
+ // warn the user that location function isn't available.
+ }
+ else {
+ Text(text = "This hardware has GPS")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Mdc3ThemeSnippets.kt b/wear/src/main/java/com/example/wear/snippets/m3/MainActivity.kt
similarity index 54%
rename from compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Mdc3ThemeSnippets.kt
rename to wear/src/main/java/com/example/wear/snippets/m3/MainActivity.kt
index 5e3e4000f..5f33d63d1 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Mdc3ThemeSnippets.kt
+++ b/wear/src/main/java/com/example/wear/snippets/m3/MainActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,25 +14,27 @@
* limitations under the License.
*/
-package com.example.compose.snippets.designsystems
+package com.example.wear.snippets.m3
import android.os.Bundle
+import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-// [START android_compose_designsystems_interop_mdc3theme]
-import com.google.accompanist.themeadapter.material3.Mdc3Theme
+import androidx.compose.runtime.Composable
+import com.example.wear.snippets.m3.list.ComposeList
+import com.example.wear.snippets.m3.pager.HorizontalPager
-class Mdc3ThemeExample : AppCompatActivity() {
+class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // Use Mdc3Theme instead of M3 MaterialTheme
- // Color scheme, typography, and shapes have been read from the
- // View-based theme used in this Activity
+
setContent {
- Mdc3Theme {
- // Your app-level composable here
- }
+ WearApp()
}
}
}
-// [END android_compose_designsystems_interop_mdc3theme]
+
+@Composable
+fun WearApp() {
+ // insert here the snippet you want to test
+ HorizontalPager()
+}
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..5e1e4c12f
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/navigation/Navigation.kt
@@ -0,0 +1,146 @@
+/*
+ * 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")!!)
+ }
+ }
+ }
+}
+
+// Implementation of one of the screens in the navigation
+@Composable
+fun MessageDetail(id: String) {
+ // .. Screen level content goes here
+ val scrollState = rememberTransformingLazyColumnState()
+
+ val padding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.BodyText
+ )
+
+ ScreenScaffold(
+ scrollState = scrollState,
+ contentPadding = padding
+ ) { scaffoldPaddingValues ->
+ // Screen content goes here
+ // [START_EXCLUDE]
+ TransformingLazyColumn(
+ state = scrollState,
+ contentPadding = scaffoldPaddingValues
+ ) {
+ item {
+ Text(
+ text = id,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ // [END_EXCLUDE]
+ // [END android_wear_navigation]
+ }
+}
+
+@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/pager/Pager.kt b/wear/src/main/java/com/example/wear/snippets/m3/pager/Pager.kt
new file mode 100644
index 000000000..38e68513f
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/pager/Pager.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.m3.pager
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.material3.AnimatedPage
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.HorizontalPagerScaffold
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.Text
+import com.google.android.horologist.compose.layout.ColumnItemType
+import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding
+
+@Composable
+fun HorizontalPager() {
+ // [START android_wear_horizontal_pager]
+ AppScaffold {
+ val pagerState = rememberPagerState(pageCount = { 10 })
+ val columnState = rememberTransformingLazyColumnState()
+ val contentPadding = rememberResponsiveColumnPadding(
+ first = ColumnItemType.ListHeader,
+ last = ColumnItemType.BodyText,
+ )
+ HorizontalPagerScaffold(pagerState = pagerState) {
+ HorizontalPager(
+ state = pagerState,
+ ) { page ->
+ AnimatedPage(pageIndex = page, pagerState = pagerState) {
+ ScreenScaffold(
+ scrollState = columnState,
+ contentPadding = contentPadding
+ ) { contentPadding ->
+ TransformingLazyColumn(
+ state = columnState,
+ contentPadding = contentPadding
+ ) {
+ item {
+ ListHeader(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "Pager sample")
+ }
+ }
+ item {
+ if (page == 0) {
+ Text(text = "Page #$page. Swipe right")
+ }
+ else{
+ Text(text = "Page #$page. Swipe left and right")
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+ // [END android_wear_horizontal_pager]
+}
\ No newline at end of file
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..4cff56480
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/rotary/Rotary.kt
@@ -0,0 +1,235 @@
+/*
+ * 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(
+ scrollState = listState,
+ 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(
+ scrollState = listState,
+ 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/Interaction.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt
new file mode 100644
index 000000000..29d63d219
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Interaction.kt
@@ -0,0 +1,273 @@
+/*
+ * 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() {
+
+ // [START android_wear_m3_interaction_loadaction_request]
+ override fun onTileRequest(
+ requestParams: RequestBuilders.TileRequest
+ ): ListenableFuture {
+
+ // When triggered by loadAction(), "name" will be "Javier", and "age" will
+ // be 37.
+ with(requestParams.currentState.stateMap) {
+ val name = this[stringAppDataKey("name")]
+ val age = this[intAppDataKey("age")]
+ }
+
+ // [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_loadaction_request]
+
+ 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/Tile.kt b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt
new file mode 100644
index 000000000..5299b685d
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/Tile.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.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
+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"
+
+// [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]
+
+class TileBreakpoints : TileService() {
+
+ override fun onTileRequest(requestParams: RequestBuilders.TileRequest) =
+ Futures.immediateFuture(
+ 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()
+ )
+}
+
+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) {
+ myAdaptiveLayout() // varies the layout depending on the size of the screen
+ }
+ )
+ .build()
+}
+// [END android_wear_tile_preview]
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..e9d4771f0
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/m3/tile/TileActivity.kt
@@ -0,0 +1,183 @@
+/*
+ * 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/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/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt
new file mode 100644
index 000000000..dc8591aed
--- /dev/null
+++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt
@@ -0,0 +1,414 @@
+/*
+ * 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.ResourceBuilders
+import androidx.wear.protolayout.ResourceBuilders.Resources
+import androidx.wear.protolayout.TimelineBuilders.Timeline
+import androidx.wear.protolayout.TriggerBuilders.createOnVisibleTrigger
+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.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
+
+// In a real implementation, this needs to change whenever the resources
+// change, to ensure the updated resources are loaded.
+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]
+ }
+}
+
+// [START android_wear_tile_animations_lottie]
+class LottieAnimation : 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 {
+
+ 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]
+
+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/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt
index 58cbaa758..e0a39de99 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,16 +16,33 @@
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.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.LayoutElementBuilders.DashedArcLine
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.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
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
private const val RESOURCES_VERSION = "1"
@@ -48,10 +65,186 @@ 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]
+}
+
+class FeatureFallback : TileService() {
+ override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture {
+
+ // [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()
+ )
+ }
}
-// [END android_wear_tile_mytileservice]
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/wear/src/main/res/drawable/animated_walk.xml b/wear/src/main/res/drawable/animated_walk.xml
new file mode 100644
index 000000000..b9e7fa835
--- /dev/null
+++ b/wear/src/main/res/drawable/animated_walk.xml
@@ -0,0 +1,682 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..52886647c
--- /dev/null
+++ b/wear/src/main/res/drawable/complication_icon.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/wear/src/main/res/drawable/ic_launcher_background.xml b/wear/src/main/res/drawable/ic_launcher_background.xml
index ca3826a46..67a0a8f0d 100644
--- a/wear/src/main/res/drawable/ic_launcher_background.xml
+++ b/wear/src/main/res/drawable/ic_launcher_background.xml
@@ -1,4 +1,19 @@
+
+
-
\ No newline at end of file
+
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..0d0bf8dca
--- /dev/null
+++ b/wear/src/main/res/drawable/ic_walk.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
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 000000000..ce8b17920
Binary files /dev/null and b/wear/src/main/res/drawable/lottie_fallback.png differ
diff --git a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index bbd3e0212..8ebcf9c97 100644
--- a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,20 @@
+
-
\ No newline at end of file
+
diff --git a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index bbd3e0212..8ebcf9c97 100644
--- a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,20 @@
+
-
\ No newline at end of file
+
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": {}
+}
diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml
index fc59c67b8..d0ddb884a 100644
--- a/wear/src/main/res/values/strings.xml
+++ b/wear/src/main/res/values/strings.xml
@@ -1,8 +1,28 @@
+
+
Wear Snippets
Voice Input
Voice Text Entry
Message List
+ Message Detail
Hello Tile
Hello Tile Description
-
\ No newline at end of file
+ My Complication
+ My Timeline Complication
+ Configuration activity
+
diff --git a/xr/.gitignore b/xr/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/xr/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts
new file mode 100644
index 000000000..9b632756d
--- /dev/null
+++ b/xr/build.gradle.kts
@@ -0,0 +1,69 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.example.xr"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "com.example.xr"
+ minSdk = 34
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = "1.0"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ jvmToolchain(17)
+ }
+ buildFeatures {
+ compose = true
+ }
+ lint {
+ disable += "RestrictedApi"
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.xr.arcore)
+ implementation(libs.androidx.xr.scenecore)
+ implementation(libs.androidx.xr.compose)
+
+ implementation(libs.androidx.activity.ktx)
+
+ implementation(libs.androidx.media3.exoplayer)
+
+ val composeBom = platform(libs.androidx.compose.bom)
+ implementation(composeBom)
+
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.util)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.graphics.shapes)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.ui.viewbinding)
+ implementation(libs.androidx.paging.compose)
+ implementation(libs.androidx.compose.animation.graphics)
+
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.material3.adaptive)
+ implementation(libs.androidx.compose.material3.adaptive.layout)
+ implementation(libs.androidx.compose.material3.adaptive.navigation)
+ implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
+ implementation(libs.androidx.compose.material)
+
+ implementation(libs.androidx.compose.runtime)
+ implementation(libs.androidx.compose.runtime.livedata)
+ implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.compose.material.ripple)
+ implementation(libs.androidx.constraintlayout.compose)
+
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.appcompat)
+}
\ No newline at end of file
diff --git a/xr/src/main/AndroidManifest.xml b/xr/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..bc726787c
--- /dev/null
+++ b/xr/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/xr/src/main/java/com/example/xr/arcore/AnchorPersistence.kt b/xr/src/main/java/com/example/xr/arcore/AnchorPersistence.kt
new file mode 100644
index 000000000..e3d38c0f8
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/arcore/AnchorPersistence.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.xr.arcore.Anchor
+import androidx.xr.arcore.AnchorCreateSuccess
+import androidx.xr.runtime.Session
+import java.util.UUID
+
+private suspend fun persistAnchor(anchor: Anchor) {
+ // [START androidxr_arcore_anchor_persist]
+ val uuid = anchor.persist()
+ // [END androidxr_arcore_anchor_persist]
+}
+
+private fun loadAnchor(session: Session, uuid: UUID) {
+ // [START androidxr_arcore_anchor_load]
+ when (val result = Anchor.load(session, uuid)) {
+ is AnchorCreateSuccess -> {
+ // Loading was successful. The anchor is stored in result.anchor.
+ }
+ else -> {
+ // handle failure
+ }
+ }
+ // [END androidxr_arcore_anchor_load]
+}
+
+private fun unpersistAnchor(session: Session, uuid: UUID) {
+ // [START androidxr_arcore_anchor_unpersist]
+ Anchor.unpersist(session, uuid)
+ // [END androidxr_arcore_anchor_unpersist]
+}
+
+private fun getPersistedAnchorUuids(session: Session) {
+ // [START androidxr_arcore_anchor_get_uuids]
+ val uuids = Anchor.getPersistedAnchorUuids(session)
+ // [END androidxr_arcore_anchor_get_uuids]
+}
diff --git a/xr/src/main/java/com/example/xr/arcore/Anchors.kt b/xr/src/main/java/com/example/xr/arcore/Anchors.kt
new file mode 100644
index 000000000..ad8ac26a6
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/arcore/Anchors.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.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.SessionConfigureSuccess
+import androidx.xr.runtime.math.Pose
+import androidx.xr.scenecore.AnchorEntity
+import androidx.xr.scenecore.Entity
+import androidx.xr.scenecore.scene
+
+fun configureAnchoring(session: Session) {
+ // [START androidxr_arcore_anchoring_configure]
+ val newConfig = session.config.copy(
+ anchorPersistence = Config.AnchorPersistenceMode.LOCAL,
+ )
+ when (val result = session.configure(newConfig)) {
+ is SessionConfigureSuccess -> TODO(/* Success! */)
+ else ->
+ TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */)
+ }
+ // [END androidxr_arcore_anchoring_configure]
+}
+
+private fun createAnchorAtPose(session: Session, pose: Pose) {
+ val pose = Pose()
+ // [START androidxr_arcore_anchor_create]
+ when (val result = Anchor.create(session, pose)) {
+ is AnchorCreateSuccess -> { /* anchor stored in `result.anchor`. */ }
+ else -> { /* handle failure */ }
+ }
+ // [END androidxr_arcore_anchor_create]
+}
+
+private fun createAnchorAtTrackable(trackable: Trackable<*>) {
+ val pose = Pose()
+ // [START androidxr_arcore_anchor_create_trackable]
+ when (val result = trackable.createAnchor(pose)) {
+ is AnchorCreateSuccess -> { /* anchor stored in `result.anchor`. */ }
+ else -> { /* handle failure */ }
+ }
+ // [END androidxr_arcore_anchor_create_trackable]
+}
+
+private fun attachEntityToAnchor(
+ session: Session,
+ entity: Entity,
+ anchor: Anchor
+) {
+ // [START androidxr_arcore_entity_tracks_anchor]
+ AnchorEntity.create(session, anchor).apply {
+ 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
new file mode 100644
index 000000000..3c0fd82fb
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt
@@ -0,0 +1,140 @@
+/*
+ * 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 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.Session
+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
+
+fun ComponentActivity.configureSession(session: Session) {
+ // [START androidxr_arcore_hand_configure]
+ val newConfig = session.config.copy(
+ handTracking = Config.HandTrackingMode.BOTH
+ )
+ when (val result = session.configure(newConfig)) {
+ is SessionConfigureSuccess -> TODO(/* Success! */)
+ else ->
+ TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */)
+ }
+ // [END androidxr_arcore_hand_configure]
+}
+
+fun ComponentActivity.collectHands(session: Session) {
+ lifecycleScope.launch {
+ // [START androidxr_arcore_hand_collect]
+ Hand.left(session)?.state?.collect { handState -> // or Hand.right(session)
+ // Hand state has been updated.
+ // Use the state of hand joints to update an entity's position.
+ renderPlanetAtHandPalm(handState)
+ }
+ // [END androidxr_arcore_hand_collect]
+ }
+ lifecycleScope.launch {
+ Hand.right(session)?.state?.collect { rightHandState ->
+ renderPlanetAtFingerTip(rightHandState)
+ }
+ }
+}
+
+fun secondaryHandDetection(activity: Activity, session: Session) {
+ fun detectGesture(handState: Flow) {}
+ // [START androidxr_arcore_hand_handedness]
+ 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]
+}
+
+fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
+ val session: Session = null!!
+ val palmEntity: GltfModelEntity = null!!
+ // [START androidxr_arcore_hand_entityAtHandPalm]
+ 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)
+ palmEntity.setEnabled(angle > Math.toRadians(40.0))
+
+ val transformedPose =
+ session.scene.perceptionSpace.transformPoseTo(
+ palmPose,
+ session.scene.activitySpace,
+ )
+ val newPosition = transformedPose.translation + transformedPose.down * 0.05f
+ palmEntity.setPose(Pose(newPosition, transformedPose.rotation))
+ // [END androidxr_arcore_hand_entityAtHandPalm]
+}
+
+fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
+ val session: Session = null!!
+ val indexFingerEntity: GltfModelEntity = null!!
+
+ // [START androidxr_arcore_hand_entityAtIndexFingerTip]
+ 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)
+ indexFingerEntity.setEnabled(angle > Math.toRadians(40.0))
+
+ val transformedPose =
+ session.scene.perceptionSpace.transformPoseTo(
+ tipPose,
+ session.scene.activitySpace,
+ )
+ val position = transformedPose.translation + transformedPose.forward * 0.03f
+ val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up)
+ 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.HAND_JOINT_TYPE_THUMB_TIP] ?: return false
+ val thumbTipPose = session.scene.perceptionSpace.transformPoseTo(thumbTip, session.scene.activitySpace)
+ 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]
+}
+
+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.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
new file mode 100644
index 000000000..828f37857
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/arcore/Planes.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.xr.arcore.Plane
+import androidx.xr.runtime.Config
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionConfigureSuccess
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Ray
+import androidx.xr.scenecore.scene
+
+fun configurePlaneTracking(session: Session) {
+ // [START androidxr_arcore_planetracking_configure]
+ val newConfig = session.config.copy(
+ planeTracking = Config.PlaneTrackingMode.HORIZONTAL_AND_VERTICAL,
+ )
+ when (val result = session.configure(newConfig)) {
+ is SessionConfigureSuccess -> TODO(/* Success! */)
+ else ->
+ TODO(/* The session could not be configured. See SessionConfigureResult for possible causes. */)
+ }
+ // [END androidxr_arcore_planetracking_configure]
+}
+
+private suspend fun subscribePlanes(session: Session) {
+ // [START androidxr_arcore_planes_subscribe]
+ Plane.subscribe(session).collect { planes ->
+ // Planes have changed; update plane rendering
+ }
+ // [END androidxr_arcore_planes_subscribe]
+}
+
+private fun hitTestTable(session: Session) {
+ 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)
+ // 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
+ }
+ // [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
new file mode 100644
index 000000000..8b89e4c63
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/Orbiter.kt
@@ -0,0 +1,161 @@
+/*
+ * 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 androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+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.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+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.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.width
+import com.example.xr.R
+
+@Composable
+private fun OrbiterExampleSubspace() {
+ // [START androidxr_compose_OrbiterExampleSubspace]
+ Subspace {
+ SpatialPanel(
+ SubspaceModifier
+ .height(824.dp)
+ .width(1400.dp),
+ dragPolicy = MovePolicy(),
+ resizePolicy = ResizePolicy(),
+ ) {
+ SpatialPanelContent()
+ OrbiterExample()
+ }
+ }
+ // [END androidxr_compose_OrbiterExampleSubspace]
+}
+
+// [START androidxr_compose_OrbiterExample]
+@Composable
+fun OrbiterExample() {
+ Orbiter(
+ position = ContentEdge.Bottom,
+ offset = 96.dp,
+ alignment = Alignment.CenterHorizontally
+ ) {
+ Surface(Modifier.clip(CircleShape)) {
+ Row(
+ Modifier
+ .background(color = Color.Black)
+ .height(100.dp)
+ .width(600.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Orbiter",
+ color = Color.White,
+ fontSize = 50.sp
+ )
+ }
+ }
+ }
+}
+// [END androidxr_compose_OrbiterExample]
+
+@Composable
+fun OrbiterAnchoringExample() {
+ // [START androidxr_compose_OrbiterAnchoringExample]
+ Subspace {
+ SpatialRow {
+ Orbiter(
+ position = ContentEdge.Top,
+ offset = 8.dp,
+ offsetType = OrbiterOffsetType.InnerEdge,
+ shape = SpatialRoundedCornerShape(size = CornerSize(50))
+ ) {
+ Text(
+ "Hello World!",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier
+ .background(Color.White)
+ .padding(16.dp)
+ )
+ }
+ SpatialPanel(
+ SubspaceModifier
+ .height(824.dp)
+ .width(1400.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .background(Color.Red)
+ )
+ }
+ SpatialPanel(
+ SubspaceModifier
+ .height(824.dp)
+ .width(1400.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .background(Color.Blue)
+ )
+ }
+ }
+ }
+ // [END androidxr_compose_OrbiterAnchoringExample]
+}
+
+@Composable
+private fun NavigationRail() {}
+
+@Composable
+private fun Ui2DToOribiter() {
+ // [START androidxr_compose_orbiter_comparison]
+ // Previous approach
+ NavigationRail()
+
+ // New XR differentiated approach
+ Orbiter(
+ position = ContentEdge.Start,
+ offset = dimensionResource(R.dimen.start_orbiter_padding),
+ alignment = Alignment.Top
+ ) {
+ NavigationRail()
+ }
+ // [END androidxr_compose_orbiter_comparison]
+}
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialCapabilities.kt b/xr/src/main/java/com/example/xr/compose/SpatialCapabilities.kt
new file mode 100644
index 000000000..b3af88c13
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialCapabilities.kt
@@ -0,0 +1,68 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+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.fillMaxHeight
+import androidx.xr.compose.subspace.layout.width
+
+@Composable
+private fun SupportingInfoPanel() {}
+
+@Composable
+private fun ButtonToPresentInfoModal() {}
+
+@Composable
+private fun SpatialCapabilitiesCheck() {
+ // [START androidxr_compose_checkSpatialCapabilities]
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ SupportingInfoPanel()
+ } else {
+ ButtonToPresentInfoModal()
+ }
+
+ // Similar check for audio
+ val spatialAudioEnabled = LocalSpatialCapabilities.current.isSpatialAudioEnabled
+ // [END androidxr_compose_checkSpatialCapabilities]
+}
+
+@Composable
+private fun checkSpatialUiEnabled() {
+ // [START androidxr_compose_checkSpatialUiEnabled]
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ Subspace {
+ SpatialPanel(
+ modifier = SubspaceModifier
+ .width(1488.dp)
+ .fillMaxHeight()
+ ) {
+ AppContent()
+ }
+ }
+ } else {
+ AppContent()
+ }
+ // [END androidxr_compose_checkSpatialUiEnabled]
+}
+
+@Composable
+private fun AppContent() {}
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt b/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt
new file mode 100644
index 000000000..f9455f26b
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Button
+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.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.xr.compose.spatial.SpatialDialog
+import androidx.xr.compose.spatial.SpatialDialogProperties
+import kotlinx.coroutines.delay
+
+// [START androidxr_compose_DelayedDialog]
+@Composable
+fun DelayedDialog() {
+ var showDialog by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ delay(3000)
+ showDialog = true
+ }
+ if (showDialog) {
+ SpatialDialog(
+ onDismissRequest = { showDialog = false },
+ SpatialDialogProperties(
+ dismissOnBackPress = true
+ )
+ ) {
+ Box(
+ Modifier
+ .height(150.dp)
+ .width(150.dp)
+ ) {
+ Button(onClick = { showDialog = false }) {
+ Text("OK")
+ }
+ }
+ }
+ }
+}
+// [END androidxr_compose_DelayedDialog]
+
+@Composable
+private fun MyDialogContent() {}
+@Composable
+private fun SpatialDialogComparison() {
+ val onDismissRequest: () -> Unit = {}
+ // [START androidxr_compose_spatialdialog_comparison]
+ // Previous approach
+ Dialog(
+ onDismissRequest = onDismissRequest
+ ) {
+ MyDialogContent()
+ }
+
+ // New XR differentiated approach
+ SpatialDialog(
+ onDismissRequest = onDismissRequest
+ ) {
+ MyDialogContent()
+ }
+ // [END androidxr_compose_spatialdialog_comparison]
+}
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt b/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt
new file mode 100644
index 000000000..68fedfe3f
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialElevation.kt
@@ -0,0 +1,34 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+import androidx.xr.compose.spatial.SpatialElevation
+import androidx.xr.compose.spatial.SpatialElevationLevel
+
+@Composable
+private fun ComposableThatShouldElevateInXr() {}
+
+@Composable
+private fun SpatialElevationExample() {
+ // [START androidxr_compose_spatialelevation]
+ // Elevate an otherwise 2D Composable (signified here by ComposableThatShouldElevateInXr).
+ 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
new file mode 100644
index 000000000..c42730758
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.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
+ 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]
+
+// [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/SpatialLayout.kt b/xr/src/main/java/com/example/xr/compose/SpatialLayout.kt
new file mode 100644
index 000000000..b0099b00a
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialLayout.kt
@@ -0,0 +1,94 @@
+/*
+ * 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 androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.xr.compose.spatial.Subspace
+import androidx.xr.compose.subspace.SpatialColumn
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.width
+
+@Composable
+private fun SpatialLayoutExampleSubspace() {
+ // [START androidxr_compose_SpatialLayoutExampleSubspace]
+ Subspace {
+ SpatialRow {
+ SpatialColumn {
+ SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
+ SpatialPanelContent("Top Left")
+ }
+ SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) {
+ SpatialPanelContent("Middle Left")
+ }
+ SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
+ SpatialPanelContent("Bottom Left")
+ }
+ }
+ SpatialColumn {
+ SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
+ SpatialPanelContent("Top Right")
+ }
+ SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) {
+ SpatialPanelContent("Middle Right")
+ }
+ SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) {
+ SpatialPanelContent("Bottom Right")
+ }
+ }
+ }
+ }
+ // [END androidxr_compose_SpatialLayoutExampleSubspace]
+}
+
+// [START androidxr_compose_SpatialLayoutExampleSpatialPanelContent]
+@Composable
+fun SpatialPanelContent(text: String) {
+ Column(
+ Modifier
+ .background(color = Color.Black)
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Panel",
+ color = Color.White,
+ fontSize = 15.sp
+ )
+ Text(
+ text = text,
+ color = Color.White,
+ fontSize = 25.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+}
+// [END androidxr_compose_SpatialLayoutExampleSpatialPanelContent]
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt
new file mode 100644
index 000000000..2b75a426b
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt
@@ -0,0 +1,94 @@
+/*
+ * 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 androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+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.width
+
+@Composable
+private fun SpatialPanelExample() {
+ // [START androidxr_compose_SpatialPanel]
+ Subspace {
+ SpatialPanel(
+ SubspaceModifier
+ .height(824.dp)
+ .width(1400.dp),
+ dragPolicy = MovePolicy(),
+ resizePolicy = ResizePolicy(),
+ ) {
+ SpatialPanelContent()
+ }
+ }
+ // [END androidxr_compose_SpatialPanel]
+}
+
+// [START androidxr_compose_SpatialPanelContent]
+@Composable
+fun SpatialPanelContent() {
+ Box(
+ Modifier
+ .background(color = Color.Black)
+ .height(500.dp)
+ .width(500.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Spatial Panel",
+ color = Color.White,
+ fontSize = 25.sp
+ )
+ }
+}
+// [END androidxr_compose_SpatialPanelContent]
+
+@Composable
+private fun AppContent() {}
+
+@Composable
+private fun ContentInSpatialPanel() {
+ // [START androidxr_compose_SpatialPanelAppContent]
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ Subspace {
+ SpatialPanel(
+ dragPolicy = MovePolicy(),
+ resizePolicy = ResizePolicy(),
+ ) {
+ AppContent()
+ }
+ }
+ } else {
+ AppContent()
+ }
+ // [END androidxr_compose_SpatialPanelAppContent]
+}
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialPopup.kt b/xr/src/main/java/com/example/xr/compose/SpatialPopup.kt
new file mode 100644
index 000000000..f42e1d1b2
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialPopup.kt
@@ -0,0 +1,39 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+import androidx.compose.ui.window.Popup
+import androidx.xr.compose.spatial.SpatialPopup
+
+@Composable
+private fun MyPopupContent() {}
+@Composable
+private fun SpatialPopupComparison() {
+ val onDismissRequest: () -> Unit = {}
+ // [START androidxr_compose_spatialpopup_comparison]
+ // Previous approach
+ Popup(onDismissRequest = onDismissRequest) {
+ MyPopupContent()
+ }
+
+ // New XR differentiated approach
+ SpatialPopup(onDismissRequest = onDismissRequest) {
+ MyPopupContent()
+ }
+ // [END androidxr_compose_spatialpopup_comparison]
+}
diff --git a/xr/src/main/java/com/example/xr/compose/SpatialRow.kt b/xr/src/main/java/com/example/xr/compose/SpatialRow.kt
new file mode 100644
index 000000000..d138411ac
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/SpatialRow.kt
@@ -0,0 +1,63 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.width
+
+@Composable
+private fun SpatialRowExample() {
+ // [START androidxr_compose_SpatialRowExample]
+ SpatialRow {
+ SpatialPanel(
+ SubspaceModifier
+ .width(384.dp)
+ .height(592.dp)
+ ) {
+ StartSupportingPanelContent()
+ }
+ SpatialPanel(
+ SubspaceModifier
+ .height(824.dp)
+ .width(1400.dp)
+ ) {
+ App()
+ }
+ SpatialPanel(
+ SubspaceModifier
+ .width(288.dp)
+ .height(480.dp)
+ ) {
+ EndSupportingPanelContent()
+ }
+ }
+ // [END androidxr_compose_SpatialRowExample]
+}
+
+@Composable
+private fun App() { }
+
+@Composable
+private fun EndSupportingPanelContent() { }
+
+@Composable
+private fun StartSupportingPanelContent() { }
diff --git a/xr/src/main/java/com/example/xr/compose/Subspace.kt b/xr/src/main/java/com/example/xr/compose/Subspace.kt
new file mode 100644
index 000000000..2cbbe1021
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/Subspace.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.os.Bundle
+import androidx.activity.ComponentActivity
+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
+
+private class SubspaceActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ // [START androidxr_compose_SubspaceSetContent]
+ setContent {
+ // This is a top-level subspace
+ ApplicationSubspace {
+ SpatialPanel {
+ MyComposable()
+ }
+ }
+ }
+ // [END androidxr_compose_SubspaceSetContent]
+ }
+}
+
+// [START androidxr_compose_SubspaceComponents]
+@Composable
+private fun MyComposable() {
+ Row {
+ PrimaryPane()
+ SecondaryPane()
+ }
+}
+
+@Composable
+private fun PrimaryPane() {
+ // This is a nested subspace, because PrimaryPane is in a SpatialPanel
+ // and that SpatialPanel is in a top-level Subspace
+ Subspace {
+ ObjectInAVolume(true)
+ }
+}
+// [END androidxr_compose_SubspaceComponents]
+
+@Composable
+private fun SecondaryPane() {}
diff --git a/xr/src/main/java/com/example/xr/compose/Views.kt b/xr/src/main/java/com/example/xr/compose/Views.kt
new file mode 100644
index 000000000..b567b2f09
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/Views.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.Context
+import android.os.Bundle
+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.spatial.Subspace
+import androidx.xr.compose.subspace.SpatialPanel
+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.runtime.Session
+import androidx.xr.runtime.math.IntSize2d
+import androidx.xr.scenecore.PanelEntity
+import com.example.xr.R
+
+private class MyCustomView(context: Context) : View(context)
+
+private class ActivityWithSubspaceContent : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // [START androidxr_compose_ActivityWithSubspaceContent]
+ setContent {
+ Subspace {
+ SpatialPanel(
+ modifier = SubspaceModifier.height(500.dp).width(500.dp).depth(25.dp)
+ ) { MyCustomView(this@ActivityWithSubspaceContent) }
+ }
+ }
+ // [END androidxr_compose_ActivityWithSubspaceContent]
+ }
+}
+
+private class FragmentWithComposeView() : Fragment() {
+ // [START androidxr_compose_FragmentWithComposeView]
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.example_fragment, container, false)
+ view.findViewById(R.id.compose_view).apply {
+ // Dispose of the Composition when the view's LifecycleOwner
+ // is destroyed
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ // In Compose world
+ SpatialPanel(SubspaceModifier.height(500.dp).width(500.dp)) {
+ Text("Spatial Panel with Orbiter")
+ }
+ }
+ }
+ return view
+ }
+ // [END androidxr_compose_FragmentWithComposeView]
+}
+
+fun ComponentActivity.PanelEntityWithView(xrSession: Session) {
+ // [START androidxr_compose_PanelEntityWithView]
+ val panelContent = MyCustomView(this)
+ val panelEntity = PanelEntity.create(
+ session = xrSession,
+ view = panelContent,
+ 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
new file mode 100644
index 000000000..2144cc3c2
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/compose/Volume.kt
@@ -0,0 +1,91 @@
+/*
+ * 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 androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.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.offset
+import androidx.xr.compose.subspace.layout.scale
+import androidx.xr.compose.subspace.layout.width
+import kotlinx.coroutines.launch
+
+@Composable
+private fun VolumeExample() {
+ // [START androidxr_compose_Volume]
+ Subspace {
+ SpatialPanel(
+ SubspaceModifier.height(1500.dp).width(1500.dp),
+ dragPolicy = MovePolicy(),
+ resizePolicy = ResizePolicy(),
+ ) {
+ ObjectInAVolume(true)
+ Box(
+ Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Welcome",
+ fontSize = 50.sp,
+ )
+ }
+ }
+ }
+ // [END androidxr_compose_Volume]
+}
+
+// [START androidxr_compose_ObjectInAVolume]
+@OptIn(ExperimentalSubspaceVolumeApi::class)
+@Composable
+fun ObjectInAVolume(show3DObject: Boolean) {
+ // [START_EXCLUDE silent]
+ val volumeXOffset = 0.dp
+ val volumeYOffset = 0.dp
+ val volumeZOffset = 0.dp
+ // [END_EXCLUDE]
+ val session = checkNotNull(LocalSession.current)
+ val scope = rememberCoroutineScope()
+ if (show3DObject) {
+ Subspace {
+ Volume(
+ modifier = SubspaceModifier
+ .offset(volumeXOffset, volumeYOffset, volumeZOffset) // Relative position
+ .scale(1.2f) // Scale to 120% of the size
+ ) { parent ->
+ scope.launch {
+ // Load your 3D model here
+ }
+ }
+ }
+ }
+}
+// [END androidxr_compose_ObjectInAVolume]
diff --git a/xr/src/main/java/com/example/xr/misc/ModeTransition.kt b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt
new file mode 100644
index 000000000..ea13d5868
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/misc/ModeTransition.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.misc
+
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.platform.LocalSpatialConfiguration
+import androidx.xr.runtime.Session
+import androidx.xr.scenecore.scene
+
+@Composable
+fun modeTransitionCompose() {
+ // [START androidxr_misc_modeTransitionCompose]
+ LocalSpatialConfiguration.current.requestHomeSpaceMode()
+ // or
+ LocalSpatialConfiguration.current.requestFullSpaceMode()
+ // [END androidxr_misc_modeTransitionCompose]
+}
+
+fun modeTransitionScenecore(xrSession: Session) {
+ // [START androidxr_misc_modeTransitionScenecore]
+ 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
new file mode 100644
index 000000000..2a9f84ff1
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/runtime/Session.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.runtime
+
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.runtime.Session
+import androidx.xr.runtime.SessionCreateSuccess
+
+// [START androidxr_localsession]
+@Composable
+fun ComposableUsingSession() {
+ val session = LocalSession.current
+}
+// [END androidxr_localsession]
+
+fun ComponentActivity.createSession() {
+ // [START androidxr_session_create]
+ when (val result = Session.create(this)) {
+ is SessionCreateSuccess -> {
+ val xrSession = result.session
+ // ...
+ }
+ else ->
+ TODO(/* A different unhandled exception was thrown. */)
+ }
+ // [END androidxr_session_create]
+}
diff --git a/xr/src/main/java/com/example/xr/scenecore/Entities.kt b/xr/src/main/java/com/example/xr/scenecore/Entities.kt
new file mode 100644
index 000000000..cf16f266e
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/Entities.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.math.Pose
+import androidx.xr.runtime.math.Quaternion
+import androidx.xr.runtime.math.Vector3
+import androidx.xr.scenecore.Entity
+
+private fun setPoseExample(entity: Entity) {
+ // [START androidxr_scenecore_entity_setPoseExample]
+ // Place the entity forward 2 meters
+ val newPosition = Vector3(0f, 0f, -2f)
+ // Rotate the entity by 180 degrees on the up axis (upside-down)
+ val newOrientation = Quaternion.fromEulerAngles(0f, 0f, 180f)
+ // Update the position and rotation on the entity
+ entity.setPose(Pose(newPosition, newOrientation))
+ // [END androidxr_scenecore_entity_setPoseExample]
+}
+
+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) {
+ // [START androidxr_scenecore_entity_entitySetScale]
+ // Double the size of the entity
+ entity.setScale(2f)
+ // [END androidxr_scenecore_entity_entitySetScale]
+}
diff --git a/xr/src/main/java/com/example/xr/scenecore/Environments.kt b/xr/src/main/java/com/example/xr/scenecore/Environments.kt
new file mode 100644
index 000000000..5ed80d36f
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/Environments.kt
@@ -0,0 +1,80 @@
+/*
+ * 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 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 java.nio.file.Paths
+
+private class Environments(val session: Session) {
+ suspend fun loadEnvironmentGeometry(context: Context) {
+ // [START androidxr_scenecore_environment_loadEnvironmentGeometry]
+ val environmentGeometry = GltfModel.create(session, Paths.get("DayGeometry.glb"))
+ // [END androidxr_scenecore_environment_loadEnvironmentGeometry]
+ }
+
+ suspend fun loadEnvironmentSkybox() {
+ // [START androidxr_scenecore_environment_loadEnvironmentSkybox]
+ val lightingForSkybox = ExrImage.createFromZip(session, Paths.get("BlueSkyboxLighting.zip"))
+ // [END androidxr_scenecore_environment_loadEnvironmentSkybox]
+ }
+
+ fun setEnvironmentPreference(environmentGeometry: GltfModel, lightingForSkybox: ExrImage) {
+ // [START androidxr_scenecore_environment_setEnvironmentPreference]
+ val spatialEnvironmentPreference =
+ SpatialEnvironment.SpatialEnvironmentPreference(lightingForSkybox, environmentGeometry)
+ 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 {
+ // 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]
+ 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 {
+ // 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.
+ }
+ // [END androidxr_scenecore_environment_setPassthroughOpacityPreference]
+ }
+
+ fun getCurrentPassthroughOpacity() {
+ // [START androidxr_scenecore_environment_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
new file mode 100644
index 000000000..cf39f9f4f
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/GltfEntity.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 android.content.ActivityNotFoundException
+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.SpatialCapabilities
+import androidx.xr.scenecore.scene
+import java.nio.file.Paths
+
+private suspend fun loadGltfFile(session: Session) {
+ // [START androidxr_scenecore_gltfmodel_create]
+ val gltfModel = GltfModel.create(session, Paths.get("models", "saturn_rings.glb"))
+ // [END androidxr_scenecore_gltfmodel_create]
+}
+
+private fun createModelEntity(session: Session, gltfModel: GltfModel) {
+ // [START androidxr_scenecore_gltfmodelentity_create]
+ if (session.scene.spatialCapabilities
+ .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT)
+ ) {
+ val gltfEntity = GltfModelEntity.create(session, gltfModel)
+ }
+ // [END androidxr_scenecore_gltfmodelentity_create]
+}
+
+private fun animateEntity(gltfEntity: GltfModelEntity) {
+ // [START androidxr_scenecore_gltfmodelentity_animation]
+ gltfEntity.startAnimation(loop = true, animationName = "Walk")
+ // [END androidxr_scenecore_gltfmodelentity_animation]
+}
+
+private fun ComponentActivity.startSceneViewer() {
+ // [START androidxr_scenecore_sceneviewer]
+ val url =
+ "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.setData(intentUri)
+ try {
+ startActivity(sceneViewerIntent)
+ } catch (e: ActivityNotFoundException) {
+ // There is no activity that could handle the intent.
+ }
+ // [END androidxr_scenecore_sceneviewer]
+}
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..e4136ea72
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/ResizableComponent.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.FloatSize2d
+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.shape = SurfaceEntity.Shape.Quad(FloatSize2d(event.newSize.width, event.newSize.height))
+ }
+ }
+ resizableComponent.minimumEntitySize = FloatSize3d(177f, 100f, 1f)
+ 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/SpatialAudio.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt
new file mode 100644
index 000000000..25b1556a4
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/SpatialAudio.kt
@@ -0,0 +1,171 @@
+/*
+ * 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 android.content.Context
+import android.media.AudioAttributes
+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
+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.scene
+
+private fun playSpatialAudioAtEntity(session: Session, appContext: Context, entity: Entity) {
+ // [START androidxr_scenecore_playSpatialAudio]
+ // Check spatial capabilities before using spatial audio
+ if (session.scene.spatialCapabilities
+ .hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)
+ ) { // The session has spatial audio capabilities
+ val maxVolume = 1F
+ val lowPriority = 0
+ val infiniteLoop = -1
+ val normalSpeed = 1F
+
+ val soundPool = SoundPool.Builder()
+ .setAudioAttributes(
+ AudioAttributes.Builder()
+ .setContentType(CONTENT_TYPE_SONIFICATION)
+ .setUsage(USAGE_ASSISTANCE_SONIFICATION)
+ .build()
+ )
+ .build()
+
+ val pointSource = PointSourceParams(entity)
+
+ val soundEffect = appContext.assets.openFd("sounds/tiger_16db.mp3")
+ val pointSoundId = soundPool.load(soundEffect, lowPriority)
+
+ soundPool.setOnLoadCompleteListener { soundPool, sampleId, status ->
+ // wait for the sound file to be loaded into the soundPool
+ if (status == 0) {
+ SpatialSoundPool.play(
+ session = session,
+ soundPool = soundPool,
+ soundID = pointSoundId,
+ params = pointSource,
+ volume = maxVolume,
+ priority = lowPriority,
+ loop = infiniteLoop,
+ rate = normalSpeed
+ )
+ }
+ }
+ } else {
+ // The session does not have spatial audio capabilities
+ }
+ // [END androidxr_scenecore_playSpatialAudio]
+}
+
+private fun playSpatialAudioAtEntitySurround(session: Session, appContext: Context) {
+ // [START androidxr_scenecore_playSpatialAudioSurround]
+ // Check spatial capabilities before using spatial audio
+ if (session.scene.spatialCapabilities.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) {
+ // The session has spatial audio capabilities
+
+ val pointSourceAttributes = PointSourceParams(session.scene.mainPanelEntity)
+
+ val mediaPlayer = MediaPlayer()
+
+ val fivePointOneAudio = appContext.assets.openFd("sounds/aac_51.ogg")
+ mediaPlayer.reset()
+ mediaPlayer.setDataSource(fivePointOneAudio)
+
+ val audioAttributes =
+ AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build()
+
+ SpatialMediaPlayer.setPointSourceParams(
+ session,
+ mediaPlayer,
+ pointSourceAttributes
+ )
+
+ mediaPlayer.setAudioAttributes(audioAttributes)
+ mediaPlayer.prepare()
+ mediaPlayer.start()
+ } else {
+ // The session does not have spatial audio capabilities
+ }
+ // [END androidxr_scenecore_playSpatialAudioSurround]
+}
+
+private fun playSpatialAudioAtEntityAmbionics(session: Session, appContext: Context) {
+ // [START androidxr_scenecore_playSpatialAudioAmbionics]
+ // Check spatial capabilities before using spatial audio
+ if (session.scene.spatialCapabilities.hasCapability(SpatialCapabilities.SPATIAL_CAPABILITY_SPATIAL_AUDIO)) {
+ // The session has spatial audio capabilities
+
+ val soundFieldAttributes =
+ SoundFieldAttributes(SpatializerConstants.AMBISONICS_ORDER_FIRST_ORDER)
+
+ val mediaPlayer = MediaPlayer()
+
+ val soundFieldAudio = appContext.assets.openFd("sounds/foa_basketball_16bit.wav")
+
+ mediaPlayer.reset()
+ mediaPlayer.setDataSource(soundFieldAudio)
+
+ val audioAttributes =
+ AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build()
+
+ SpatialMediaPlayer.setSoundFieldAttributes(
+ session,
+ mediaPlayer,
+ soundFieldAttributes
+ )
+
+ mediaPlayer.setAudioAttributes(audioAttributes)
+ mediaPlayer.prepare()
+ mediaPlayer.start()
+ } else {
+ // The session does not have spatial audio capabilities
+ }
+ // [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
new file mode 100644
index 000000000..7fb784080
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/SpatialCapabilities.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.SpatialCapabilities
+import androidx.xr.scenecore.scene
+
+fun checkMultipleCapabilities(xrSession: Session) {
+ // [START androidxr_compose_checkMultipleCapabilities]
+ // Example 1: check if enabling passthrough mode is allowed
+ if (xrSession.scene.spatialCapabilities.hasCapability(
+ SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL
+ )
+ ) {
+ xrSession.scene.spatialEnvironment.preferredPassthroughOpacity = 1f
+ }
+ // Example 2: multiple capability flags can be checked simultaneously:
+ if (xrSession.scene.spatialCapabilities.hasCapability(
+ SpatialCapabilities.SPATIAL_CAPABILITY_PASSTHROUGH_CONTROL and
+ SpatialCapabilities.SPATIAL_CAPABILITY_3D_CONTENT
+ )
+ ) {
+ // ...
+ }
+ // [END androidxr_compose_checkMultipleCapabilities]
+}
diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt
new file mode 100644
index 000000000..85f1dfa05
--- /dev/null
+++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt
@@ -0,0 +1,226 @@
+/*
+ * 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 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.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.scene
+import java.nio.file.Paths
+import kotlinx.coroutines.launch
+
+private fun ComponentActivity.surfaceEntityCreate(xrSession: Session) {
+ // [START androidxr_scenecore_surfaceEntityCreate]
+ val stereoSurfaceEntity = SurfaceEntity.create(
+ 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)
+ .path("sbs_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_surfaceEntityCreate]
+}
+
+private fun ComponentActivity.surfaceEntityCreateSbs(xrSession: Session) {
+ // [START androidxr_scenecore_surfaceEntityCreateSbs]
+ // Set up the surface for playing a 180° video on a hemisphere.
+ val hemisphereStereoSurfaceEntity =
+ SurfaceEntity.create(
+ session = xrSession,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
+ pose = xrSession.scene.spatialUser.head?.transformPoseTo(
+ Pose.Identity,
+ xrSession.scene.activitySpace
+ )!!,
+ shape = SurfaceEntity.Shape.Hemisphere(1.0f),
+ )
+ // ... and use the surface for playing the media.
+ // [END androidxr_scenecore_surfaceEntityCreateSbs]
+}
+
+private fun ComponentActivity.surfaceEntityCreateTb(xrSession: Session) {
+ // [START androidxr_scenecore_surfaceEntityCreateTb]
+ // Set up the surface for playing a 360° video on a sphere.
+ val sphereStereoSurfaceEntity =
+ SurfaceEntity.create(
+ session = xrSession,
+ stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
+ pose = xrSession.scene.spatialUser.head?.transformPoseTo(
+ Pose.Identity,
+ xrSession.scene.activitySpace
+ )!!,
+ shape = SurfaceEntity.Shape.Sphere(1.0f),
+ )
+ // ... 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(
+ 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)
+ .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]
+}
+
+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.STEREO_MODE_SIDE_BY_SIDE,
+ pose = Pose(Vector3(0.0f, 0.0f, -1.5f)),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+ surfaceProtection = SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_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.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.STEREO_MODE_MONO,
+ pose = Pose(Vector3(0.0f, 0.0f, -1.5f)),
+ shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+ )
+ hdrSurfaceEntity.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.edgeFeatheringParams =
+ SurfaceEntity.EdgeFeatheringParams.RectangleFeather(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"),
+ )
+
+ // Apply the alpha mask.
+ surfaceEntity.primaryAlphaMaskTexture = alphaMaskTexture
+
+ // To remove the mask, set the property to null.
+ surfaceEntity.primaryAlphaMaskTexture = null
+ }
+ // [END androidxr_scenecore_surfaceEntityAlphaMasking]
+}
diff --git a/xr/src/main/res/layout/example_fragment.xml b/xr/src/main/res/layout/example_fragment.xml
new file mode 100644
index 000000000..607557309
--- /dev/null
+++ b/xr/src/main/res/layout/example_fragment.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/xr/src/main/res/values/dimens.xml b/xr/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..25ca08b8f
--- /dev/null
+++ b/xr/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+
+
+
+ 8dp
+ 8dp
+