Skip to content

Commit 4493417

Browse files
authored
Add infrastructure for androidx.xr and snippets for ARCore for Jetpack XR Hands (#459)
* Add infrastructure for androidx.xr and snippets for ARCore for Jetpack XR Hands * Add XR to spotless and build workflows * Apply Spotless --------- Co-authored-by: devbridie <[email protected]>
1 parent 1702667 commit 4493417

File tree

9 files changed

+216
-0
lines changed

9 files changed

+216
-0
lines changed

Diff for: .github/workflows/apply_spotless.yml

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ jobs:
5050
- name: Run spotlessApply for Misc
5151
run: ./gradlew :misc:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace
5252

53+
- name: Run spotlessApply for XR
54+
run: ./gradlew :xr:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace
55+
5356
- name: Auto-commit if spotlessApply has changes
5457
uses: stefanzweifel/git-auto-commit-action@v5
5558
with:

Diff for: .github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ jobs:
4747
run: ./gradlew :wear:build
4848
- name: Build misc snippets
4949
run: ./gradlew :misc:build
50+
- name: Build XR snippets
51+
run: ./gradlew :xr:build

Diff for: gradle/libs.versions.toml

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ hilt = "2.55"
4242
horologist = "0.6.22"
4343
junit = "4.13.2"
4444
kotlin = "2.1.10"
45+
kotlinxCoroutinesGuava = "1.9.0"
4546
kotlinxSerializationJson = "1.8.0"
4647
ksp = "2.1.10-1.0.30"
4748
maps-compose = "6.4.4"
@@ -55,13 +56,15 @@ playServicesWearable = "19.0.0"
5556
protolayout = "1.2.1"
5657
recyclerview = "1.4.0"
5758
# @keep
59+
androidx-xr = "1.0.0-alpha02"
5860
targetSdk = "34"
5961
tiles = "1.4.1"
6062
version-catalog-update = "0.8.5"
6163
wear = "1.3.0"
6264
wearComposeFoundation = "1.4.1"
6365
wearComposeMaterial = "1.4.1"
6466
wearToolingPreview = "1.0.0"
67+
activityKtx = "1.10.0"
6568

6669
[libraries]
6770
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
@@ -136,6 +139,9 @@ androidx-window = { module = "androidx.window:window", version.ref = "androidx-w
136139
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" }
137140
androidx-window-java = {module = "androidx.window:window-java", version.ref = "androidx-window-java" }
138141
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.0"
142+
androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr" }
143+
androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr" }
144+
androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr" }
139145
android-identity-googleid = {module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "android-googleid"}
140146
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
141147
coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
@@ -154,9 +160,11 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi
154160
junit = { module = "junit:junit", version.ref = "junit" }
155161
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
156162
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
163+
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
157164
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
158165
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
159166
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
167+
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
160168

161169
[plugins]
162170
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

Diff for: settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ include(
2929
":views",
3030
":misc",
3131
":identity:credentialmanager",
32+
":xr",
3233
)

Diff for: xr/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

Diff for: xr/build.gradle.kts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
}
5+
6+
android {
7+
namespace = "com.example.xr"
8+
compileSdk = 35
9+
10+
defaultConfig {
11+
applicationId = "com.example.xr"
12+
minSdk = 34
13+
targetSdk = 35
14+
versionCode = 1
15+
versionName = "1.0"
16+
}
17+
compileOptions {
18+
sourceCompatibility = JavaVersion.VERSION_11
19+
targetCompatibility = JavaVersion.VERSION_11
20+
}
21+
kotlinOptions {
22+
jvmTarget = "11"
23+
}
24+
}
25+
26+
dependencies {
27+
implementation(libs.androidx.xr.arcore)
28+
implementation(libs.androidx.xr.scenecore)
29+
implementation(libs.androidx.xr.compose)
30+
implementation(libs.androidx.activity.ktx)
31+
implementation(libs.guava)
32+
implementation(libs.kotlinx.coroutines.guava)
33+
34+
}

Diff for: xr/src/main/AndroidManifest.xml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:tools="http://schemas.android.com/tools"
3+
xmlns:android="http://schemas.android.com/apk/res/android">
4+
5+
<application
6+
android:label="XR"
7+
tools:ignore="MissingApplicationIcon" />
8+
9+
</manifest>

Diff for: xr/src/main/java/com/example/xr/arcore/Hands.kt

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.arcore
18+
19+
import android.annotation.SuppressLint
20+
import android.os.Bundle
21+
import androidx.activity.ComponentActivity
22+
import androidx.lifecycle.lifecycleScope
23+
import androidx.xr.arcore.Hand
24+
import androidx.xr.arcore.HandJointType
25+
import androidx.xr.compose.platform.setSubspaceContent
26+
import androidx.xr.runtime.Session
27+
import androidx.xr.runtime.math.Pose
28+
import androidx.xr.runtime.math.Quaternion
29+
import androidx.xr.runtime.math.Vector3
30+
import androidx.xr.scenecore.Entity
31+
import androidx.xr.scenecore.GltfModel
32+
import androidx.xr.scenecore.GltfModelEntity
33+
import kotlinx.coroutines.guava.await
34+
import kotlinx.coroutines.launch
35+
36+
class SampleHandsActivity : ComponentActivity() {
37+
lateinit var session: Session
38+
lateinit var scenecoreSession: androidx.xr.scenecore.Session
39+
lateinit var sessionHelper: SessionLifecycleHelper
40+
41+
var palmEntity: Entity? = null
42+
var indexFingerEntity: Entity? = null
43+
44+
override fun onCreate(savedInstanceState: Bundle?) {
45+
super.onCreate(savedInstanceState)
46+
setSubspaceContent { }
47+
48+
scenecoreSession = androidx.xr.scenecore.Session.create(this@SampleHandsActivity)
49+
lifecycleScope.launch {
50+
val model = GltfModel.create(scenecoreSession, "models/saturn_rings.glb").await()
51+
palmEntity = GltfModelEntity.create(scenecoreSession, model).apply {
52+
setScale(0.3f)
53+
setHidden(true)
54+
}
55+
indexFingerEntity = GltfModelEntity.create(scenecoreSession, model).apply {
56+
setScale(0.2f)
57+
setHidden(true)
58+
}
59+
}
60+
61+
sessionHelper = SessionLifecycleHelper(
62+
onCreateCallback = { session = it },
63+
onResumeCallback = {
64+
collectHands(session)
65+
}
66+
)
67+
lifecycle.addObserver(sessionHelper)
68+
}
69+
}
70+
71+
fun SampleHandsActivity.collectHands(session: Session) {
72+
lifecycleScope.launch {
73+
// [START androidxr_arcore_hand_collect]
74+
Hand.left(session)?.state?.collect { handState -> // or Hand.right(session)
75+
// Hand state has been updated.
76+
// Use the state of hand joints to update an entity's position.
77+
renderPlanetAtHandPalm(handState)
78+
}
79+
// [END androidxr_arcore_hand_collect]
80+
}
81+
lifecycleScope.launch {
82+
Hand.right(session)?.state?.collect { rightHandState ->
83+
renderPlanetAtFingerTip(rightHandState)
84+
}
85+
}
86+
}
87+
88+
@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
89+
fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
90+
val palmEntity = palmEntity ?: return
91+
// [START androidxr_arcore_hand_entityAtHandPalm]
92+
val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return
93+
94+
// the down direction points in the same direction as the palm
95+
val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up)
96+
palmEntity.setHidden(angle > Math.toRadians(40.0))
97+
98+
val transformedPose =
99+
scenecoreSession.perceptionSpace.transformPoseTo(
100+
palmPose,
101+
scenecoreSession.activitySpace,
102+
)
103+
val newPosition = transformedPose.translation + transformedPose.down * 0.05f
104+
palmEntity.setPose(Pose(newPosition, transformedPose.rotation))
105+
// [END androidxr_arcore_hand_entityAtHandPalm]
106+
}
107+
108+
@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
109+
fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
110+
val indexFingerEntity = indexFingerEntity ?: return
111+
112+
// [START androidxr_arcore_hand_entityAtIndexFingerTip]
113+
val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return
114+
115+
// the forward direction points towards the finger tip.
116+
val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up)
117+
indexFingerEntity.setHidden(angle > Math.toRadians(40.0))
118+
119+
val transformedPose =
120+
scenecoreSession.perceptionSpace.transformPoseTo(
121+
tipPose,
122+
scenecoreSession.activitySpace,
123+
)
124+
val position = transformedPose.translation + transformedPose.forward * 0.03f
125+
val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up)
126+
indexFingerEntity.setPose(Pose(position, rotation))
127+
// [END androidxr_arcore_hand_entityAtIndexFingerTip]
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.xr.arcore
18+
19+
import androidx.lifecycle.DefaultLifecycleObserver
20+
import androidx.xr.runtime.Session
21+
22+
/**
23+
* This is a dummy version of [SessionLifecycleHelper](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:xr/arcore/integration-tests/whitebox/src/main/kotlin/androidx/xr/arcore/apps/whitebox/common/SessionLifecycleHelper.kt).
24+
* This will be removed when Session becomes a LifecycleOwner in cl/726643897.
25+
*/
26+
class SessionLifecycleHelper(
27+
val onCreateCallback: (Session) -> Unit,
28+
29+
val onResumeCallback: (() -> Unit)? = null,
30+
) : DefaultLifecycleObserver

0 commit comments

Comments
 (0)