Skip to content

Commit d30ae93

Browse files
authored
Merge branch 'main' into device_compatibility_mode
2 parents c5d9a23 + 4493417 commit d30ae93

File tree

10 files changed

+361
-0
lines changed

10 files changed

+361
-0
lines changed

.github/workflows/apply_spotless.yml

Lines changed: 3 additions & 0 deletions
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:

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
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

gradle/libs.versions.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ hilt = "2.55"
4343
horologist = "0.6.22"
4444
junit = "4.13.2"
4545
kotlin = "2.1.10"
46+
kotlinxCoroutinesGuava = "1.9.0"
4647
kotlinxSerializationJson = "1.8.0"
4748
ksp = "2.1.10-1.0.30"
4849
maps-compose = "6.4.4"
@@ -56,13 +57,15 @@ playServicesWearable = "19.0.0"
5657
protolayout = "1.2.1"
5758
recyclerview = "1.4.0"
5859
# @keep
60+
androidx-xr = "1.0.0-alpha02"
5961
targetSdk = "34"
6062
tiles = "1.4.1"
6163
version-catalog-update = "0.8.5"
6264
wear = "1.3.0"
6365
wearComposeFoundation = "1.4.1"
6466
wearComposeMaterial = "1.4.1"
6567
wearToolingPreview = "1.0.0"
68+
activityKtx = "1.10.0"
6669

6770
[libraries]
6871
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
@@ -138,6 +141,9 @@ androidx-window = { module = "androidx.window:window", version.ref = "androidx-w
138141
androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window-core" }
139142
androidx-window-java = {module = "androidx.window:window-java", version.ref = "androidx-window-java" }
140143
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.0"
144+
androidx-xr-arcore = { module = "androidx.xr.arcore:arcore", version.ref = "androidx-xr" }
145+
androidx-xr-compose = { module = "androidx.xr.compose:compose", version.ref = "androidx-xr" }
146+
androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.ref = "androidx-xr" }
141147
android-identity-googleid = {module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "android-googleid"}
142148
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
143149
coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
@@ -156,9 +162,11 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi
156162
junit = { module = "junit:junit", version.ref = "junit" }
157163
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
158164
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
165+
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }
159166
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
160167
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
161168
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
169+
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
162170

163171
[plugins]
164172
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.example.identity.credentialmanager
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.util.Log
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.LaunchedEffect
8+
import androidx.compose.runtime.rememberCoroutineScope
9+
import androidx.credentials.CredentialManager
10+
import androidx.credentials.GetCredentialRequest
11+
import androidx.credentials.GetCredentialResponse
12+
import androidx.credentials.GetPasswordOption
13+
import androidx.credentials.PasswordCredential
14+
import androidx.credentials.exceptions.GetCredentialCancellationException
15+
import androidx.credentials.exceptions.GetCredentialCustomException
16+
import androidx.credentials.exceptions.GetCredentialException
17+
import androidx.credentials.exceptions.GetCredentialInterruptedException
18+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
19+
import androidx.credentials.exceptions.GetCredentialUnknownException
20+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
21+
import androidx.credentials.exceptions.NoCredentialException
22+
import kotlinx.coroutines.CoroutineScope
23+
import kotlinx.coroutines.launch
24+
25+
class SmartLockToCredMan(
26+
private val credentialManager: CredentialManager,
27+
private val activityContext: Context,
28+
private val coroutineScope: CoroutineScope,
29+
) {
30+
// [START android_identity_init_password_option]
31+
// Retrieves the user's saved password for your app from their
32+
// password provider.
33+
val getPasswordOption = GetPasswordOption()
34+
// [END android_identity_init_password_option]
35+
36+
// [START android_identity_get_cred_request]
37+
val getCredRequest = GetCredentialRequest(
38+
listOf(getPasswordOption)
39+
)
40+
// [END android_identity_get_cred_request]
41+
42+
val TAG: String = "tag"
43+
44+
// [START android_identity_launch_sign_in_flow]
45+
fun launchSignInFlow() {
46+
coroutineScope.launch {
47+
try {
48+
// Attempt to retrieve the credential from the Credential Manager.
49+
val result = credentialManager.getCredential(
50+
// Use an activity-based context to avoid undefined system UI
51+
// launching behavior.
52+
context = activityContext,
53+
request = getCredRequest
54+
)
55+
56+
// Process the successfully retrieved credential.
57+
handleSignIn(result)
58+
} catch (e: GetCredentialException) {
59+
// Handle any errors that occur during the credential retrieval
60+
// process.
61+
handleFailure(e)
62+
}
63+
}
64+
}
65+
66+
private fun handleSignIn(result: GetCredentialResponse) {
67+
// Extract the credential from the response.
68+
val credential = result.credential
69+
70+
// Determine the type of credential and handle it accordingly.
71+
when (credential) {
72+
is PasswordCredential -> {
73+
val username = credential.id
74+
val password = credential.password
75+
76+
// Use the extracted username and password to perform
77+
// authentication.
78+
}
79+
80+
else -> {
81+
// Handle unrecognized credential types.
82+
Log.e(TAG, "Unexpected type of credential")
83+
}
84+
}
85+
}
86+
87+
private fun handleFailure(e: GetCredentialException) {
88+
// Handle specific credential retrieval errors.
89+
when (e) {
90+
is GetCredentialCancellationException -> {
91+
/* This exception is thrown when the user intentionally cancels
92+
the credential retrieval operation. Update the application's state
93+
accordingly. */
94+
}
95+
96+
is GetCredentialCustomException -> {
97+
/* This exception is thrown when a custom error occurs during the
98+
credential retrieval flow. Refer to the documentation of the
99+
third-party SDK used to create the GetCredentialRequest for
100+
handling this exception. */
101+
}
102+
103+
is GetCredentialInterruptedException -> {
104+
/* This exception is thrown when an interruption occurs during the
105+
credential retrieval flow. Determine whether to retry the
106+
operation or proceed with an alternative authentication method. */
107+
}
108+
109+
is GetCredentialProviderConfigurationException -> {
110+
/* This exception is thrown when there is a mismatch in
111+
configurations for the credential provider. Verify that the
112+
provider dependency is included in the manifest and that the
113+
required system services are enabled. */
114+
}
115+
116+
is GetCredentialUnknownException -> {
117+
/* This exception is thrown when the credential retrieval
118+
operation fails without providing any additional details. Handle
119+
the error appropriately based on the application's context. */
120+
}
121+
122+
is GetCredentialUnsupportedException -> {
123+
/* This exception is thrown when the device does not support the
124+
Credential Manager feature. Inform the user that credential-based
125+
authentication is unavailable and guide them to an alternative
126+
authentication method. */
127+
}
128+
129+
is NoCredentialException -> {
130+
/* This exception is thrown when there are no viable credentials
131+
available for the user. Prompt the user to sign up for an account
132+
or provide an alternative authentication method. Upon successful
133+
authentication, store the login information using
134+
androidx.credentials.CredentialManager.createCredential to
135+
facilitate easier sign-in the next time. */
136+
}
137+
138+
else -> {
139+
// Handle unexpected exceptions.
140+
Log.w(TAG, "Unexpected exception type: ${e::class.java.name}")
141+
}
142+
}
143+
}
144+
// [END android_identity_launch_sign_in_flow]
145+
}

settings.gradle.kts

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

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: 128 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)