Skip to content
Merged
10 changes: 7 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ okHttp = "4.12.0"
playServicesWearable = "19.0.0"
protolayout = "1.2.1"
recyclerview = "1.4.0"
# @keep
androidx-xr-arcore = "1.0.0-alpha04"
androidx-xr-scenecore = "1.0.0-alpha04"
androidx-xr-compose = "1.0.0-alpha04"
targetSdk = "34"
tiles = "1.4.1"
version-catalog-update = "1.0.0"
Expand Down Expand Up @@ -149,9 +153,9 @@ androidx-window = { module = "androidx.window:window", version.ref = "androidx-w
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" }
androidx-window-java = { module = "androidx.window:window-java", version.ref = "androidx-window-java" }
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.1"
androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr" }
androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr" }
androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr" }
androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr-arcore" }
androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr-compose" }
androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr-scenecore" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" }
Expand Down
25 changes: 23 additions & 2 deletions xr/src/main/java/com/example/xr/arcore/Anchors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,31 @@ package com.example.xr.arcore
import androidx.xr.arcore.Anchor
import androidx.xr.arcore.AnchorCreateSuccess
import androidx.xr.arcore.Trackable
import androidx.xr.runtime.Config
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionConfigureConfigurationNotSupported
import androidx.xr.runtime.SessionConfigurePermissionsNotGranted
import androidx.xr.runtime.SessionConfigureSuccess
import androidx.xr.runtime.math.Pose
import androidx.xr.scenecore.AnchorEntity
import androidx.xr.scenecore.Entity
import androidx.xr.scenecore.scene

@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted
fun configureAnchoring(session: Session) {
// [START androidxr_arcore_anchoring_configure]
val newConfig = session.config.copy(
anchorPersistence = Config.AnchorPersistenceMode.Enabled,
)
when (val result = session.configure(newConfig)) {
is SessionConfigureConfigurationNotSupported ->
TODO(/* Some combinations of configurations are not valid. Handle this failure case. */)
is SessionConfigurePermissionsNotGranted ->
TODO(/* The required permissions in result.permissions have not been granted. */)
is SessionConfigureSuccess -> TODO(/* Success! */)
}
// [END androidxr_arcore_anchoring_configure]
}

private fun createAnchorAtPose(session: Session, pose: Pose) {
val pose = Pose()
Expand All @@ -45,13 +66,13 @@ private fun createAnchorAtTrackable(trackable: Trackable<*>) {
}

private fun attachEntityToAnchor(
session: androidx.xr.scenecore.Session,
session: Session,
entity: Entity,
anchor: Anchor
) {
// [START androidxr_arcore_entity_tracks_anchor]
AnchorEntity.create(session, anchor).apply {
setParent(session.activitySpace)
setParent(session.scene.activitySpace)
addChild(entity)
}
// [END androidxr_arcore_entity_tracks_anchor]
Expand Down
80 changes: 30 additions & 50 deletions xr/src/main/java/com/example/xr/arcore/Hands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,39 @@

package com.example.xr.arcore

import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import androidx.xr.arcore.Hand
import androidx.xr.arcore.HandJointType
import androidx.xr.compose.platform.setSubspaceContent
import androidx.xr.runtime.Config
import androidx.xr.runtime.HandJointType
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionConfigureConfigurationNotSupported
import androidx.xr.runtime.SessionConfigurePermissionsNotGranted
import androidx.xr.runtime.SessionConfigureSuccess
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Quaternion
import androidx.xr.runtime.math.Vector3
import androidx.xr.scenecore.Entity
import androidx.xr.scenecore.GltfModel
import androidx.xr.scenecore.GltfModelEntity
import kotlinx.coroutines.guava.await
import androidx.xr.scenecore.scene
import kotlinx.coroutines.launch

class SampleHandsActivity : ComponentActivity() {
lateinit var session: Session
lateinit var scenecoreSession: androidx.xr.scenecore.Session
lateinit var sessionHelper: SessionLifecycleHelper

var palmEntity: Entity? = null
var indexFingerEntity: Entity? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSubspaceContent { }

scenecoreSession = androidx.xr.scenecore.Session.create(this@SampleHandsActivity)
lifecycleScope.launch {
val model = GltfModel.create(scenecoreSession, "models/saturn_rings.glb").await()
palmEntity = GltfModelEntity.create(scenecoreSession, model).apply {
setScale(0.3f)
setHidden(true)
}
indexFingerEntity = GltfModelEntity.create(scenecoreSession, model).apply {
setScale(0.2f)
setHidden(true)
}
}

sessionHelper = SessionLifecycleHelper(
onCreateCallback = { session = it },
onResumeCallback = {
collectHands(session)
}
)
lifecycle.addObserver(sessionHelper)
@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted
fun ComponentActivity.configureSession(session: Session) {
// [START androidxr_arcore_hand_configure]
val newConfig = session.config.copy(
handTracking = Config.HandTrackingMode.Enabled
)
when (val result = session.configure(newConfig)) {
is SessionConfigureConfigurationNotSupported ->
TODO(/* Some combinations of configurations are not valid. Handle this failure case. */)
is SessionConfigurePermissionsNotGranted ->
TODO(/* The required permissions in result.permissions have not been granted. */)
is SessionConfigureSuccess -> TODO(/* Success! */)
}
// [END androidxr_arcore_hand_configure]
}

fun SampleHandsActivity.collectHands(session: Session) {
fun ComponentActivity.collectHands(session: Session) {
lifecycleScope.launch {
// [START androidxr_arcore_hand_collect]
Hand.left(session)?.state?.collect { handState -> // or Hand.right(session)
Expand All @@ -85,9 +65,9 @@ fun SampleHandsActivity.collectHands(session: Session) {
}
}

@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
val palmEntity = palmEntity ?: return
fun ComponentActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
val session: Session = null!!
val palmEntity: GltfModelEntity = null!!
// [START androidxr_arcore_hand_entityAtHandPalm]
val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return

Expand All @@ -96,18 +76,18 @@ fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
palmEntity.setHidden(angle > Math.toRadians(40.0))

val transformedPose =
scenecoreSession.perceptionSpace.transformPoseTo(
session.scene.perceptionSpace.transformPoseTo(
palmPose,
scenecoreSession.activitySpace,
session.scene.activitySpace,
)
val newPosition = transformedPose.translation + transformedPose.down * 0.05f
palmEntity.setPose(Pose(newPosition, transformedPose.rotation))
// [END androidxr_arcore_hand_entityAtHandPalm]
}

@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
val indexFingerEntity = indexFingerEntity ?: return
fun ComponentActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
val session: Session = null!!
val indexFingerEntity: GltfModelEntity = null!!

// [START androidxr_arcore_hand_entityAtIndexFingerTip]
val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return
Expand All @@ -117,9 +97,9 @@ fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
indexFingerEntity.setHidden(angle > Math.toRadians(40.0))

val transformedPose =
scenecoreSession.perceptionSpace.transformPoseTo(
session.scene.perceptionSpace.transformPoseTo(
tipPose,
scenecoreSession.activitySpace,
session.scene.activitySpace,
)
val position = transformedPose.translation + transformedPose.forward * 0.03f
val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up)
Expand Down
24 changes: 22 additions & 2 deletions xr/src/main/java/com/example/xr/arcore/Planes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,30 @@
package com.example.xr.arcore

import androidx.xr.arcore.Plane
import androidx.xr.runtime.Config
import androidx.xr.runtime.Session
import androidx.xr.runtime.SessionConfigureConfigurationNotSupported
import androidx.xr.runtime.SessionConfigurePermissionsNotGranted
import androidx.xr.runtime.SessionConfigureSuccess
import androidx.xr.runtime.math.Pose
import androidx.xr.runtime.math.Ray
import androidx.xr.scenecore.scene

@Suppress("RestrictedApi") // b/416288516 - session.config and session.configure() are incorrectly restricted
fun configurePlaneTracking(session: Session) {
// [START androidxr_arcore_planetracking_configure]
val newConfig = session.config.copy(
planeTracking = Config.PlaneTrackingMode.HorizontalAndVertical,
)
when (val result = session.configure(newConfig)) {
is SessionConfigureConfigurationNotSupported ->
TODO(/* Some combinations of configurations are not valid. Handle this failure case. */)
is SessionConfigurePermissionsNotGranted ->
TODO(/* The required permissions in result.permissions have not been granted. */)
is SessionConfigureSuccess -> TODO(/* Success! */)
}
// [END androidxr_arcore_planetracking_configure]
}

private suspend fun subscribePlanes(session: Session) {
// [START androidxr_arcore_planes_subscribe]
Expand All @@ -30,8 +51,7 @@ private suspend fun subscribePlanes(session: Session) {
}

private fun hitTestTable(session: Session) {
val scenecoreSession: androidx.xr.scenecore.Session = null!!
val pose = scenecoreSession.spatialUser.head?.transformPoseTo(Pose(), scenecoreSession.perceptionSpace) ?: return
val pose = session.scene.spatialUser.head?.transformPoseTo(Pose(), session.scene.perceptionSpace) ?: return
val ray = Ray(pose.translation, pose.forward)
// [START androidxr_arcore_hitTest]
val results = androidx.xr.arcore.hitTest(session, ray)
Expand Down
30 changes: 0 additions & 30 deletions xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt

This file was deleted.

71 changes: 71 additions & 0 deletions xr/src/main/java/com/example/xr/compose/SpatialExternalSurface.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.xr.compose

import android.content.ContentResolver
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialExternalSurface
import androidx.xr.compose.subspace.StereoMode
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.height
import androidx.xr.compose.subspace.layout.width

// [START androidxr_compose_SpatialExternalSurfaceStereo]
@Composable
fun SpatialExternalSurfaceContent() {
val context = LocalContext.current
Subspace {
SpatialExternalSurface(
modifier = SubspaceModifier
.width(1200.dp) // Default width is 400.dp if no width modifier is specified
.height(676.dp), // Default height is 400.dp if no height modifier is specified
// Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending
// upon which type of content you are rendering: monoscopic content, side-by-side stereo
// content, or top-bottom stereo content
stereoMode = StereoMode.SideBySide,
) {
val exoPlayer = remember { ExoPlayer.Builder(context).build() }
val videoUri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
// Represents a side-by-side stereo video, where each frame contains a pair of
// video frames arranged side-by-side. The frame on the left represents the left
// eye view, and the frame on the right represents the right eye view.
.path("sbs_video.mp4")
.build()
val mediaItem = MediaItem.fromUri(videoUri)

// onSurfaceCreated is invoked only one time, when the Surface is created
onSurfaceCreated { surface ->
exoPlayer.setVideoSurface(surface)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.play()
}
// onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its
// associated Surface are destroyed
onSurfaceDestroyed { exoPlayer.release() }
}
}
}
// [END androidxr_compose_SpatialExternalSurfaceStereo]
2 changes: 1 addition & 1 deletion xr/src/main/java/com/example/xr/compose/SpatialRow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import androidx.xr.compose.subspace.layout.width
@Composable
private fun SpatialRowExample() {
// [START androidxr_compose_SpatialRowExample]
SpatialRow(curveRadius = 825.dp) {
SpatialRow {
SpatialPanel(
SubspaceModifier
.width(384.dp)
Expand Down
3 changes: 2 additions & 1 deletion xr/src/main/java/com/example/xr/compose/Subspace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.xr.compose.spatial.ApplicationSubspace
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialPanel

Expand All @@ -32,7 +33,7 @@ private class SubspaceActivity : ComponentActivity() {
// [START androidxr_compose_SubspaceSetContent]
setContent {
// This is a top-level subspace
Subspace {
ApplicationSubspace {
SpatialPanel {
MyComposable()
}
Expand Down
Loading