diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8b44b8a5..100540af 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -155,6 +155,10 @@ dependencies {
implementation(projects.core.theme)
implementation(projects.core.util)
+ // library must be compileOnly, see
+ // https://developer.android.com/develop/xr/jetpack-xr-sdk/getting-started#enable-minification
+ compileOnly(libs.androidx.xr.extensions)
+
baselineProfile(projects.benchmark)
// Android Instrumented Tests
diff --git a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt
index 8b3a930a..1ffae662 100644
--- a/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt
+++ b/core/network/src/main/java/com/android/developers/androidify/RemoteConfigDataSource.kt
@@ -45,6 +45,8 @@ interface RemoteConfigDataSource {
fun getBotBackgroundInstructionPrompt(): String
fun watchfaceFeatureEnabled(): Boolean
+
+ fun isXrEnabled(): Boolean
}
@Singleton
@@ -117,4 +119,8 @@ class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource
override fun watchfaceFeatureEnabled(): Boolean {
return remoteConfig.getBoolean("watchface_feature_enabled")
}
+
+ override fun isXrEnabled(): Boolean {
+ return remoteConfig.getBoolean("xr_feature_enabled")
+ }
}
diff --git a/core/network/src/main/res/xml/remote_config_defaults.xml b/core/network/src/main/res/xml/remote_config_defaults.xml
index 6b6f6c63..eb8daa7e 100644
--- a/core/network/src/main/res/xml/remote_config_defaults.xml
+++ b/core/network/src/main/res/xml/remote_config_defaults.xml
@@ -23,6 +23,10 @@
background_vibes_feature_enabled
false
+
+ xr_feature_enabled
+ false
+
bot_background_instruction_prompt
Add the input image android bot as the main subject to the result,
diff --git a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt
index eec9e976..5cfea0b8 100644
--- a/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt
+++ b/core/testing/src/main/java/com/android/developers/testing/network/TestRemoteConfigDataSource.kt
@@ -83,4 +83,8 @@ class TestRemoteConfigDataSource(private val useGeminiNano: Boolean) : RemoteCon
override fun watchfaceFeatureEnabled(): Boolean {
return true
}
+
+ override fun isXrEnabled(): Boolean {
+ return false
+ }
}
diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt
index 976546d2..9a1a2266 100644
--- a/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt
+++ b/core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt
@@ -36,6 +36,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.android.developers.androidify.theme.AndroidifyTheme
@@ -78,6 +80,31 @@ fun SquiggleBackground(
}
}
+/**
+ * Background squiggle that tries to fit in its parent.
+ */
+@Composable
+fun SquiggleBackgroundFull() {
+ val vectorBackground =
+ rememberVectorPainter(ImageVector.vectorResource(R.drawable.squiggle_full))
+ Box(modifier = Modifier.fillMaxSize()) {
+ Image(
+ painter = vectorBackground,
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Fit,
+ )
+ }
+}
+
+@Preview(device = PIXEL_TABLET)
+@Composable
+fun SquiggleFullImagePreview() {
+ AndroidifyTheme {
+ SquiggleBackgroundFull()
+ }
+}
+
@LargeScreensPreview
@Composable
private fun SquiggleBackgroundLargePreview() {
diff --git a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt
index 41d40f7e..2fca24a9 100644
--- a/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt
+++ b/core/theme/src/main/java/com/android/developers/androidify/theme/components/TopAppBar.kt
@@ -26,20 +26,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@@ -57,10 +54,9 @@ fun AndroidifyTopAppBar(
titleText: String = stringResource(R.string.androidify_title),
isMediumWindowSize: Boolean = false,
backEnabled: Boolean = false,
- aboutEnabled: Boolean = true,
expandedCenterButtons: @Composable () -> Unit = {},
onBackPressed: () -> Unit = {},
- onAboutClicked: () -> Unit = {},
+ actions: (@Composable () -> Unit)? = null,
) {
if (isMediumWindowSize) {
Box(
@@ -95,16 +91,16 @@ fun AndroidifyTopAppBar(
expandedCenterButtons()
}
- if (aboutEnabled) {
- AboutButton(
- modifier = Modifier
- .align(Alignment.CenterEnd)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLowest,
- shape = CircleShape,
- ),
- onAboutClicked = onAboutClicked,
- )
+ Row(
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainerLowest,
+ shape = CircleShape,
+ ),
+
+ ) {
+ actions?.invoke()
}
}
} else {
@@ -124,9 +120,7 @@ fun AndroidifyTopAppBar(
}
},
actions = {
- if (aboutEnabled) {
- AboutButton(onAboutClicked = onAboutClicked)
- }
+ actions?.invoke()
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest),
)
@@ -143,38 +137,6 @@ private fun BackButton(onBackPressed: () -> Unit) {
}
}
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AndroidifyTranslucentTopAppBar(
- modifier: Modifier = Modifier,
- titleText: String = stringResource(R.string.androidify_title),
- isMediumSizeLayout: Boolean = false,
-) {
- if (isMediumSizeLayout) {
- TopAppBar(
- title = {
- Spacer(Modifier.statusBarsPadding())
- AndroidifyTitle(titleText)
- },
- modifier = modifier.clip(
- MaterialTheme.shapes.large.copy(topStart = CornerSize(0f), topEnd = CornerSize(0f)),
- ),
- colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
- )
- } else {
- CenterAlignedTopAppBar(
- title = {
- Spacer(Modifier.statusBarsPadding())
- AndroidifyTitle(titleText)
- },
- modifier = modifier.clip(
- MaterialTheme.shapes.large.copy(topStart = CornerSize(0f), topEnd = CornerSize(0f)),
- ),
- colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
- )
- }
-}
-
@Composable
private fun AndroidifyTitle(text: String) {
Text(text, fontWeight = FontWeight.Bold)
@@ -182,7 +144,7 @@ private fun AndroidifyTitle(text: String) {
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
-private fun AboutButton(modifier: Modifier = Modifier, onAboutClicked: () -> Unit = {}) {
+fun AboutButton(modifier: Modifier = Modifier, onAboutClicked: () -> Unit = {}) {
val sharedTransitionScope = LocalSharedTransitionScope.current
with(sharedTransitionScope) {
IconButton(
diff --git a/core/theme/src/main/res/drawable/squiggle_full.xml b/core/theme/src/main/res/drawable/squiggle_full.xml
new file mode 100644
index 00000000..4fc308ca
--- /dev/null
+++ b/core/theme/src/main/res/drawable/squiggle_full.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/xr/.gitignore b/core/xr/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/xr/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/xr/build.gradle.kts b/core/xr/build.gradle.kts
new file mode 100644
index 00000000..58b89183
--- /dev/null
+++ b/core/xr/build.gradle.kts
@@ -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.
+ */
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+android {
+ namespace = "com.android.developers.androidify.xr"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get())
+ targetCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get())
+ }
+ kotlinOptions {
+ jvmTarget = libs.versions.jvmTarget.get()
+ }
+
+}
+
+dependencies {
+ implementation(libs.androidx.xr.compose)
+
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(projects.core.util)
+ implementation(projects.core.theme)
+}
diff --git a/core/xr/proguard-rules.pro b/core/xr/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/core/xr/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/core/xr/src/main/AndroidManifest.xml b/core/xr/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..b47d651b
--- /dev/null
+++ b/core/xr/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.kt
new file mode 100644
index 00000000..a587f465
--- /dev/null
+++ b/core/xr/src/main/java/com/android/developers/androidify/xr/MainPanelWorkaround.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.android.developers.androidify.xr
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.scenecore.scene
+import kotlinx.coroutines.delay
+
+/*
+ * A composable that attempts to continually hide the mainPanel.
+ *
+ * This is a temporary workaround for b/440325404, that causes the mainPanelEntity when an
+ * ApplicationSubspace transitions out of the composition due to a race condition when transitioning
+ * the two hierarchies.
+ */
+@Composable
+fun MainPanelWorkaround() {
+ val session = LocalSession.current
+ LaunchedEffect(null) {
+ while (true) {
+ delay(100L)
+ session?.scene?.mainPanelEntity?.setEnabled(false)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt
new file mode 100644
index 00000000..119d0b19
--- /dev/null
+++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SharedTransitions.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.android.developers.androidify.xr
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.BoundsTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Placeable
+import com.android.developers.androidify.theme.LocalSharedTransitionScope
+
+/**
+ * On Android XR in Full Space Mode, spatial panels inflate into a different Window.
+ * A layout using a SharedTransitionScope will fail to find shared elements across different
+ * spatial panels.
+ *
+ * This composable replaces the LocalSharedTransitionScope with a no-op SharedTransitionScope,
+ * effectively disabling the shared transitions.
+ */
+@Composable
+fun DisableSharedTransition(content: @Composable (() -> Unit)) {
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ SharedTransitionLayout {
+ CompositionLocalProvider(LocalSharedTransitionScope provides shimSharedTransitionScope(this)) {
+ content()
+ }
+ }
+}
+
+/** A fake implementation of SharedTransitionScope that disables registering shared transitions. */
+@ExperimentalSharedTransitionApi
+private fun shimSharedTransitionScope(original: SharedTransitionScope): SharedTransitionScope {
+ return object : SharedTransitionScope {
+ override val isTransitionActive: Boolean
+ get() = false
+
+ override fun Modifier.skipToLookaheadSize(): Modifier = this
+
+ override fun Modifier.renderInSharedTransitionScopeOverlay(
+ zIndexInOverlay: Float,
+ renderInOverlay: (SharedTransitionScope) -> Boolean,
+ ) = this
+
+ override fun Modifier.sharedElement(
+ sharedContentState: SharedTransitionScope.SharedContentState,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ boundsTransform: BoundsTransform,
+ placeHolderSize: SharedTransitionScope.PlaceHolderSize,
+ renderInOverlayDuringTransition: Boolean,
+ zIndexInOverlay: Float,
+ clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
+ ) = this
+
+ override fun Modifier.sharedBounds(
+ sharedContentState: SharedTransitionScope.SharedContentState,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ enter: EnterTransition,
+ exit: ExitTransition,
+ boundsTransform: BoundsTransform,
+ resizeMode: SharedTransitionScope.ResizeMode,
+ placeHolderSize: SharedTransitionScope.PlaceHolderSize,
+ renderInOverlayDuringTransition: Boolean,
+ zIndexInOverlay: Float,
+ clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
+ ) = this
+
+ override fun Modifier.sharedElementWithCallerManagedVisibility(
+ sharedContentState: SharedTransitionScope.SharedContentState,
+ visible: Boolean,
+ boundsTransform: BoundsTransform,
+ placeHolderSize: SharedTransitionScope.PlaceHolderSize,
+ renderInOverlayDuringTransition: Boolean,
+ zIndexInOverlay: Float,
+ clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
+ ) = this
+
+ override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip =
+ original.OverlayClip(clipShape)
+
+ override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
+ get() = with(original) { lookaheadScopeCoordinates }
+
+ override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
+ with(original) {
+ return toLookaheadCoordinates()
+ }
+ }
+ }
+}
diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.kt
new file mode 100644
index 00000000..1222357e
--- /dev/null
+++ b/core/xr/src/main/java/com/android/developers/androidify/xr/SpatialUiModes.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.android.developers.androidify.xr
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.platform.SpatialCapabilities
+import androidx.xr.scenecore.scene
+
+/** Check if the device is XR-enabled, but is not yet rendering spatial UI. */
+@Composable
+fun couldRequestFullSpace(): Boolean {
+ return LocalSpatialConfiguration.current.hasXrSpatialFeature && !LocalSpatialCapabilities.current.isSpatialUiEnabled
+}
+
+/** Check if the device is XR-enabled and is rendering spatial UI. */
+@Composable
+fun SpatialCapabilities.couldRequestHomeSpace(): Boolean {
+ if (!LocalSpatialConfiguration.current.hasXrSpatialFeature) return false
+ return isSpatialUiEnabled
+}
+
+/** Default styling for an IconButton with a home space button and behavior. */
+@Composable
+fun RequestHomeSpaceIconButton(modifier: Modifier = Modifier) {
+ val session = LocalSession.current ?: return
+
+ IconButton(
+ modifier = modifier,
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ ),
+ onClick = {
+ session.scene.requestHomeSpaceMode()
+ },
+ ) {
+ Icon(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(8.dp),
+ imageVector = ImageVector.vectorResource(R.drawable.collapse_content_24px),
+ contentDescription = stringResource(R.string.xr_to_home_space_mode),
+ )
+ }
+}
+
+/** Default styling for an TopAppBar Button with a full space button and behavior. */
+@Composable
+fun RequestFullSpaceIconButton(modifier: Modifier = Modifier) {
+ val session = LocalSession.current ?: return
+
+ IconButton(
+ modifier = modifier,
+ onClick = {
+ session.scene.requestFullSpaceMode()
+ },
+ ) {
+ Icon(
+ ImageVector.vectorResource(R.drawable.expand_content_24px),
+ contentDescription = stringResource(R.string.xr_to_full_space_mode),
+ )
+ }
+}
diff --git a/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt
new file mode 100644
index 00000000..b06d5e65
--- /dev/null
+++ b/core/xr/src/main/java/com/android/developers/androidify/xr/XrPreview.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.android.developers.androidify.xr
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.ui.tooling.preview.Devices.PIXEL_7_PRO
+import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.xr.compose.platform.SpatialCapabilities
+import androidx.xr.compose.platform.SpatialConfiguration
+import androidx.xr.runtime.Session
+
+/**
+ * Preview for a layout that could go into Full Space Mode.
+ */
+@Composable
+fun SupportsFullSpaceModeRequestProvider(contents: @Composable () -> Unit) {
+ CompositionLocalProvider(LocalSpatialConfiguration provides HasSpatialFeatureSpatialConfiguration) {
+ CompositionLocalProvider(LocalSpatialCapabilities provides SupportsFullSpaceModeRequestCapabilities) {
+ CompositionLocalProvider(LocalSession provides null) {
+ contents()
+ }
+ }
+ }
+}
+
+private object HasSpatialFeatureSpatialConfiguration : SpatialConfiguration {
+ override val hasXrSpatialFeature: Boolean
+ get() = true
+}
+
+private object SupportsFullSpaceModeRequestCapabilities : SpatialCapabilities {
+ override val isSpatialUiEnabled: Boolean
+ get() = false
+ override val isContent3dEnabled: Boolean
+ get() = true
+ override val isAppEnvironmentEnabled: Boolean
+ get() = true
+ override val isPassthroughControlEnabled: Boolean
+ get() = true
+ override val isSpatialAudioEnabled: Boolean
+ get() = true
+}
+
+/**
+ * Workaround composition locals for b/441901724.
+ * Any composable referencing XR composition locals will fail to preview instead of gracefully
+ * degrading due to failing to resolve XR capabilities.
+ *
+ * This can be removed when the default for XR capabilities under the preview is no capabilities
+ * instead of throwing an exception.
+ * */
+val LocalSpatialCapabilities: ProvidableCompositionLocal =
+ compositionLocalWithComputedDefaultOf {
+ runCatching {
+ androidx.xr.compose.platform.LocalSpatialCapabilities.currentValue
+ }.getOrDefault(NoSpatialCapabilities)
+ }
+
+/**
+ * Workaround composition locals for b/441901724.
+ * Any composable referencing XR composition locals will fail to preview instead of gracefully
+ * degrading due to failing to resolve XR capabilities.
+ *
+ * This can be removed when the default for XR capabilities under the preview is no capabilities
+ * instead of throwing an exception.
+ * */
+val LocalSpatialConfiguration: ProvidableCompositionLocal =
+ compositionLocalWithComputedDefaultOf {
+ runCatching {
+ androidx.xr.compose.platform.LocalSpatialConfiguration.currentValue
+ }.getOrDefault(LacksSpatialFeatureSpatialConfiguration)
+ }
+
+/**
+ * Workaround composition locals for b/441901724.
+ * Any composable referencing XR composition locals will fail to preview instead of gracefully
+ * degrading due to failing to resolve XR capabilities.
+ *
+ * This can be removed when the default for XR capabilities under the preview is no capabilities
+ * instead of throwing an exception.
+ * */
+val LocalSession: ProvidableCompositionLocal =
+ compositionLocalWithComputedDefaultOf {
+ runCatching {
+ androidx.xr.compose.platform.LocalSession.currentValue
+ }.getOrNull()
+ }
+
+private object NoSpatialCapabilities : SpatialCapabilities {
+ override val isSpatialUiEnabled: Boolean
+ get() = false
+ override val isContent3dEnabled: Boolean
+ get() = false
+ override val isAppEnvironmentEnabled: Boolean
+ get() = false
+ override val isPassthroughControlEnabled: Boolean
+ get() = false
+ override val isSpatialAudioEnabled: Boolean
+ get() = false
+}
+
+private object LacksSpatialFeatureSpatialConfiguration : SpatialConfiguration {
+ override val hasXrSpatialFeature: Boolean
+ get() = false
+}
+
+@Preview(device = PIXEL_TABLET, name = "Android XR (Home Space Mode)")
+annotation class XrHomeSpaceMediumPreview
+
+@Preview(device = PIXEL_7_PRO, name = "Android XR (Home Space Mode)")
+annotation class XrHomeSpaceCompactPreview
diff --git a/core/xr/src/main/res/drawable/collapse_content_24px.xml b/core/xr/src/main/res/drawable/collapse_content_24px.xml
new file mode 100644
index 00000000..f11b7743
--- /dev/null
+++ b/core/xr/src/main/res/drawable/collapse_content_24px.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/core/xr/src/main/res/drawable/expand_content_24px.xml b/core/xr/src/main/res/drawable/expand_content_24px.xml
new file mode 100644
index 00000000..e2a6035c
--- /dev/null
+++ b/core/xr/src/main/res/drawable/expand_content_24px.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/core/xr/src/main/res/values/strings.xml b/core/xr/src/main/res/values/strings.xml
new file mode 100644
index 00000000..9f061fa7
--- /dev/null
+++ b/core/xr/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ To Full Space Mode
+ To Home Space Mode
+
diff --git a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt
index 5c0a36d7..8dda915f 100644
--- a/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt
+++ b/data/src/main/java/com/android/developers/androidify/data/ConfigProvider.kt
@@ -33,4 +33,8 @@ class ConfigProvider @Inject constructor(val remoteConfigDataSource: RemoteConfi
fun getDancingDroidLink(): String {
return remoteConfigDataSource.getDancingDroidLink()
}
+
+ fun isXrEnabled(): Boolean {
+ return remoteConfigDataSource.isXrEnabled()
+ }
}
diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt
index c16c1de6..db0043e9 100644
--- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt
+++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationScreen.kt
@@ -139,6 +139,7 @@ import com.android.developers.androidify.theme.Primary90
import com.android.developers.androidify.theme.R
import com.android.developers.androidify.theme.Secondary
import com.android.developers.androidify.theme.SharedElementKey
+import com.android.developers.androidify.theme.components.AboutButton
import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
import com.android.developers.androidify.theme.components.GradientAssistElevatedChip
import com.android.developers.androidify.theme.components.PrimaryButton
@@ -290,9 +291,10 @@ fun EditScreen(
AndroidifyTopAppBar(
backEnabled = true,
isMediumWindowSize = isExpanded,
- aboutEnabled = true,
+ actions = {
+ AboutButton { onAboutPressed() }
+ },
onBackPressed = onBackPressed,
- onAboutClicked = onAboutPressed,
expandedCenterButtons = {
PromptTypeToolbar(
uiState.selectedPromptOption,
diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt
index 0085e8a2..ccf39eef 100644
--- a/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt
+++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/LoadingScreen.kt
@@ -87,7 +87,7 @@ fun LoadingScreen(
) {
Scaffold(
topBar = {
- AndroidifyTopAppBar(isMediumWindowSize = isMediumScreen, aboutEnabled = false)
+ AndroidifyTopAppBar(isMediumWindowSize = isMediumScreen)
},
bottomBar = {
Box(
diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts
index 0e2dc23d..68410264 100644
--- a/feature/home/build.gradle.kts
+++ b/feature/home/build.gradle.kts
@@ -74,6 +74,9 @@ dependencies {
exclude(group = "com.google.guava")
}
+ implementation(libs.androidx.xr.compose)
+ implementation(projects.core.xr)
+
implementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.tooling.preview)
diff --git a/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt b/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt
index 450f2581..5e0b077e 100644
--- a/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt
+++ b/feature/home/src/androidTest/java/com/android/developers/androidify/home/HomeScreenTest.kt
@@ -48,7 +48,7 @@ class HomeScreenTest {
SharedElementContextPreview {
HomeScreenContents(
- isMediumWindowSize = false, // Provide a default or mock value
+ layoutType = HomeScreenLayoutType.Compact, // Provide a default or mock value
onClickLetsGo = { offset: IntOffset -> // Match the lambda signature
wasClicked = true
},
@@ -74,7 +74,7 @@ class HomeScreenTest {
composeTestRule.setContent {
SharedElementContextPreview {
HomeScreenContents(
- isMediumWindowSize = false, // Ensure compact mode for pager
+ layoutType = HomeScreenLayoutType.Compact, // Ensure compact mode for pager
onClickLetsGo = { },
onAboutClicked = {},
videoLink = "",
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt
index b42e157e..370a849f 100644
--- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreen.kt
@@ -16,115 +16,33 @@
package com.android.developers.androidify.home
import androidx.annotation.OptIn
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
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.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.foundation.text.InlineTextContent
-import androidx.compose.foundation.text.TextAutoSize
-import androidx.compose.foundation.text.appendInlineContent
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedIconButton
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
-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.clipToBounds
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onLayoutRectChanged
-import androidx.compose.ui.layout.onVisibilityChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.Placeholder
-import androidx.compose.ui.text.PlaceholderVerticalAlign
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.media3.common.MediaItem
-import androidx.media3.common.Player
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.ui.compose.PlayerSurface
-import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
-import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
-import coil3.compose.AsyncImage
-import com.android.developers.androidify.theme.Blue
+import com.android.developers.androidify.home.xr.HomeScreenContentsSpatial
import com.android.developers.androidify.theme.SharedElementContextPreview
-import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
-import com.android.developers.androidify.theme.components.AndroidifyTranslucentTopAppBar
import com.android.developers.androidify.theme.components.SquiggleBackground
import com.android.developers.androidify.util.LargeScreensPreview
-import com.android.developers.androidify.util.LocalOcclusion
import com.android.developers.androidify.util.PhonePreview
-import com.android.developers.androidify.util.isAtLeastMedium
-import com.android.developers.androidify.theme.R as ThemeR
@ExperimentalMaterial3ExpressiveApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun HomeScreen(
homeScreenViewModel: HomeViewModel = hiltViewModel(),
- isMediumWindowSize: Boolean = isAtLeastMedium(),
onClickLetsGo: (IntOffset) -> Unit = {},
onAboutClicked: () -> Unit = {},
) {
val state = homeScreenViewModel.state.collectAsStateWithLifecycle()
+ val xrEnabled = state.value.isXrEnabled
+ val layoutType = calculateLayoutType(xrEnabled)
if (!state.value.isAppActive) {
AppInactiveScreen()
@@ -132,9 +50,10 @@ fun HomeScreen(
HomeScreenContents(
state.value.videoLink,
state.value.dancingDroidLink,
- isMediumWindowSize,
+ layoutType,
onClickLetsGo,
onAboutClicked,
+ xrEnabled,
)
}
}
@@ -143,188 +62,57 @@ fun HomeScreen(
fun HomeScreenContents(
videoLink: String?,
dancingBotLink: String?,
- isMediumWindowSize: Boolean,
+ layoutType: HomeScreenLayoutType,
onClickLetsGo: (IntOffset) -> Unit,
onAboutClicked: () -> Unit,
+ xrEnabled: Boolean = false,
) {
- Box {
- SquiggleBackground()
- var positionButtonClick by remember {
- mutableStateOf(IntOffset.Zero)
- }
- Column(
- modifier = Modifier
- .fillMaxSize()
- .safeDrawingPadding(),
- ) {
- if (isMediumWindowSize) {
- AndroidifyTranslucentTopAppBar(isMediumSizeLayout = true)
-
- Row(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 32.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Box(
- modifier = Modifier.weight(0.8f),
- ) {
- VideoPlayerRotatedCard(
- videoLink,
- modifier = Modifier
- .padding(32.dp)
- .align(Alignment.Center),
- )
- }
- Box(
- modifier = Modifier
- .weight(1.2f)
- .align(Alignment.CenterVertically),
- ) {
- MainHomeContent(dancingBotLink)
- HomePageButton(
- modifier = Modifier
- .onLayoutRectChanged {
- positionButtonClick = it.boundsInWindow.center
- }
- .align(Alignment.BottomCenter)
- .padding(bottom = 16.dp)
- .height(64.dp)
- .width(220.dp),
- onClick = {
- onClickLetsGo(positionButtonClick)
- },
- )
- }
- }
- } else {
- CompactPager(
+ when (layoutType) {
+ HomeScreenLayoutType.Compact ->
+ SquiggleBackgroundBox {
+ HomeScreenCompactPager(
videoLink,
dancingBotLink,
onClickLetsGo,
onAboutClicked,
+ xrEnabled,
)
}
- }
- }
-}
-
-@Composable
-private fun CompactPager(
- videoLink: String?,
- dancingBotLink: String?,
- onClick: (IntOffset) -> Unit,
- onAboutClicked: () -> Unit,
-) {
- val pagerState = rememberPagerState(pageCount = { 2 })
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- AndroidifyTopAppBar(
- aboutEnabled = true,
- onAboutClicked = onAboutClicked,
- )
- HorizontalPager(
- state = pagerState,
- modifier = Modifier.weight(.8f),
- beyondViewportPageCount = 1,
- ) { page ->
- if (page == 0) {
- MainHomeContent(
- dancingBotLink = dancingBotLink,
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .align(Alignment.CenterHorizontally),
+ HomeScreenLayoutType.Medium ->
+ SquiggleBackgroundBox {
+ HomeScreenMediumContents(
+ Modifier,
+ videoLink,
+ dancingBotLink,
+ onClickLetsGo,
+ onAboutClicked,
+ xrEnabled,
)
- } else {
- Box(modifier = Modifier.fillMaxSize()) {
- VideoPlayerRotatedCard(
- videoLink = videoLink,
- modifier = Modifier
- .padding(horizontal = 32.dp)
- .align(Alignment.Center),
- )
- }
}
- }
- Row(
- modifier = Modifier
- .weight(.1f)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- repeat(pagerState.pageCount) { iteration ->
- val isCurrent by remember { derivedStateOf { pagerState.currentPage == iteration } }
- val animatedColor by animateColorAsState(
- if (isCurrent) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onTertiary,
- label = "animatedFirstColor",
- )
- Box(
- modifier = Modifier
- .padding(2.dp)
- .clip(RoundedCornerShape(size = 16.dp))
- .animateContentSize()
- .background(
- color = animatedColor,
- shape = RoundedCornerShape(size = 16.dp),
- )
- .height(16.dp)
- .width(if (isCurrent) 40.dp else 16.dp),
- )
- }
- }
- Spacer(modifier = Modifier.size(12.dp))
- var buttonPosition by remember {
- mutableStateOf(IntOffset.Zero)
- }
- HomePageButton(
- modifier = Modifier
- .onLayoutRectChanged {
- buttonPosition = it.boundsInWindow.center
- }
- .padding(bottom = 16.dp)
- .height(64.dp)
- .width(220.dp),
- colors = ButtonDefaults.buttonColors()
- .copy(containerColor = Blue),
- onClick = {
- onClick(buttonPosition)
- },
- )
+ HomeScreenLayoutType.Spatial ->
+ HomeScreenContentsSpatial(
+ videoLink,
+ dancingBotLink,
+ onClickLetsGo,
+ onAboutClicked,
+ )
}
}
@Composable
-private fun VideoPlayerRotatedCard(
- videoLink: String?,
- modifier: Modifier = Modifier,
-) {
- val aspectRatio = 280f / 380f
- val videoInstructionText = stringResource(R.string.instruction_video_transcript)
- Box(
- modifier = modifier
- .focusable()
- .semantics { contentDescription = videoInstructionText }
- .aspectRatio(aspectRatio)
- .rotate(-3f)
- .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large)
- .background(
- color = Color.White,
- shape = MaterialTheme.shapes.large,
- ),
- ) {
- VideoPlayer(
- videoLink,
+private fun SquiggleBackgroundBox(contents: @Composable () -> Unit) {
+ Box {
+ SquiggleBackground()
+
+ Box(
modifier = Modifier
- .aspectRatio(aspectRatio)
- .align(Alignment.Center)
- .clip(MaterialTheme.shapes.large)
- .clipToBounds(),
- )
+ .fillMaxSize()
+ .safeDrawingPadding(),
+ ) {
+ contents()
+ }
}
}
@@ -334,7 +122,7 @@ private fun VideoPlayerRotatedCard(
private fun HomeScreenPhonePreview() {
SharedElementContextPreview {
HomeScreenContents(
- isMediumWindowSize = false,
+ layoutType = HomeScreenLayoutType.Compact,
onClickLetsGo = {},
videoLink = "",
dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
@@ -349,7 +137,7 @@ private fun HomeScreenPhonePreview() {
private fun HomeScreenLargeScreensPreview() {
SharedElementContextPreview {
HomeScreenContents(
- isMediumWindowSize = true,
+ layoutType = HomeScreenLayoutType.Medium,
onClickLetsGo = { },
videoLink = "",
dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
@@ -357,245 +145,3 @@ private fun HomeScreenLargeScreensPreview() {
)
}
}
-
-@Composable
-private fun MainHomeContent(
- dancingBotLink: String?,
- modifier: Modifier = Modifier,
-) {
- Column(
- modifier = modifier,
- ) {
- DecorativeSquiggleLimeGreen()
- DancingBotHeadlineText(
- dancingBotLink,
- modifier = Modifier.weight(1f),
- )
- DecorativeSquiggleLightGreen()
- }
-}
-
-@Composable
-private fun ColumnScope.DecorativeSquiggleLightGreen() {
- val infiniteAnimation = rememberInfiniteTransition()
- val rotationAnimation = infiniteAnimation.animateFloat(
- 0f,
- 720f,
- animationSpec = infiniteRepeatable(
- tween(12000, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse,
- ),
- )
- Image(
- painter = rememberVectorPainter(
- ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle_2),
- ),
- contentDescription = null, // decorative element
- modifier = Modifier
- .padding(start = 60.dp)
- .size(60.dp)
- .align(Alignment.Start)
- .graphicsLayer {
- rotationZ = rotationAnimation.value
- },
- )
-}
-
-@Composable
-private fun ColumnScope.DecorativeSquiggleLimeGreen() {
- val infiniteAnimation = rememberInfiniteTransition()
- val rotationAnimation = infiniteAnimation.animateFloat(
- 0f,
- -720f,
- animationSpec = infiniteRepeatable(
- tween(24000, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse,
- ),
- )
- Image(
- painter = rememberVectorPainter(
- ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle),
- ),
- contentDescription = null, // decorative element
- modifier = Modifier
- .padding(end = 80.dp)
- .size(60.dp)
- .align(Alignment.End)
- .graphicsLayer {
- rotationZ = rotationAnimation.value
- },
-
- )
-}
-
-@Preview
-@Composable
-private fun HomePageButton(
- modifier: Modifier = Modifier,
- colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue),
- onClick: () -> Unit = {},
-) {
- val style = MaterialTheme.typography.titleLarge.copy(
- fontWeight = FontWeight(700),
- letterSpacing = .15f.sp,
- )
-
- Button(
- onClick = onClick,
- modifier = modifier,
- colors = colors,
- ) {
- Text(
- stringResource(R.string.home_button_label),
- style = style,
- )
- }
-}
-
-@Composable
-private fun DancingBot(
- dancingBotLink: String?,
- modifier: Modifier,
-) {
- if (LocalInspectionMode.current) {
- Image(
- painter = painterResource(id = R.drawable.dancing_droid_gif_placeholder),
- contentDescription = null,
- modifier = modifier,
- )
- } else {
- AsyncImage(
- model = dancingBotLink,
- modifier = modifier,
- contentDescription = null,
- )
- }
-}
-
-@Composable
-private fun DancingBotHeadlineText(
- dancingBotLink: String?,
- modifier: Modifier = Modifier,
-) {
- Box(modifier = modifier) {
- val animatedBot = "animatedBot"
- val text = buildAnnotatedString {
- append(stringResource(R.string.customize_your_own))
- // Attach "animatedBot" annotation on the placeholder
- appendInlineContent(animatedBot)
- append(stringResource(R.string.into_an_android_bot))
- }
- var placeHolderSize by remember {
- mutableStateOf(220.sp)
- }
- val inlineContent = mapOf(
- Pair(
- animatedBot,
- InlineTextContent(
- Placeholder(
- width = placeHolderSize,
- height = placeHolderSize,
- placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
- ),
- ) {
- DancingBot(
- dancingBotLink,
- modifier = Modifier
- .padding(top = 32.dp)
- .fillMaxSize(),
- )
- },
- ),
- )
- BasicText(
- text,
- modifier = Modifier
- .align(Alignment.Center)
- .padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
- style = MaterialTheme.typography.titleLarge,
- autoSize = TextAutoSize.StepBased(maxFontSize = 220.sp),
- maxLines = 5,
- onTextLayout = { result ->
- placeHolderSize = result.layoutInput.style.fontSize * 3.5f
- },
- inlineContent = inlineContent,
- )
- }
-}
-
-@OptIn(UnstableApi::class) // New Media3 Compose artifact is currently experimental
-@Composable
-private fun VideoPlayer(
- videoLink: String?,
- modifier: Modifier = Modifier,
-) {
- if (LocalInspectionMode.current) {
- Image(
- painter = painterResource(id = R.drawable.promo_video_placeholder),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier = modifier,
- )
- return
- } else {
- val context = LocalContext.current
- var player by remember { mutableStateOf(null) }
- LifecycleStartEffect(videoLink) {
- if (videoLink != null) {
- player = ExoPlayer.Builder(context).build().apply {
- setMediaItem(MediaItem.fromUri(videoLink))
- repeatMode = Player.REPEAT_MODE_ONE
- prepare()
- }
- }
- onStopOrDispose {
- player?.release()
- player = null
- }
- }
-
- var videoFullyOnScreen by remember { mutableStateOf(false) }
- val isWindowOccluded = LocalOcclusion.current
- Box(
- Modifier
- .background(MaterialTheme.colorScheme.surfaceContainerLowest)
- .onVisibilityChanged(
- minDurationMs = 100,
- minFractionVisible = 1f,
- ) { fullyVisible -> videoFullyOnScreen = fullyVisible }
- .then(modifier),
- ) {
- player?.let { currentPlayer ->
- LaunchedEffect(videoFullyOnScreen, LocalOcclusion.current.value) {
- if (videoFullyOnScreen && !isWindowOccluded.value) currentPlayer.play() else currentPlayer.pause()
- }
-
- // Render the video
- PlayerSurface(currentPlayer, surfaceType = SURFACE_TYPE_TEXTURE_VIEW)
-
- // Show a play / pause button
- val playPauseButtonState = rememberPlayPauseButtonState(currentPlayer)
- OutlinedIconButton(
- onClick = playPauseButtonState::onClick,
- enabled = playPauseButtonState.isEnabled,
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp),
- colors = IconButtonDefaults.outlinedIconButtonColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
- disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
- ),
- ) {
- val icon =
- if (playPauseButtonState.showPlay) R.drawable.rounded_play_arrow_24 else R.drawable.rounded_pause_24
- val contentDescription =
- if (playPauseButtonState.showPlay) R.string.play else R.string.pause
- Icon(
- painterResource(icon),
- stringResource(contentDescription),
- )
- }
- }
- }
- }
-}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt
new file mode 100644
index 00000000..ec64d73e
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenCompact.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.android.developers.androidify.home
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+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.layout.onLayoutRectChanged
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.android.developers.androidify.theme.Blue
+import com.android.developers.androidify.theme.SharedElementContextPreview
+import com.android.developers.androidify.theme.components.AboutButton
+import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
+import com.android.developers.androidify.util.PhonePreview
+import com.android.developers.androidify.xr.RequestFullSpaceIconButton
+import com.android.developers.androidify.xr.couldRequestFullSpace
+
+@Composable
+fun HomeScreenCompactPager(
+ videoLink: String?,
+ dancingBotLink: String?,
+ onClick: (IntOffset) -> Unit,
+ onAboutClicked: () -> Unit,
+ xrEnabled: Boolean = false,
+) {
+ val pagerState = rememberPagerState(pageCount = { 2 })
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AndroidifyTopAppBar(
+ actions = {
+ AboutButton { onAboutClicked() }
+ if (xrEnabled && couldRequestFullSpace()) {
+ RequestFullSpaceIconButton()
+ }
+ },
+ )
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier.weight(.8f),
+ beyondViewportPageCount = 1,
+ ) { page ->
+ if (page == 0) {
+ MainHomeContent(
+ dancingBotLink = dancingBotLink,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .align(Alignment.CenterHorizontally),
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ VideoPlayerRotatedCard(
+ videoLink = videoLink,
+ modifier = Modifier
+ .padding(horizontal = 32.dp)
+ .align(Alignment.Center),
+ )
+ }
+ }
+ }
+ Row(
+ modifier = Modifier
+ .weight(.1f)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ repeat(pagerState.pageCount) { iteration ->
+ val isCurrent by remember { derivedStateOf { pagerState.currentPage == iteration } }
+ val animatedColor by animateColorAsState(
+ if (isCurrent) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onTertiary,
+ label = "animatedFirstColor",
+ )
+
+ Box(
+ modifier = Modifier
+ .padding(2.dp)
+ .clip(RoundedCornerShape(size = 16.dp))
+ .animateContentSize()
+ .background(
+ color = animatedColor,
+ shape = RoundedCornerShape(size = 16.dp),
+ )
+ .height(16.dp)
+ .width(if (isCurrent) 40.dp else 16.dp),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.size(12.dp))
+ var buttonPosition by remember {
+ mutableStateOf(IntOffset.Zero)
+ }
+ HomePageButton(
+ modifier = Modifier
+ .onLayoutRectChanged {
+ buttonPosition = it.boundsInWindow.center
+ }
+ .padding(bottom = 16.dp)
+ .height(64.dp)
+ .width(220.dp),
+ colors = ButtonDefaults.buttonColors()
+ .copy(containerColor = Blue),
+ onClick = {
+ onClick(buttonPosition)
+ },
+ )
+ }
+}
+
+@ExperimentalMaterial3ExpressiveApi
+@PhonePreview
+@Composable
+private fun HomeScreenPhonePreview() {
+ SharedElementContextPreview {
+ HomeScreenContents(
+ layoutType = HomeScreenLayoutType.Compact,
+ onClickLetsGo = {},
+ videoLink = "",
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onAboutClicked = {},
+ )
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt
new file mode 100644
index 00000000..678600e5
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenComponents.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.android.developers.androidify.home
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.foundation.text.TextAutoSize
+import androidx.compose.foundation.text.appendInlineContent
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+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.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.PlaceholderVerticalAlign
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil3.compose.AsyncImage
+import com.android.developers.androidify.theme.Blue
+import com.android.developers.androidify.theme.R as ThemeR
+
+@Composable
+fun MainHomeContent(
+ dancingBotLink: String?,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ DecorativeSquiggleLimeGreen()
+ DancingBotHeadlineText(
+ dancingBotLink,
+ modifier = Modifier.weight(1f),
+ )
+ DecorativeSquiggleLightGreen()
+ }
+}
+
+@Composable
+fun ColumnScope.DecorativeSquiggleLightGreen() {
+ val infiniteAnimation = rememberInfiniteTransition()
+ val rotationAnimation = infiniteAnimation.animateFloat(
+ 0f,
+ 720f,
+ animationSpec = infiniteRepeatable(
+ tween(12000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ )
+ Image(
+ painter = rememberVectorPainter(
+ ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle_2),
+ ),
+ contentDescription = null, // decorative element
+ modifier = Modifier
+ .padding(start = 60.dp)
+ .size(60.dp)
+ .align(Alignment.Start)
+ .graphicsLayer {
+ rotationZ = rotationAnimation.value
+ },
+ )
+}
+
+@Composable
+fun ColumnScope.DecorativeSquiggleLimeGreen() {
+ val infiniteAnimation = rememberInfiniteTransition()
+ val rotationAnimation = infiniteAnimation.animateFloat(
+ 0f,
+ -720f,
+ animationSpec = infiniteRepeatable(
+ tween(24000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse,
+ ),
+ )
+ Image(
+ painter = rememberVectorPainter(
+ ImageVector.vectorResource(ThemeR.drawable.decorative_squiggle),
+ ),
+ contentDescription = null, // decorative element
+ modifier = Modifier
+ .padding(end = 80.dp)
+ .size(60.dp)
+ .align(Alignment.End)
+ .graphicsLayer {
+ rotationZ = rotationAnimation.value
+ },
+
+ )
+}
+
+@Preview
+@Composable
+fun HomePageButton(
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = ButtonDefaults.buttonColors().copy(containerColor = Blue),
+ onClick: () -> Unit = {},
+) {
+ val style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight(700),
+ letterSpacing = .15f.sp,
+ )
+
+ Button(
+ onClick = onClick,
+ modifier = modifier,
+ colors = colors,
+ ) {
+ Text(
+ stringResource(R.string.home_button_label),
+ style = style,
+ )
+ }
+}
+
+@Composable
+private fun DancingBot(
+ dancingBotLink: String?,
+ modifier: Modifier,
+) {
+ if (LocalInspectionMode.current) {
+ Image(
+ painter = painterResource(id = R.drawable.dancing_droid_gif_placeholder),
+ contentDescription = null,
+ modifier = modifier,
+ )
+ } else {
+ AsyncImage(
+ model = dancingBotLink,
+ modifier = modifier,
+ contentDescription = null,
+ )
+ }
+}
+
+@Composable
+fun DancingBotHeadlineText(
+ dancingBotLink: String?,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier = modifier) {
+ val animatedBot = "animatedBot"
+ val text = buildAnnotatedString {
+ append(stringResource(R.string.customize_your_own))
+ // Attach "animatedBot" annotation on the placeholder
+ appendInlineContent(animatedBot)
+ append(stringResource(R.string.into_an_android_bot))
+ }
+ var placeHolderSize by remember {
+ mutableStateOf(220.sp)
+ }
+ val inlineContent = mapOf(
+ Pair(
+ animatedBot,
+ InlineTextContent(
+ Placeholder(
+ width = placeHolderSize,
+ height = placeHolderSize,
+ placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter,
+ ),
+ ) {
+ DancingBot(
+ dancingBotLink,
+ modifier = Modifier
+ .padding(top = 32.dp)
+ .fillMaxSize(),
+ )
+ },
+ ),
+ )
+ BasicText(
+ text,
+ modifier = Modifier
+ .align(Alignment.Center)
+ .padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ autoSize = TextAutoSize.StepBased(maxFontSize = 220.sp),
+ maxLines = 5,
+ onTextLayout = { result ->
+ placeHolderSize = result.layoutInput.style.fontSize * 3.5f
+ },
+ inlineContent = inlineContent,
+ )
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt
new file mode 100644
index 00000000..2828f9e2
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenLayoutType.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.android.developers.androidify.home
+
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.platform.LocalSpatialCapabilities
+import com.android.developers.androidify.util.isAtLeastMedium
+
+enum class HomeScreenLayoutType {
+ Compact,
+ Medium,
+ Spatial,
+}
+
+@Composable
+fun calculateLayoutType(enableXr: Boolean = false): HomeScreenLayoutType {
+ return when {
+ LocalSpatialCapabilities.current.isSpatialUiEnabled && enableXr -> HomeScreenLayoutType.Spatial
+ isAtLeastMedium() -> HomeScreenLayoutType.Medium
+ else -> HomeScreenLayoutType.Compact
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt
new file mode 100644
index 00000000..6d4cecbb
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenMedium.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.android.developers.androidify.home
+
+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.material3.ExperimentalMaterial3ExpressiveApi
+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.layout.onLayoutRectChanged
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.android.developers.androidify.theme.SharedElementContextPreview
+import com.android.developers.androidify.theme.components.AboutButton
+import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
+import com.android.developers.androidify.util.LargeScreensPreview
+import com.android.developers.androidify.xr.RequestFullSpaceIconButton
+import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider
+import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview
+import com.android.developers.androidify.xr.couldRequestFullSpace
+
+@Composable
+fun HomeScreenMediumContents(
+ modifier: Modifier,
+ videoLink: String?,
+ dancingBotLink: String?,
+ onClickLetsGo: (IntOffset) -> Unit,
+ onAboutClicked: () -> Unit,
+ xrEnabled: Boolean = false,
+) {
+ var positionButtonClick by remember {
+ mutableStateOf(IntOffset.Zero)
+ }
+ AndroidifyTopAppBar(
+ isMediumWindowSize = true,
+ actions = {
+ AboutButton(onAboutClicked = onAboutClicked)
+ if (xrEnabled && couldRequestFullSpace()) {
+ RequestFullSpaceIconButton()
+ }
+ },
+ )
+
+ Row(
+ modifier = modifier
+ .padding(horizontal = 32.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier.weight(0.8f),
+ ) {
+ VideoPlayerRotatedCard(
+ videoLink,
+ modifier = Modifier
+ .padding(32.dp)
+ .align(Alignment.Center),
+ )
+ }
+ Box(
+ modifier = Modifier
+ .weight(1.2f)
+ .align(Alignment.CenterVertically),
+ ) {
+ MainHomeContent(dancingBotLink)
+
+ HomePageButton(
+ modifier = Modifier
+ .onLayoutRectChanged {
+ positionButtonClick = it.boundsInWindow.center
+ }
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 16.dp)
+ .height(64.dp)
+ .width(220.dp),
+ onClick = {
+ onClickLetsGo(positionButtonClick)
+ },
+ )
+ }
+ }
+}
+
+@ExperimentalMaterial3ExpressiveApi
+@LargeScreensPreview
+@Composable
+private fun HomeScreenLargeScreensPreview() {
+ SharedElementContextPreview {
+ HomeScreenContents(
+ layoutType = HomeScreenLayoutType.Medium,
+ onClickLetsGo = { },
+ videoLink = "",
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onAboutClicked = {},
+ )
+ }
+}
+
+@ExperimentalMaterial3ExpressiveApi
+@XrHomeSpaceMediumPreview
+@Composable
+private fun HomeScreenLargeScreensHomeSpaceModePreview() {
+ SupportsFullSpaceModeRequestProvider {
+ SharedElementContextPreview {
+ HomeScreenContents(
+ layoutType = HomeScreenLayoutType.Medium,
+ onClickLetsGo = { },
+ videoLink = "",
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onAboutClicked = {},
+ )
+ }
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt
new file mode 100644
index 00000000..eb7c17ac
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeScreenVideoPlayer.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.android.developers.androidify.home
+
+import androidx.annotation.OptIn
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedIconButton
+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.clipToBounds
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onVisibilityChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LifecycleStartEffect
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.compose.PlayerSurface
+import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
+import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
+import com.android.developers.androidify.util.LocalOcclusion
+
+@OptIn(UnstableApi::class) // New Media3 Compose artifact is currently experimental
+@Composable
+fun VideoPlayer(
+ videoLink: String?,
+ modifier: Modifier = Modifier,
+) {
+ if (LocalInspectionMode.current) {
+ Image(
+ painter = painterResource(id = R.drawable.promo_video_placeholder),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = modifier,
+ )
+ return
+ } else {
+ val context = LocalContext.current
+ var player by remember { mutableStateOf(null) }
+ LifecycleStartEffect(videoLink) {
+ if (videoLink != null) {
+ player = ExoPlayer.Builder(context).build().apply {
+ setMediaItem(MediaItem.fromUri(videoLink))
+ repeatMode = Player.REPEAT_MODE_ONE
+ prepare()
+ }
+ }
+ onStopOrDispose {
+ player?.release()
+ player = null
+ }
+ }
+
+ var videoFullyOnScreen by remember { mutableStateOf(false) }
+ val isWindowOccluded = LocalOcclusion.current
+ Box(
+ Modifier
+ .background(MaterialTheme.colorScheme.surfaceContainerLowest)
+ .onVisibilityChanged(
+ minDurationMs = 100,
+ minFractionVisible = 1f,
+ ) { fullyVisible -> videoFullyOnScreen = fullyVisible }
+ .then(modifier),
+ ) {
+ player?.let { currentPlayer ->
+ LaunchedEffect(videoFullyOnScreen, LocalOcclusion.current.value) {
+ if (videoFullyOnScreen && !isWindowOccluded.value) currentPlayer.play() else currentPlayer.pause()
+ }
+
+ // Render the video
+ PlayerSurface(currentPlayer, surfaceType = SURFACE_TYPE_TEXTURE_VIEW)
+
+ // Show a play / pause button
+ val playPauseButtonState = rememberPlayPauseButtonState(currentPlayer)
+ OutlinedIconButton(
+ onClick = playPauseButtonState::onClick,
+ enabled = playPauseButtonState.isEnabled,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ colors = IconButtonDefaults.outlinedIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
+ disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
+ ),
+ ) {
+ val icon =
+ if (playPauseButtonState.showPlay) R.drawable.rounded_play_arrow_24 else R.drawable.rounded_pause_24
+ val contentDescription =
+ if (playPauseButtonState.showPlay) R.string.play else R.string.pause
+ Icon(
+ painterResource(icon),
+ stringResource(contentDescription),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun VideoPlayerRotatedCard(
+ videoLink: String?,
+ modifier: Modifier = Modifier,
+) {
+ val aspectRatio = 280f / 380f
+ val videoInstructionText = stringResource(R.string.instruction_video_transcript)
+ Box(
+ modifier = modifier
+ .focusable()
+ .semantics { contentDescription = videoInstructionText }
+ .aspectRatio(aspectRatio)
+ .rotate(-3f)
+ .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large)
+ .background(
+ color = Color.White,
+ shape = MaterialTheme.shapes.large,
+ ),
+ ) {
+ VideoPlayer(
+ videoLink,
+ modifier = Modifier
+ .aspectRatio(aspectRatio)
+ .align(Alignment.Center)
+ .clip(MaterialTheme.shapes.large)
+ .clipToBounds(),
+ )
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt
index 40d3ab14..51475ff5 100644
--- a/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/HomeViewModel.kt
@@ -29,6 +29,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo
isAppActive = !configProvider.isAppInactive(),
dancingDroidLink = configProvider.getDancingDroidLink(),
videoLink = configProvider.getPromoVideoLink(),
+ isXrEnabled = configProvider.isXrEnabled(),
),
)
val state = _state.asStateFlow()
@@ -36,6 +37,7 @@ class HomeViewModel @Inject constructor(configProvider: ConfigProvider) : ViewMo
data class HomeState(
val isAppActive: Boolean = true,
+ val isXrEnabled: Boolean = false,
val videoLink: String? = null,
val dancingDroidLink: String? = null,
)
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.kt
new file mode 100644
index 00000000..22df109a
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenHomeSpaceModePreviews.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.android.developers.androidify.home.xr
+
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.runtime.Composable
+import com.android.developers.androidify.home.HomeScreenContents
+import com.android.developers.androidify.home.HomeScreenLayoutType
+import com.android.developers.androidify.theme.SharedElementContextPreview
+import com.android.developers.androidify.xr.SupportsFullSpaceModeRequestProvider
+import com.android.developers.androidify.xr.XrHomeSpaceCompactPreview
+import com.android.developers.androidify.xr.XrHomeSpaceMediumPreview
+
+@XrHomeSpaceMediumPreview
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+private fun HomeScreenMediumXrHomeSpaceModePreview() {
+ SharedElementContextPreview {
+ SupportsFullSpaceModeRequestProvider {
+ HomeScreenContents(
+ layoutType = HomeScreenLayoutType.Medium,
+ onClickLetsGo = { },
+ videoLink = "",
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onAboutClicked = {},
+ )
+ }
+ }
+}
+
+@ExperimentalMaterial3ExpressiveApi
+@XrHomeSpaceCompactPreview
+@Composable
+private fun HomeScreenCompactXrHomeSpaceModePreview() {
+ SupportsFullSpaceModeRequestProvider {
+ SharedElementContextPreview {
+ HomeScreenContents(
+ layoutType = HomeScreenLayoutType.Compact,
+ onClickLetsGo = {},
+ videoLink = "",
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onAboutClicked = {},
+ )
+ }
+ }
+}
diff --git a/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt
new file mode 100644
index 00000000..38a5c4bd
--- /dev/null
+++ b/feature/home/src/main/java/com/android/developers/androidify/home/xr/HomeScreenSpatial.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.android.developers.androidify.home.xr
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+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.layout.onLayoutRectChanged
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+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.SpatialPanel
+import androidx.xr.compose.subspace.layout.SpatialAlignment
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.aspectRatio
+import androidx.xr.compose.subspace.layout.fillMaxHeight
+import androidx.xr.compose.subspace.layout.fillMaxWidth
+import androidx.xr.compose.subspace.layout.movable
+import androidx.xr.compose.subspace.layout.offset
+import androidx.xr.compose.subspace.layout.resizable
+import androidx.xr.compose.subspace.layout.rotate
+import com.android.developers.androidify.home.HomePageButton
+import com.android.developers.androidify.home.MainHomeContent
+import com.android.developers.androidify.home.VideoPlayer
+import com.android.developers.androidify.theme.SharedElementContextPreview
+import com.android.developers.androidify.theme.components.AboutButton
+import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
+import com.android.developers.androidify.theme.components.SquiggleBackgroundFull
+import com.android.developers.androidify.util.TabletPreview
+import com.android.developers.androidify.xr.DisableSharedTransition
+import com.android.developers.androidify.xr.MainPanelWorkaround
+import com.android.developers.androidify.xr.RequestHomeSpaceIconButton
+
+@Composable
+fun HomeScreenContentsSpatial(
+ videoLink: String?,
+ dancingBotLink: String?,
+ onClickLetsGo: (IntOffset) -> Unit,
+ onAboutClicked: () -> Unit,
+) {
+ DisableSharedTransition {
+ Subspace {
+ MainPanelWorkaround()
+ SpatialPanel(
+ SubspaceModifier
+ .movable()
+ .resizable()
+ .fillMaxWidth(1f)
+ .aspectRatio(1.7f),
+ ) {
+ Orbiter(
+ position = ContentEdge.Top,
+ offsetType = OrbiterOffsetType.OuterEdge,
+ offset = 32.dp,
+ alignment = Alignment.End,
+ ) {
+ RequestHomeSpaceIconButton(
+ modifier = Modifier
+ .size(64.dp, 64.dp)
+ .padding(8.dp),
+ )
+ }
+ HomeScreenSpatialMainContent(dancingBotLink, onClickLetsGo, onAboutClicked)
+ Subspace {
+ SpatialPanel(
+ SubspaceModifier
+ .fillMaxWidth(0.2f)
+ .fillMaxHeight(0.8f)
+ .aspectRatio(0.77f)
+ .resizable(maintainAspectRatio = true)
+ .movable()
+ .align(SpatialAlignment.CenterRight)
+ .offset(z = 10.dp)
+ .rotate(0f, 0f, 5f),
+ ) {
+ VideoPlayer(videoLink)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun HomeScreenSpatialMainContent(
+ dancingBotLink: String?,
+ onClickLetsGo: (IntOffset) -> Unit,
+ onAboutClicked: () -> Unit,
+) {
+ var positionButtonClick by remember {
+ mutableStateOf(IntOffset.Zero)
+ }
+ Box {
+ SquiggleBackgroundFull()
+ Column(
+ modifier = Modifier
+ .fillMaxWidth(0.55f)
+ .fillMaxHeight(1f)
+ .align(Alignment.Center),
+ ) {
+ AndroidifyTopAppBar(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .width(220.dp)
+ .padding(bottom = 16.dp, top = 48.dp),
+
+ actions = {
+ AboutButton { onAboutClicked() }
+ },
+ )
+ MainHomeContent(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .fillMaxHeight(0.8f),
+ dancingBotLink = dancingBotLink,
+ )
+
+ HomePageButton(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 48.dp)
+ .onLayoutRectChanged {
+ positionButtonClick = it.boundsInWindow.center
+ }
+ .height(64.dp)
+ .width(220.dp),
+ onClick = {
+ onClickLetsGo(positionButtonClick)
+ },
+ )
+ }
+ }
+}
+
+@TabletPreview
+@Composable
+private fun HomeScreenSpatialMainContentPreview() {
+ SharedElementContextPreview {
+ HomeScreenSpatialMainContent(
+ dancingBotLink = "https://services.google.com/fh/files/misc/android_dancing.gif",
+ onClickLetsGo = {},
+ onAboutClicked = {},
+ )
+ }
+}
diff --git a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt
index 5cdf0e62..49439c55 100644
--- a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt
+++ b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt
@@ -31,7 +31,7 @@ class HomeScreenScreenshotTest {
AndroidifyTheme {
SharedElementContextPreview {
HomeScreenContents(
- isMediumWindowSize = isAtLeastMedium(),
+ layoutType = calculateLayoutType(enableXr = false),
onClickLetsGo = { },
onAboutClicked = {},
videoLink = "",
diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt
index 9b5f8736..3eaffa62 100644
--- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt
+++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt
@@ -81,6 +81,7 @@ import com.android.developers.androidify.results.R
import com.android.developers.androidify.results.shareImage
import com.android.developers.androidify.theme.AndroidifyTheme
import com.android.developers.androidify.theme.LocalAnimateBoundsScope
+import com.android.developers.androidify.theme.components.AboutButton
import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
import com.android.developers.androidify.theme.components.PrimaryButton
import com.android.developers.androidify.theme.components.SecondaryOutlinedButton
@@ -175,7 +176,9 @@ private fun CustomizeExportContents(
titleText = stringResource(R.string.customize_and_export),
isMediumWindowSize = isMediumWindowSize,
onBackPressed = onBackPress,
- onAboutClicked = onInfoPress,
+ actions = {
+ AboutButton { onInfoPress() }
+ },
)
},
containerColor = MaterialTheme.colorScheme.surface,
diff --git a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt
index a6c2036c..96a685b6 100644
--- a/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt
+++ b/feature/results/src/main/java/com/android/developers/androidify/results/ResultsScreen.kt
@@ -67,6 +67,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.developers.androidify.theme.AndroidifyTheme
+import com.android.developers.androidify.theme.components.AboutButton
import com.android.developers.androidify.theme.components.AndroidifyTopAppBar
import com.android.developers.androidify.theme.components.PrimaryButton
import com.android.developers.androidify.theme.components.ResultsBackground
@@ -109,7 +110,9 @@ fun ResultsScreen(
onBackPressed = {
onBackPress()
},
- onAboutClicked = onAboutPress,
+ actions = {
+ AboutButton { onAboutPress() }
+ },
)
},
modifier = modifier
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9dedc003..5a6d434c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,6 +6,7 @@ agp = "8.11.1"
bcpkixJdk18on = "1.81"
compileSdk = "36"
core = "1.5.0"
+extensionsXr = "1.0.0"
leakcanaryAndroid = "2.14"
minSdk = "26"
javaVersion = "17"
@@ -81,6 +82,7 @@ lifecycleProcess = "2.9.1"
mlkitCommon = "18.11.0"
mlkitSegmentation = "16.0.0-beta1"
playServicesBase = "18.4.0"
+xr-compose = "1.0.0-alpha06"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
@@ -176,6 +178,9 @@ validator-push-cli = { module = "com.google.android.wearable.watchface.validator
watchface-push = { group = "androidx.wear.watchfacepush", name="watchfacepush", version.ref = "watchFacePush" }
play-services-base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "playServicesBase" }
google-firebase-appcheck-debug = { group = "com.google.firebase", name = "firebase-appcheck-debug"}
+androidx-xr-compose = { group = "androidx.xr.compose", name="compose", version.ref = "xr-compose"}
+androidx-xr-extensions = { module = "com.android.extensions.xr:extensions-xr", version.ref = "extensionsXr" }
+
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d9ff8b8c..7e3f7b83 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -39,6 +39,7 @@ include(":core:network")
include(":core:util")
include(":core:theme")
include(":core:testing")
+include(":core:xr")
include(":benchmark")
include(":watchface")
include(":wear")