Skip to content

Commit 365ef45

Browse files
committed
Add infrastructure for androidx.xr and snippets for ARCore for Jetpack XR Hands
1 parent d2ccac0 commit 365ef45

File tree

7 files changed

+182
-1
lines changed

7 files changed

+182
-1
lines changed

gradle/libs.versions.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ hilt = "2.55"
3535
horologist = "0.6.22"
3636
junit = "4.13.2"
3737
kotlin = "2.1.10"
38+
kotlinxCoroutinesGuava = "1.9.0"
3839
kotlinxSerializationJson = "1.8.0"
3940
ksp = "2.1.10-1.0.29"
4041
maps-compose = "6.4.2"
@@ -48,13 +49,15 @@ playServicesWearable = "19.0.0"
4849
protolayout = "1.2.1"
4950
recyclerview = "1.4.0"
5051
# @keep
52+
androidx-xr = "1.0.0-alpha02"
5153
targetSdk = "34"
5254
tiles = "1.4.1"
5355
version-catalog-update = "0.8.5"
5456
wear = "1.3.0"
5557
wearComposeFoundation = "1.4.0"
5658
wearComposeMaterial = "1.4.0"
5759
wearToolingPreview = "1.0.0"
60+
activityKtx = "1.10.0"
5861

5962
[libraries]
6063
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
@@ -124,6 +127,9 @@ androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" }
124127
androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" }
125128
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" }
126129
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.0"
130+
androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr" }
131+
androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr" }
132+
androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr" }
127133
coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
128134
compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" }
129135
compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" }
@@ -140,9 +146,11 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi
140146
junit = { module = "junit:junit", version.ref = "junit" }
141147
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
142148
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
149+
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
143150
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
144151
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
145152
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
153+
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
146154

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

settings.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ include(
2727
":compose:snippets",
2828
":wear",
2929
":views",
30-
":misc"
30+
":misc",
31+
":xr",
3132
)

xr/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

xr/build.gradle.kts

Lines changed: 34 additions & 0 deletions
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+
}

xr/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
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>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.example.xr.arcore
2+
3+
import android.annotation.SuppressLint
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.lifecycle.lifecycleScope
7+
import androidx.xr.arcore.Hand
8+
import androidx.xr.arcore.HandJointType
9+
import androidx.xr.compose.platform.setSubspaceContent
10+
import androidx.xr.runtime.Session
11+
import androidx.xr.runtime.math.Pose
12+
import androidx.xr.runtime.math.Quaternion
13+
import androidx.xr.runtime.math.Vector3
14+
import androidx.xr.scenecore.Entity
15+
import androidx.xr.scenecore.GltfModel
16+
import androidx.xr.scenecore.GltfModelEntity
17+
import kotlinx.coroutines.guava.await
18+
import kotlinx.coroutines.launch
19+
20+
class SampleHandsActivity : ComponentActivity() {
21+
lateinit var session: Session
22+
lateinit var scenecoreSession: androidx.xr.scenecore.Session
23+
lateinit var sessionHelper: SessionLifecycleHelper
24+
25+
var palmEntity: Entity? = null
26+
var indexFingerEntity: Entity? = null
27+
28+
override fun onCreate(savedInstanceState: Bundle?) {
29+
super.onCreate(savedInstanceState)
30+
setSubspaceContent { }
31+
32+
scenecoreSession = androidx.xr.scenecore.Session.create(this@SampleHandsActivity)
33+
lifecycleScope.launch {
34+
val model = GltfModel.create(scenecoreSession, "models/saturn_rings.glb").await()
35+
palmEntity = GltfModelEntity.create(scenecoreSession, model).apply {
36+
setScale(0.3f)
37+
setHidden(true)
38+
}
39+
indexFingerEntity = GltfModelEntity.create(scenecoreSession, model).apply {
40+
setScale(0.2f)
41+
setHidden(true)
42+
}
43+
}
44+
45+
sessionHelper = SessionLifecycleHelper(
46+
onCreateCallback = { session = it },
47+
onResumeCallback = {
48+
collectHands(session)
49+
}
50+
)
51+
lifecycle.addObserver(sessionHelper)
52+
}
53+
}
54+
55+
fun SampleHandsActivity.collectHands(session: Session) {
56+
lifecycleScope.launch {
57+
// [START androidxr_arcore_hand_collect]
58+
Hand.left(session)?.state?.collect { handState -> // or Hand.right(session)
59+
// Hand state has been updated.
60+
// Use the state of hand joints to update an entity's position.
61+
renderPlanetAtHandPalm(handState)
62+
}
63+
// [END androidxr_arcore_hand_collect]
64+
}
65+
lifecycleScope.launch {
66+
Hand.right(session)?.state?.collect { rightHandState ->
67+
renderPlanetAtFingerTip(rightHandState)
68+
}
69+
}
70+
}
71+
72+
@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
73+
fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) {
74+
val palmEntity = palmEntity ?: return
75+
// [START androidxr_arcore_hand_entityAtHandPalm]
76+
val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return
77+
78+
// the down direction points in the same direction as the palm
79+
val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up)
80+
palmEntity.setHidden(angle > Math.toRadians(40.0))
81+
82+
val transformedPose =
83+
scenecoreSession.perceptionSpace.transformPoseTo(
84+
palmPose,
85+
scenecoreSession.activitySpace,
86+
)
87+
val newPosition = transformedPose.translation + transformedPose.down * 0.05f
88+
palmEntity.setPose(Pose(newPosition, transformedPose.rotation))
89+
// [END androidxr_arcore_hand_entityAtHandPalm]
90+
}
91+
92+
@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504
93+
fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) {
94+
val indexFingerEntity = indexFingerEntity ?: return
95+
96+
// [START androidxr_arcore_hand_entityAtIndexFingerTip]
97+
val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return
98+
99+
// the forward direction points towards the finger tip.
100+
val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up)
101+
indexFingerEntity.setHidden(angle > Math.toRadians(40.0))
102+
103+
val transformedPose =
104+
scenecoreSession.perceptionSpace.transformPoseTo(
105+
tipPose,
106+
scenecoreSession.activitySpace,
107+
)
108+
val position = transformedPose.translation + transformedPose.forward * 0.03f
109+
val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up)
110+
indexFingerEntity.setPose(Pose(position, rotation))
111+
// [END androidxr_arcore_hand_entityAtIndexFingerTip]
112+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.example.xr.arcore
2+
3+
import androidx.lifecycle.DefaultLifecycleObserver
4+
import androidx.xr.runtime.Session
5+
6+
/**
7+
* 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).
8+
* This will be removed when Session becomes a LifecycleOwner in cl/726643897.
9+
*/
10+
class SessionLifecycleHelper(
11+
val onCreateCallback: (Session) -> Unit,
12+
13+
val onResumeCallback: (() -> Unit)? = null,
14+
) : DefaultLifecycleObserver {
15+
16+
}

0 commit comments

Comments
 (0)