From 365ef45110feda7e45fd25f94c36ded9b35029d4 Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 20 Feb 2025 11:42:40 +0100 Subject: [PATCH 1/3] Add infrastructure for androidx.xr and snippets for ARCore for Jetpack XR Hands --- gradle/libs.versions.toml | 8 ++ settings.gradle.kts | 3 +- xr/.gitignore | 1 + xr/build.gradle.kts | 34 ++++++ xr/src/main/AndroidManifest.xml | 9 ++ .../main/java/com/example/xr/arcore/Hands.kt | 112 ++++++++++++++++++ .../xr/arcore/SessionLifecycleHelper.kt | 16 +++ 7 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 xr/.gitignore create mode 100644 xr/build.gradle.kts create mode 100644 xr/src/main/AndroidManifest.xml create mode 100644 xr/src/main/java/com/example/xr/arcore/Hands.kt create mode 100644 xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fdc4663df..6662e36ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ hilt = "2.55" horologist = "0.6.22" junit = "4.13.2" kotlin = "2.1.10" +kotlinxCoroutinesGuava = "1.9.0" kotlinxSerializationJson = "1.8.0" ksp = "2.1.10-1.0.29" maps-compose = "6.4.2" @@ -48,6 +49,7 @@ playServicesWearable = "19.0.0" protolayout = "1.2.1" recyclerview = "1.4.0" # @keep +androidx-xr = "1.0.0-alpha02" targetSdk = "34" tiles = "1.4.1" version-catalog-update = "0.8.5" @@ -55,6 +57,7 @@ wear = "1.3.0" wearComposeFoundation = "1.4.0" wearComposeMaterial = "1.4.0" wearToolingPreview = "1.0.0" +activityKtx = "1.10.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -124,6 +127,9 @@ androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" } androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.0" +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" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } @@ -140,9 +146,11 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 65b99c5b1..cd691ab22 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,5 +27,6 @@ include( ":compose:snippets", ":wear", ":views", - ":misc" + ":misc", + ":xr", ) diff --git a/xr/.gitignore b/xr/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/xr/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts new file mode 100644 index 000000000..afbff0151 --- /dev/null +++ b/xr/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.xr" + compileSdk = 35 + + defaultConfig { + applicationId = "com.example.xr" + minSdk = 34 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.xr.arcore) + implementation(libs.androidx.xr.scenecore) + implementation(libs.androidx.xr.compose) + implementation(libs.androidx.activity.ktx) + implementation(libs.guava) + implementation(libs.kotlinx.coroutines.guava) + +} \ No newline at end of file diff --git a/xr/src/main/AndroidManifest.xml b/xr/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6d6399c14 --- /dev/null +++ b/xr/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt new file mode 100644 index 000000000..1ea8c4195 --- /dev/null +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -0,0 +1,112 @@ +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.Session +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 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) + } +} + +fun SampleHandsActivity.collectHands(session: Session) { + lifecycleScope.launch { + // [START androidxr_arcore_hand_collect] + Hand.left(session)?.state?.collect { handState -> // or Hand.right(session) + // Hand state has been updated. + // Use the state of hand joints to update an entity's position. + renderPlanetAtHandPalm(handState) + } + // [END androidxr_arcore_hand_collect] + } + lifecycleScope.launch { + Hand.right(session)?.state?.collect { rightHandState -> + renderPlanetAtFingerTip(rightHandState) + } + } +} + +@SuppressLint("RestrictedApi") // HandJointType is mistakenly @Restrict: b/397415504 +fun SampleHandsActivity.renderPlanetAtHandPalm(leftHandState: Hand.State) { + val palmEntity = palmEntity ?: return + // [START androidxr_arcore_hand_entityAtHandPalm] + val palmPose = leftHandState.handJoints[HandJointType.PALM] ?: return + + // the down direction points in the same direction as the palm + val angle = Vector3.angleBetween(palmPose.rotation * Vector3.Down, Vector3.Up) + palmEntity.setHidden(angle > Math.toRadians(40.0)) + + val transformedPose = + scenecoreSession.perceptionSpace.transformPoseTo( + palmPose, + scenecoreSession.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 + + // [START androidxr_arcore_hand_entityAtIndexFingerTip] + val tipPose = rightHandState.handJoints[HandJointType.INDEX_TIP] ?: return + + // the forward direction points towards the finger tip. + val angle = Vector3.angleBetween(tipPose.rotation * Vector3.Forward, Vector3.Up) + indexFingerEntity.setHidden(angle > Math.toRadians(40.0)) + + val transformedPose = + scenecoreSession.perceptionSpace.transformPoseTo( + tipPose, + scenecoreSession.activitySpace, + ) + val position = transformedPose.translation + transformedPose.forward * 0.03f + val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up) + indexFingerEntity.setPose(Pose(position, rotation)) + // [END androidxr_arcore_hand_entityAtIndexFingerTip] +} \ No newline at end of file diff --git a/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt b/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt new file mode 100644 index 000000000..93c706796 --- /dev/null +++ b/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt @@ -0,0 +1,16 @@ +package com.example.xr.arcore + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.xr.runtime.Session + +/** + * 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). + * This will be removed when Session becomes a LifecycleOwner in cl/726643897. + */ +class SessionLifecycleHelper( + val onCreateCallback: (Session) -> Unit, + + val onResumeCallback: (() -> Unit)? = null, + ) : DefaultLifecycleObserver { + +} \ No newline at end of file From 43e748b10c269ba71423bc57b4493204abbd8a9a Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Thu, 20 Feb 2025 16:46:18 +0100 Subject: [PATCH 2/3] Add XR to spotless and build workflows --- .github/workflows/apply_spotless.yml | 3 +++ .github/workflows/build.yml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index 5ba1f6fe5..0c8dcce4f 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -50,6 +50,9 @@ jobs: - name: Run spotlessApply for Misc run: ./gradlew :misc:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + - name: Run spotlessApply for XR + run: ./gradlew :xr:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + - name: Auto-commit if spotlessApply has changes uses: stefanzweifel/git-auto-commit-action@v5 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97b36f468..b5124ef05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,3 +47,5 @@ jobs: run: ./gradlew :wear:build - name: Build misc snippets run: ./gradlew :misc:build + - name: Build XR snippets + run: ./gradlew :xr:build From 5ad4e2797ebd880afad5c5559f910fa0160b496c Mon Sep 17 00:00:00 2001 From: devbridie <442644+devbridie@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:48:34 +0000 Subject: [PATCH 3/3] Apply Spotless --- .../main/java/com/example/xr/arcore/Hands.kt | 18 ++++++++++++++++- .../xr/arcore/SessionLifecycleHelper.kt | 20 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/xr/src/main/java/com/example/xr/arcore/Hands.kt b/xr/src/main/java/com/example/xr/arcore/Hands.kt index 1ea8c4195..26cc0ba8e 100644 --- a/xr/src/main/java/com/example/xr/arcore/Hands.kt +++ b/xr/src/main/java/com/example/xr/arcore/Hands.kt @@ -1,3 +1,19 @@ +/* + * 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.arcore import android.annotation.SuppressLint @@ -109,4 +125,4 @@ fun SampleHandsActivity.renderPlanetAtFingerTip(rightHandState: Hand.State) { val rotation = Quaternion.fromLookTowards(transformedPose.up, Vector3.Up) indexFingerEntity.setPose(Pose(position, rotation)) // [END androidxr_arcore_hand_entityAtIndexFingerTip] -} \ No newline at end of file +} diff --git a/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt b/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt index 93c706796..77462257f 100644 --- a/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt +++ b/xr/src/main/java/com/example/xr/arcore/SessionLifecycleHelper.kt @@ -1,3 +1,19 @@ +/* + * 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.arcore import androidx.lifecycle.DefaultLifecycleObserver @@ -11,6 +27,4 @@ class SessionLifecycleHelper( val onCreateCallback: (Session) -> Unit, val onResumeCallback: (() -> Unit)? = null, - ) : DefaultLifecycleObserver { - -} \ No newline at end of file +) : DefaultLifecycleObserver