diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2484ed0..f2909b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,17 +18,16 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - alias(libs.plugins.screenshot) } android { namespace = "com.example.helloandroidxr" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "com.example.helloandroidxr" minSdk = 24 - targetSdk = 35 + targetSdk = 36 versionCode = 1 versionName = "1.0" @@ -57,7 +56,6 @@ android { composeOptions { kotlinCompilerExtensionVersion = "1.5.4" } - experimentalProperties["android.experimental.enableScreenshotTest"] = true } dependencies { @@ -67,6 +65,7 @@ dependencies { implementation(libs.androidx.scenecore) implementation(libs.androidx.compose) implementation(libs.kotlinx.coroutines.guava) + compileOnly(libs.androidx.extensions.xr) //This is necessary for Proguard minification implementation(libs.material) implementation(libs.androidx.compose.material3) @@ -76,6 +75,4 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.ui.tooling) - - screenshotTestImplementation(libs.androidx.compose.ui.tooling) } \ No newline at end of file diff --git a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_b6cdeb17_0.png b/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_b6cdeb17_0.png deleted file mode 100644 index 8a8e0b5..0000000 Binary files a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_b6cdeb17_0.png and /dev/null differ diff --git a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_f367ca6d_0.png b/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_f367ca6d_0.png deleted file mode 100644 index 40359ab..0000000 Binary files a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_f367ca6d_0.png and /dev/null differ diff --git a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_b6cdeb17_0.png b/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_b6cdeb17_0.png deleted file mode 100644 index 437b637..0000000 Binary files a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_b6cdeb17_0.png and /dev/null differ diff --git a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_f367ca6d_0.png b/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_f367ca6d_0.png deleted file mode 100644 index 360dd15..0000000 Binary files a/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_f367ca6d_0.png and /dev/null differ diff --git a/app/src/main/java/com/example/helloandroidxr/environment/EnvironmentController.kt b/app/src/main/java/com/example/helloandroidxr/environment/EnvironmentController.kt index 4fd0874..3846a72 100644 --- a/app/src/main/java/com/example/helloandroidxr/environment/EnvironmentController.kt +++ b/app/src/main/java/com/example/helloandroidxr/environment/EnvironmentController.kt @@ -16,10 +16,10 @@ package com.example.helloandroidxr.environment +import android.net.Uri import android.util.Log -import androidx.concurrent.futures.await -import androidx.xr.scenecore.GltfModel import androidx.xr.runtime.Session +import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.SpatialEnvironment import androidx.xr.scenecore.scene import kotlinx.coroutines.CoroutineScope @@ -29,11 +29,13 @@ class EnvironmentController(private val xrSession: Session, private val coroutin private val assetCache: HashMap = HashMap() private var activeEnvironmentModelName: String? = null - fun requestHomeSpaceMode() = xrSession.scene.spatialEnvironment.requestHomeSpaceMode() + fun requestHomeSpaceMode() = xrSession.scene.requestHomeSpaceMode() - fun requestFullSpaceMode() = xrSession.scene.spatialEnvironment.requestFullSpaceMode() + fun requestFullSpaceMode() = xrSession.scene.requestFullSpaceMode() - fun requestPassthrough() = xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(1f) + fun requestPassthrough() { + xrSession.scene.spatialEnvironment.preferredPassthroughOpacity = 1f + } /** * Request the system load a custom Environment @@ -51,13 +53,11 @@ class EnvironmentController(private val xrSession: Session, private val coroutin skybox = null, geometry = environmentModel ).let { - xrSession.scene.spatialEnvironment.setSpatialEnvironmentPreference( - it - ) + xrSession.scene.spatialEnvironment.preferredSpatialEnvironment = it } activeEnvironmentModelName = environmentModelName } - xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(0f) + xrSession.scene.spatialEnvironment.preferredPassthroughOpacity = 0f } catch (e: Exception) { Log.e( @@ -74,7 +74,7 @@ class EnvironmentController(private val xrSession: Session, private val coroutin if (!assetCache.containsKey(modelName)) { try { val gltfModel = - GltfModel.create(xrSession, modelName).await() + GltfModel.create(xrSession, Uri.parse(modelName)) assetCache[modelName] = gltfModel } catch (e: Exception) { diff --git a/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt b/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt index 5bfdc25..a0676ab 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt @@ -59,8 +59,8 @@ import androidx.compose.ui.unit.dp import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass import androidx.xr.compose.platform.LocalSpatialCapabilities +import androidx.xr.compose.spatial.ContentEdge import androidx.xr.compose.spatial.Orbiter -import androidx.xr.compose.spatial.OrbiterEdge import androidx.xr.compose.spatial.Subspace import androidx.xr.compose.subspace.SpatialColumn import androidx.xr.compose.subspace.SpatialPanel @@ -71,6 +71,7 @@ import androidx.xr.compose.subspace.layout.fillMaxSize import androidx.xr.compose.subspace.layout.fillMaxWidth import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.padding import androidx.xr.compose.subspace.layout.resizable import androidx.xr.compose.subspace.layout.size @@ -260,7 +261,7 @@ private fun TopAppBar() { ) { Spacer(Modifier.weight(1f)) Orbiter( - position = OrbiterEdge.Top, + position = ContentEdge.Top, offset = dimensionResource(R.dimen.top_ornament_padding), alignment = Alignment.Start ) { @@ -268,7 +269,7 @@ private fun TopAppBar() { } Spacer(Modifier.weight(1f)) Orbiter( - position = OrbiterEdge.Top, + position = ContentEdge.Top, offset = dimensionResource(R.dimen.top_ornament_padding), alignment = Alignment.End ) { @@ -280,22 +281,28 @@ private fun TopAppBar() { @Composable private fun PrimaryContent(modifier: Modifier = Modifier) { var showBugdroid by rememberSaveable { mutableStateOf(false) } + val stringResId = if (showBugdroid) R.string.hide_bugdroid else R.string.show_bugdroid if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { Surface(modifier.fillMaxSize()) { Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) { Button( onClick = { - showBugdroid = true + showBugdroid = !showBugdroid }, modifier = modifier ) { Text( - text = stringResource(id = R.string.show_bugdroid), + text = stringResource(id = stringResId), style = MaterialTheme.typography.labelLarge ) } - BugdroidModel(showBugdroid = showBugdroid) + BugdroidModel( + showBugdroid = showBugdroid, + modifier = SubspaceModifier + .fillMaxSize() + .offset(z = 400.dp) // Relative position from the panel + ) } } } else { diff --git a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt index 2b73bc3..d64c73b 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt @@ -16,52 +16,113 @@ package com.example.helloandroidxr.ui.components +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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 import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalDensity import androidx.xr.compose.platform.LocalSession import androidx.xr.compose.spatial.Subspace -import androidx.xr.compose.subspace.Volume +import androidx.xr.compose.subspace.SceneCoreEntity +import androidx.xr.compose.subspace.SceneCoreEntitySizeAdapter import androidx.xr.compose.subspace.layout.SubspaceModifier -import androidx.xr.compose.subspace.layout.offset +import androidx.xr.compose.subspace.layout.scale +import androidx.xr.compose.unit.Meter +import androidx.xr.runtime.Session import androidx.xr.scenecore.GltfModel import androidx.xr.scenecore.GltfModelEntity import com.example.helloandroidxr.R -import kotlinx.coroutines.guava.await -import kotlinx.coroutines.launch import java.io.InputStream +// Bugdroid glb height in meters +private const val bugdroidHeight = 2.08f +// The desired amount of the available layout height to use for the bugdroid +private const val fillRatio = 0.5f + @Composable -fun BugdroidModel(showBugdroid: Boolean) { +fun BugdroidModel(showBugdroid: Boolean, modifier: SubspaceModifier = SubspaceModifier) { if (showBugdroid) { val xrSession = checkNotNull(LocalSession.current) - val scope = rememberCoroutineScope() + // Load the GltfModel data before creating the entity. + var gltfModel by remember { mutableStateOf(null) } val context = LocalContext.current - Subspace { - val inputStream: InputStream = - context.resources.openRawResource(R.raw.bugdroid_animated_wave) - Volume( - SubspaceModifier.offset(z = 400.dp) // Relative position - ) { parent -> - scope.launch { - val gltfModel = GltfModel.create( - session = xrSession, - assetData = inputStream.readBytes(), - assetKey = "BUGDROID" - ).await() - val gltfEntity = GltfModelEntity.create(xrSession, gltfModel) - // Make this glTF a child of the Volume - gltfEntity.setParent(parent) - // Change the size of the large glTF to 10% - gltfEntity.setScale(0.1f) - gltfEntity.startAnimation( - loop = true, - animationName = "Armature|Take 001|BaseLayer" - ) - } + LaunchedEffect(Unit) { + if (gltfModel == null) { + gltfModel = BugdroidGltfModelCache.getOrLoadModel(xrSession, context) + } + } + gltfModel?.let { gltfModel -> + Subspace { + val density = LocalDensity.current + var scale by remember { mutableFloatStateOf(1f) } + SceneCoreEntity( + factory = { + GltfModelEntity.create(xrSession, gltfModel).also { entity -> + entity.startAnimation( + loop = true, animationName = "Armature|Take 001|BaseLayer" + ) + } + }, + sizeAdapter = SceneCoreEntitySizeAdapter(onLayoutSizeChanged = { size -> + // Calculate the scale we should use for the entity based on the size the + // layout is setting on the SceneCoreEntity + val scaleToFillLayoutHeight = Meter + .fromPixel(size.height.toFloat(), density).toM() / bugdroidHeight + //Limit the scale to a ratio of the available space + scale = scaleToFillLayoutHeight * fillRatio + }), + modifier = modifier.scale(scale) + ) + } + } + } + // Clean up the cache when the composable leaves the composition. + DisposableEffect(Unit) { + onDispose { + BugdroidGltfModelCache.clearCache() + } + } +} + +/** + * Singleton object to cache the GltfModel. + */ +private object BugdroidGltfModelCache { + private var cachedModel: GltfModel? = null + + @SuppressLint("RestrictedApi") + suspend fun getOrLoadModel( + xrCoreSession: Session, context: Context + ): GltfModel? { + return if (cachedModel == null) { + try { + val inputStream: InputStream = + context.resources.openRawResource(R.raw.bugdroid_animated_wave) + cachedModel = GltfModel.create( + xrCoreSession, inputStream.readBytes(), "BUGDROID" + ) + cachedModel + } catch (e: Exception) { + Log.e(TAG, "Error loading GLTF model", e) + null } + } else { + cachedModel } } + + fun clearCache() { + cachedModel = null + } + + const val TAG = "BugdroidGltfModelCache" } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68d8030..e3dc530 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,5 +26,6 @@ set virtual environment set passthrough Show bugdroid + Hide bugdroid \ No newline at end of file diff --git a/app/src/screenshotTest/kotlin/com/example/helloandroidxr/ui/AppPreviewScreenshots.kt b/app/src/screenshotTest/kotlin/com/example/helloandroidxr/ui/AppPreviewScreenshots.kt deleted file mode 100644 index 5f4b194..0000000 --- a/app/src/screenshotTest/kotlin/com/example/helloandroidxr/ui/AppPreviewScreenshots.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.helloandroidxr.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme -import com.example.helloandroidxr.ui.components.SearchTextBox - -class AppPreviewScreenshots { - - @Composable - @Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") - @Preview(device = "spec:width=411dp,height=891dp") - fun AppLayoutPreview() { - HelloAndroidXRTheme { - HelloAndroidXRApp() - } - } - - @Composable - @Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") - @Preview(device = "spec:width=411dp,height=891dp") - fun SearchTextBoxPreview() { - HelloAndroidXRTheme { - SearchTextBox(onSearch = {}) - } - } - -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 0b4164d..0d6441c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,5 +37,3 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true - -android.experimental.enableScreenshotTest=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1db4002..303e80a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,24 @@ [versions] -androidx-runtime = "1.8.1" -agp = "8.10.0" -arcore = "1.0.0-alpha04" -compose = "1.0.0-alpha04" -scenecore = "1.0.0-alpha04" +androidx-runtime = "1.9.0" +agp = "8.12.0" +arcore = "1.0.0-alpha06" +compose = "1.0.0-alpha06" +extensionsXr = "1.0.0" +scenecore = "1.0.0-alpha06" kotlinxCoroutinesGuava = "1.10.2" -kotlin = "2.1.21" -concurrentFuturesKtx = "1.2.0" +kotlin = "2.2.0" +concurrentFuturesKtx = "1.3.0" activityCompose = "1.10.1" -composeBom = "2025.05.00" +composeBom = "2025.08.00" material = "1.12.0" -screenshot = "0.0.1-alpha09" adaptiveAndroid = "1.1.0" -kotlinAndroid = "2.1.21" +kotlinAndroid = "2.2.0" [libraries] androidx-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "arcore" } androidx-compose = { module = "androidx.xr.compose:compose", version.ref = "compose" } androidx-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "scenecore" } +androidx-extensions-xr = { module = "com.android.extensions.xr:extensions-xr", version.ref = "extensionsXr" } kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } material = { module = "com.google.android.material:material", version.ref = "material" } @@ -32,4 +33,3 @@ androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", nam android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinAndroid" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0ea1d9e..733e13b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -17,6 +17,6 @@ #Mon Aug 26 16:14:13 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists