├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .idea └── copyright │ ├── Apache_2_0.xml │ └── profiles_settings.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── debug │ └── screenshotTest │ │ └── reference │ │ └── com │ │ └── example │ │ └── helloandroidxr │ │ └── ui │ │ └── AppPreviewScreenshots │ │ ├── AppLayoutPreview_b6cdeb17_0.png │ │ ├── AppLayoutPreview_f367ca6d_0.png │ │ ├── SearchTextBoxPreview_b6cdeb17_0.png │ │ └── SearchTextBoxPreview_f367ca6d_0.png │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── green_hills_ktx2_mipmap.glb │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── helloandroidxr │ │ │ ├── MainActivity.kt │ │ │ ├── environment │ │ │ └── EnvironmentController.kt │ │ │ └── ui │ │ │ ├── HelloAndroidXRApp.kt │ │ │ ├── TextPane.kt │ │ │ ├── components │ │ │ ├── BugdroidModel.kt │ │ │ ├── EnvironmentControls.kt │ │ │ └── SearchBar.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── environment_24px.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_request_full_space.xml │ │ ├── ic_request_home_space.xml │ │ └── passthrough_24px.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ └── bugdroid_animated_wave.glb │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── screenshotTest │ └── kotlin │ └── com │ └── example │ └── helloandroidxr │ └── ui │ └── AppPreviewScreenshots.kt ├── build.gradle.kts ├── docs ├── code-of-conduct.md └── contributing.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Tests pass 6 | - [ ] Appropriate changes to documentation are included in the PR 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | paths-ignore: 8 | - '**.md' 9 | 10 | pull_request: 11 | branches: [ main ] 12 | paths-ignore: 13 | - '**.md' 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up JDK 17 26 | uses: actions/setup-java@v4 27 | with: 28 | distribution: 'adopt' 29 | java-version: 17 30 | 31 | - uses: gradle/actions/setup-gradle@v4 32 | 33 | - name: Build 34 | run: ./gradlew build 35 | 36 | - name: Test 37 | run: ./gradlew test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | build/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # Eclipse project files 21 | .classpath 22 | .project 23 | 24 | # Windows thumbnail db 25 | .DS_Store 26 | 27 | # IDEA/Android Studio project files, because 28 | # the project can be imported from settings.gradle.kts 29 | *.iml 30 | .idea/* 31 | !.idea/copyright 32 | # Keep the code styles. 33 | !/.idea/codeStyles 34 | /.idea/codeStyles/* 35 | !/.idea/codeStyles/Project.xml 36 | !/.idea/codeStyles/codeStyleConfig.xml 37 | 38 | # Gradle cache 39 | .gradle 40 | 41 | # Android Studio captures folder 42 | captures/ 43 | 44 | # Kotlin 45 | .kotlin -------------------------------------------------------------------------------- /.idea/copyright/Apache_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello Android XR 2 | 3 | This repository contains an Android Studio project that provides a straightforward example of the 4 | basic functionality afforded to Android apps in Android XR. 5 | 6 | For more information, please [read the documentation](https://developer.android.com/develop/xr). 7 | 8 | # Features 9 | 10 | In the sample you can see an implementation of: 11 | 12 | - Spatial Panels 13 | - Orbiters 14 | - Environments 15 | - and more 16 | 17 | # 💻 Development Environment 18 | 19 | **Hello Android XR** uses the Gradle build system and can be imported directly into Android Studio. 20 | Ensure you have the latest Canary version available, and update the XR emulator image in Android 21 | Studio's SDK Manager before creating a new XR Emulator. The Canary version of Android Studio is 22 | available [here](https://developer.android.com/studio/preview)). 23 | 24 | # Additional Resources 25 | 26 | - https://developer.android.com/xr 27 | - https://developer.android.com/develop/xr 28 | - https://developer.android.com/design/ui/xr 29 | - https://developer.android.com/develop/xr#bootcamp 30 | 31 | # License 32 | 33 | **Hello Android XR** is distributed under the terms of the Apache License (Version 2.0). See the 34 | [license](LICENSE) for more information. 35 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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 | plugins { 18 | alias(libs.plugins.android.application) 19 | alias(libs.plugins.kotlin.android) 20 | alias(libs.plugins.kotlin.compose) 21 | alias(libs.plugins.screenshot) 22 | } 23 | 24 | android { 25 | namespace = "com.example.helloandroidxr" 26 | compileSdk = 35 27 | 28 | defaultConfig { 29 | applicationId = "com.example.helloandroidxr" 30 | minSdk = 24 31 | targetSdk = 35 32 | versionCode = 1 33 | versionName = "1.0" 34 | 35 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | 38 | buildTypes { 39 | release { 40 | isMinifyEnabled = false 41 | proguardFiles( 42 | getDefaultProguardFile("proguard-android-optimize.txt"), 43 | "proguard-rules.pro" 44 | ) 45 | } 46 | } 47 | compileOptions { 48 | sourceCompatibility = JavaVersion.VERSION_1_8 49 | targetCompatibility = JavaVersion.VERSION_1_8 50 | } 51 | kotlinOptions { 52 | jvmTarget = "1.8" 53 | } 54 | buildFeatures { 55 | compose = true 56 | } 57 | composeOptions { 58 | kotlinCompilerExtensionVersion = "1.5.4" 59 | } 60 | experimentalProperties["android.experimental.enableScreenshotTest"] = true 61 | } 62 | 63 | dependencies { 64 | val composeBom = platform(libs.androidx.compose.bom) 65 | implementation(composeBom) 66 | implementation(libs.androidx.arcore) 67 | implementation(libs.androidx.scenecore) 68 | implementation(libs.androidx.compose) 69 | implementation(libs.kotlinx.coroutines.guava) 70 | 71 | implementation(libs.material) 72 | implementation(libs.androidx.compose.material3) 73 | implementation(libs.androidx.adaptive.android) 74 | implementation(libs.androidx.concurrent.futures) 75 | implementation(libs.androidx.compose.runtime) 76 | implementation(libs.androidx.activity.compose) 77 | 78 | implementation(libs.androidx.compose.ui.tooling) 79 | 80 | screenshotTestImplementation(libs.androidx.compose.ui.tooling) 81 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_b6cdeb17_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_b6cdeb17_0.png -------------------------------------------------------------------------------- /app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_f367ca6d_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/AppLayoutPreview_f367ca6d_0.png -------------------------------------------------------------------------------- /app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_b6cdeb17_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_b6cdeb17_0.png -------------------------------------------------------------------------------- /app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_f367ca6d_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/debug/screenshotTest/reference/com/example/helloandroidxr/ui/AppPreviewScreenshots/SearchTextBoxPreview_f367ca6d_0.png -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 22 | 23 | 24 | 34 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/assets/green_hills_ktx2_mipmap.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/assets/green_hills_ktx2_mipmap.glb -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import androidx.activity.enableEdgeToEdge 23 | import com.example.helloandroidxr.ui.HelloAndroidXRApp 24 | import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme 25 | 26 | class MainActivity : ComponentActivity() { 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | enableEdgeToEdge() 31 | setContent { 32 | HelloAndroidXRTheme { 33 | HelloAndroidXRApp() 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/environment/EnvironmentController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.environment 18 | 19 | import android.util.Log 20 | import androidx.concurrent.futures.await 21 | import androidx.xr.scenecore.GltfModel 22 | import androidx.xr.runtime.Session 23 | import androidx.xr.scenecore.SpatialEnvironment 24 | import androidx.xr.scenecore.scene 25 | import kotlinx.coroutines.CoroutineScope 26 | import kotlinx.coroutines.launch 27 | 28 | class EnvironmentController(private val xrSession: Session, private val coroutineScope: CoroutineScope) { 29 | private val assetCache: HashMap = HashMap() 30 | private var activeEnvironmentModelName: String? = null 31 | 32 | fun requestHomeSpaceMode() = xrSession.scene.spatialEnvironment.requestHomeSpaceMode() 33 | 34 | fun requestFullSpaceMode() = xrSession.scene.spatialEnvironment.requestFullSpaceMode() 35 | 36 | fun requestPassthrough() = xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(1f) 37 | 38 | /** 39 | * Request the system load a custom Environment 40 | */ 41 | fun requestCustomEnvironment(environmentModelName: String) { 42 | coroutineScope.launch { 43 | try { 44 | if (activeEnvironmentModelName == null || 45 | activeEnvironmentModelName != environmentModelName 46 | ) { 47 | 48 | val environmentModel = assetCache[environmentModelName] as GltfModel 49 | 50 | SpatialEnvironment.SpatialEnvironmentPreference( 51 | skybox = null, 52 | geometry = environmentModel 53 | ).let { 54 | xrSession.scene.spatialEnvironment.setSpatialEnvironmentPreference( 55 | it 56 | ) 57 | } 58 | activeEnvironmentModelName = environmentModelName 59 | } 60 | xrSession.scene.spatialEnvironment.setPassthroughOpacityPreference(0f) 61 | 62 | } catch (e: Exception) { 63 | Log.e( 64 | "Hello Android XR", 65 | "Failed to update Environment Preference for $environmentModelName: $e" 66 | ) 67 | } 68 | } 69 | } 70 | 71 | fun loadModelAsset(modelName: String) { 72 | coroutineScope.launch { 73 | //load the asset if it hasn't been loaded previously 74 | if (!assetCache.containsKey(modelName)) { 75 | try { 76 | val gltfModel = 77 | GltfModel.create(xrSession, modelName).await() 78 | assetCache[modelName] = gltfModel 79 | 80 | } catch (e: Exception) { 81 | Log.e( 82 | "Hello Android XR", 83 | "Failed to load model for $modelName: $e" 84 | ) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui 18 | 19 | import androidx.compose.animation.core.Animatable 20 | import androidx.compose.animation.core.FastOutSlowInEasing 21 | import androidx.compose.animation.core.tween 22 | import androidx.compose.foundation.layout.Arrangement 23 | import androidx.compose.foundation.layout.Box 24 | import androidx.compose.foundation.layout.Column 25 | import androidx.compose.foundation.layout.IntrinsicSize 26 | import androidx.compose.foundation.layout.Row 27 | import androidx.compose.foundation.layout.Spacer 28 | import androidx.compose.foundation.layout.fillMaxHeight 29 | import androidx.compose.foundation.layout.fillMaxSize 30 | import androidx.compose.foundation.layout.fillMaxWidth 31 | import androidx.compose.foundation.layout.height 32 | import androidx.compose.foundation.layout.padding 33 | import androidx.compose.foundation.layout.requiredHeight 34 | import androidx.compose.foundation.layout.systemBarsPadding 35 | import androidx.compose.foundation.layout.width 36 | import androidx.compose.foundation.rememberScrollState 37 | import androidx.compose.foundation.shape.RoundedCornerShape 38 | import androidx.compose.foundation.verticalScroll 39 | import androidx.compose.material3.Button 40 | import androidx.compose.material3.MaterialTheme 41 | import androidx.compose.material3.Surface 42 | import androidx.compose.material3.Text 43 | import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo 44 | import androidx.compose.runtime.Composable 45 | import androidx.compose.runtime.LaunchedEffect 46 | import androidx.compose.runtime.getValue 47 | import androidx.compose.runtime.mutableStateOf 48 | import androidx.compose.runtime.remember 49 | import androidx.compose.runtime.saveable.rememberSaveable 50 | import androidx.compose.runtime.setValue 51 | import androidx.compose.ui.Alignment 52 | import androidx.compose.ui.Modifier 53 | import androidx.compose.ui.draw.alpha 54 | import androidx.compose.ui.draw.clip 55 | import androidx.compose.ui.res.dimensionResource 56 | import androidx.compose.ui.res.stringResource 57 | import androidx.compose.ui.tooling.preview.Preview 58 | import androidx.compose.ui.unit.dp 59 | import androidx.window.core.layout.WindowSizeClass 60 | import androidx.window.core.layout.WindowWidthSizeClass 61 | import androidx.xr.compose.platform.LocalSpatialCapabilities 62 | import androidx.xr.compose.spatial.Orbiter 63 | import androidx.xr.compose.spatial.OrbiterEdge 64 | import androidx.xr.compose.spatial.Subspace 65 | import androidx.xr.compose.subspace.SpatialColumn 66 | import androidx.xr.compose.subspace.SpatialPanel 67 | import androidx.xr.compose.subspace.SpatialRow 68 | import androidx.xr.compose.subspace.layout.SubspaceModifier 69 | import androidx.xr.compose.subspace.layout.alpha 70 | import androidx.xr.compose.subspace.layout.fillMaxSize 71 | import androidx.xr.compose.subspace.layout.fillMaxWidth 72 | import androidx.xr.compose.subspace.layout.height 73 | import androidx.xr.compose.subspace.layout.movable 74 | import androidx.xr.compose.subspace.layout.padding 75 | import androidx.xr.compose.subspace.layout.resizable 76 | import androidx.xr.compose.subspace.layout.size 77 | import androidx.xr.compose.subspace.layout.width 78 | import com.example.helloandroidxr.R 79 | import com.example.helloandroidxr.ui.components.BugdroidModel 80 | import com.example.helloandroidxr.ui.components.EnvironmentControls 81 | import com.example.helloandroidxr.ui.components.SearchBar 82 | import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme 83 | import kotlinx.coroutines.launch 84 | 85 | @Composable 86 | fun HelloAndroidXRApp() { 87 | if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { 88 | SpatialLayout( 89 | primaryContent = { PrimaryContent() }, 90 | firstSupportingContent = { BlockOfContentOne() }, 91 | secondSupportingContent = { BlockOfContentTwo() } 92 | ) 93 | } else { 94 | NonSpatialTwoPaneLayout( 95 | secondaryPane = { 96 | BlockOfContentOne() 97 | BlockOfContentTwo() 98 | }, 99 | primaryPane = { PrimaryContent() } 100 | ) 101 | } 102 | } 103 | 104 | /** 105 | * Layout that displays content in [SpatialPanel]s, should be used when spatial UI is enabled. 106 | */ 107 | @Composable 108 | private fun SpatialLayout( 109 | primaryContent: @Composable () -> Unit, 110 | firstSupportingContent: @Composable () -> Unit, 111 | secondSupportingContent: @Composable () -> Unit 112 | ) { 113 | val animatedAlpha = remember { Animatable(0.5f) } 114 | LaunchedEffect(Unit) { 115 | launch { 116 | animatedAlpha.animateTo( 117 | 1.0f, 118 | animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) 119 | ) 120 | } 121 | } 122 | Subspace { 123 | SpatialRow(modifier = SubspaceModifier.height(816.dp).fillMaxWidth()) { 124 | SpatialColumn(modifier = SubspaceModifier.width(400.dp)) { 125 | SpatialPanel( 126 | SubspaceModifier 127 | .alpha(animatedAlpha.value) 128 | .size(400.dp) 129 | .padding(bottom = 16.dp) 130 | .movable() 131 | .resizable() 132 | ) { 133 | firstSupportingContent() 134 | } 135 | SpatialPanel( 136 | SubspaceModifier 137 | .alpha(animatedAlpha.value) 138 | .weight(1f) 139 | .movable() 140 | .resizable() 141 | ) { 142 | secondSupportingContent() 143 | } 144 | } 145 | SpatialPanel( 146 | modifier = SubspaceModifier 147 | .alpha(animatedAlpha.value) 148 | .fillMaxSize() 149 | .padding(left = 16.dp) 150 | .movable() 151 | .resizable() 152 | ) { 153 | Column { 154 | TopAppBar() 155 | primaryContent() 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Layout that displays content in a 2-pane layout, should be used when spatial UI is not enabled. 164 | */ 165 | @Composable 166 | private fun NonSpatialTwoPaneLayout( 167 | primaryPane: @Composable () -> Unit, 168 | secondaryPane: @Composable () -> Unit, 169 | modifier: Modifier = Modifier, 170 | windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass 171 | ) { 172 | val animatedAlpha = remember { Animatable(0.5f) } 173 | LaunchedEffect(Unit) { 174 | launch { 175 | animatedAlpha.animateTo( 176 | 1.0f, 177 | animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) 178 | ) 179 | } 180 | } 181 | Column( 182 | modifier = modifier 183 | .alpha(animatedAlpha.value) 184 | .padding(16.dp) 185 | .systemBarsPadding() 186 | ) { 187 | TopAppBar() 188 | Spacer(Modifier.height(16.dp)) 189 | if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT) { 190 | TopAndBottomPaneLayout(primaryPane, secondaryPane) 191 | } else { 192 | SideBySidePaneLayout(primaryPane, secondaryPane) 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * Positions the panes in a horizontal orientation 199 | */ 200 | @Composable 201 | private fun SideBySidePaneLayout( 202 | primaryPane: @Composable () -> Unit, 203 | secondaryPane: @Composable () -> Unit, 204 | modifier: Modifier = Modifier 205 | ) { 206 | Row(modifier) { 207 | Surface( 208 | Modifier 209 | .width(400.dp) 210 | .clip(RoundedCornerShape(16.dp)) 211 | ) { 212 | Column { 213 | secondaryPane() 214 | } 215 | } 216 | Spacer(Modifier.width(16.dp)) 217 | Surface(modifier.clip(RoundedCornerShape(16.dp))) { 218 | primaryPane() 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Positions the panes in a scrollable vertical orientation 225 | */ 226 | @Composable 227 | private fun TopAndBottomPaneLayout( 228 | primaryPane: @Composable () -> Unit, 229 | secondaryPane: @Composable () -> Unit, 230 | modifier: Modifier = Modifier 231 | ) { 232 | Column(modifier.verticalScroll(rememberScrollState())) { 233 | Surface(Modifier.requiredHeight(500.dp)) { 234 | primaryPane() 235 | } 236 | Spacer(Modifier.height(16.dp)) 237 | Surface( 238 | Modifier 239 | .requiredHeight(500.dp) 240 | .fillMaxWidth() 241 | .clip(RoundedCornerShape(16.dp)) 242 | ) { 243 | Column { 244 | secondaryPane() 245 | } 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * Contains controls that decompose into Orbiters when spatial UI is enabled 252 | */ 253 | @Composable 254 | private fun TopAppBar() { 255 | Row( 256 | horizontalArrangement = Arrangement.SpaceBetween, 257 | modifier = Modifier 258 | .height(IntrinsicSize.Min) 259 | .fillMaxWidth() 260 | ) { 261 | Spacer(Modifier.weight(1f)) 262 | Orbiter( 263 | position = OrbiterEdge.Top, 264 | offset = dimensionResource(R.dimen.top_ornament_padding), 265 | alignment = Alignment.Start 266 | ) { 267 | SearchBar() 268 | } 269 | Spacer(Modifier.weight(1f)) 270 | Orbiter( 271 | position = OrbiterEdge.Top, 272 | offset = dimensionResource(R.dimen.top_ornament_padding), 273 | alignment = Alignment.End 274 | ) { 275 | EnvironmentControls() 276 | } 277 | } 278 | } 279 | 280 | @Composable 281 | private fun PrimaryContent(modifier: Modifier = Modifier) { 282 | var showBugdroid by rememberSaveable { mutableStateOf(false) } 283 | 284 | if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { 285 | Surface(modifier.fillMaxSize()) { 286 | Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) { 287 | Button( 288 | onClick = { 289 | showBugdroid = true 290 | }, 291 | modifier = modifier 292 | ) { 293 | Text( 294 | text = stringResource(id = R.string.show_bugdroid), 295 | style = MaterialTheme.typography.labelLarge 296 | ) 297 | } 298 | BugdroidModel(showBugdroid = showBugdroid) 299 | } 300 | } 301 | } else { 302 | TextPane( 303 | text = stringResource(R.string.primary_content), 304 | modifier = modifier.clip(RoundedCornerShape(16.dp)) 305 | ) 306 | } 307 | } 308 | 309 | @Composable 310 | private fun BlockOfContentOne(modifier: Modifier = Modifier) { 311 | TextPane(stringResource(R.string.block_of_content_1), modifier = modifier.height(240.dp)) 312 | } 313 | 314 | @Composable 315 | private fun BlockOfContentTwo(modifier: Modifier = Modifier) { 316 | TextPane(stringResource(R.string.block_of_content_2), modifier = modifier.fillMaxHeight()) 317 | } 318 | 319 | @Composable 320 | @Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") 321 | @Preview(device = "spec:width=411dp,height=891dp") 322 | fun AppLayoutPreview() { 323 | HelloAndroidXRTheme { 324 | HelloAndroidXRApp() 325 | } 326 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/TextPane.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.material3.Surface 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme 28 | 29 | @Composable 30 | fun TextPane(text: String, modifier: Modifier = Modifier) { 31 | Surface(modifier = modifier.fillMaxSize()) { 32 | Text(text = text, modifier = Modifier.padding(16.dp)) 33 | } 34 | } 35 | 36 | @Composable 37 | @Preview 38 | private fun MyLayOutPreview() { 39 | HelloAndroidXRTheme { 40 | TextPane(modifier = Modifier, text = "Primary") 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt: -------------------------------------------------------------------------------- 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.helloandroidxr.ui.components 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.unit.dp 23 | import androidx.xr.compose.platform.LocalSession 24 | import androidx.xr.compose.spatial.Subspace 25 | import androidx.xr.compose.subspace.Volume 26 | import androidx.xr.compose.subspace.layout.SubspaceModifier 27 | import androidx.xr.compose.subspace.layout.offset 28 | import androidx.xr.scenecore.GltfModel 29 | import androidx.xr.scenecore.GltfModelEntity 30 | import com.example.helloandroidxr.R 31 | import kotlinx.coroutines.guava.await 32 | import kotlinx.coroutines.launch 33 | import java.io.InputStream 34 | 35 | @Composable 36 | fun BugdroidModel(showBugdroid: Boolean) { 37 | if (showBugdroid) { 38 | val xrSession = checkNotNull(LocalSession.current) 39 | val scope = rememberCoroutineScope() 40 | val context = LocalContext.current 41 | 42 | Subspace { 43 | val inputStream: InputStream = 44 | context.resources.openRawResource(R.raw.bugdroid_animated_wave) 45 | Volume( 46 | SubspaceModifier.offset(z = 400.dp) // Relative position 47 | ) { parent -> 48 | scope.launch { 49 | val gltfModel = GltfModel.create( 50 | session = xrSession, 51 | assetData = inputStream.readBytes(), 52 | assetKey = "BUGDROID" 53 | ).await() 54 | val gltfEntity = GltfModelEntity.create(xrSession, gltfModel) 55 | // Make this glTF a child of the Volume 56 | gltfEntity.setParent(parent) 57 | // Change the size of the large glTF to 10% 58 | gltfEntity.setScale(0.1f) 59 | gltfEntity.startAnimation( 60 | loop = true, 61 | animationName = "Armature|Take 001|BaseLayer" 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/components/EnvironmentControls.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui.components 18 | 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.LocalActivity 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.layout.IntrinsicSize 23 | import androidx.compose.foundation.layout.Row 24 | import androidx.compose.foundation.layout.height 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.size 27 | import androidx.compose.foundation.layout.width 28 | import androidx.compose.foundation.shape.CircleShape 29 | import androidx.compose.material3.Icon 30 | import androidx.compose.material3.IconButton 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.Surface 33 | import androidx.compose.material3.VerticalDivider 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.ui.Alignment 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.draw.clip 39 | import androidx.compose.ui.res.painterResource 40 | import androidx.compose.ui.res.stringResource 41 | import androidx.compose.ui.tooling.preview.Preview 42 | import androidx.compose.ui.unit.dp 43 | import androidx.lifecycle.lifecycleScope 44 | import androidx.xr.compose.platform.LocalSession 45 | import androidx.xr.compose.platform.LocalSpatialCapabilities 46 | import com.example.helloandroidxr.R 47 | import com.example.helloandroidxr.environment.EnvironmentController 48 | import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme 49 | 50 | /** 51 | * Controls for changing the user's Environment, and toggling between Home Space and Full Space 52 | */ 53 | @Composable 54 | fun EnvironmentControls(modifier: Modifier = Modifier) { 55 | // If we aren't able to access the session, these buttons wouldn't work and shouldn't be shown 56 | val activity = LocalActivity.current 57 | val session = LocalSession.current 58 | if (session != null && activity is ComponentActivity) { 59 | val uiIsSpatialized = LocalSpatialCapabilities.current.isSpatialUiEnabled 60 | val environmentController = remember(activity) { 61 | EnvironmentController(session, activity.lifecycleScope) 62 | } 63 | //load the model early so it's in memory for when we need it 64 | val environmentModelName = "green_hills_ktx2_mipmap.glb" 65 | environmentController.loadModelAsset(environmentModelName) 66 | 67 | Surface(modifier.clip(CircleShape)) { 68 | Row(Modifier.width(IntrinsicSize.Min)) { 69 | if (uiIsSpatialized) { 70 | SetVirtualEnvironmentButton { 71 | environmentController.requestCustomEnvironment( 72 | environmentModelName 73 | ) 74 | } 75 | SetPassthroughButton { environmentController.requestPassthrough() } 76 | VerticalDivider( 77 | modifier = Modifier 78 | .height(32.dp) 79 | .align(Alignment.CenterVertically), 80 | color = MaterialTheme.colorScheme.onSurface 81 | ) 82 | RequestHomeSpaceButton { environmentController.requestHomeSpaceMode() } 83 | } else { 84 | RequestFullSpaceButton { environmentController.requestFullSpaceMode() } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | private fun SetVirtualEnvironmentButton( 93 | modifier: Modifier = Modifier, onclick: () -> Unit 94 | ) { 95 | IconButton( 96 | onClick = onclick, 97 | modifier = modifier 98 | .padding(16.dp) 99 | .background(MaterialTheme.colorScheme.onSecondary, CircleShape) 100 | .size(56.dp) 101 | ) { 102 | Icon( 103 | painter = painterResource(R.drawable.environment_24px), 104 | contentDescription = stringResource(id = R.string.set_virtual_environment), 105 | ) 106 | } 107 | } 108 | 109 | @Composable 110 | private fun SetPassthroughButton( 111 | modifier: Modifier = Modifier, onclick: () -> Unit 112 | ) { 113 | IconButton( 114 | onClick = onclick, 115 | modifier = modifier 116 | .padding(top = 16.dp, bottom = 16.dp, end = 16.dp) 117 | .background(MaterialTheme.colorScheme.onSecondary, CircleShape) 118 | .size(56.dp) 119 | ) { 120 | Icon( 121 | painter = painterResource(R.drawable.passthrough_24px), 122 | contentDescription = stringResource(id = R.string.set_passthrough), 123 | ) 124 | } 125 | } 126 | 127 | @Composable 128 | private fun RequestHomeSpaceButton(onclick: () -> Unit) { 129 | IconButton( 130 | onClick = onclick, 131 | modifier = Modifier 132 | .padding(16.dp) 133 | .background(MaterialTheme.colorScheme.onSecondary, CircleShape) 134 | .size(56.dp) 135 | ) { 136 | Icon( 137 | painter = painterResource(id = R.drawable.ic_request_home_space), 138 | contentDescription = stringResource(R.string.enter_home_space_mode) 139 | ) 140 | } 141 | } 142 | 143 | @Composable 144 | private fun RequestFullSpaceButton(onclick: () -> Unit) { 145 | IconButton( 146 | onClick = onclick, modifier = Modifier.padding(8.dp) 147 | ) { 148 | Icon( 149 | painter = painterResource(id = R.drawable.ic_request_full_space), 150 | contentDescription = stringResource(R.string.enter_full_space_mode) 151 | ) 152 | } 153 | } 154 | 155 | @Preview 156 | @Composable 157 | private fun PreviewSetVirtualEnvironmentButton() { 158 | HelloAndroidXRTheme { 159 | SetVirtualEnvironmentButton {} 160 | } 161 | } 162 | 163 | @Preview 164 | @Composable 165 | private fun PreviewRequestHomeSpaceButton() { 166 | HelloAndroidXRTheme { 167 | RequestHomeSpaceButton {} 168 | } 169 | } 170 | 171 | @Preview 172 | @Composable 173 | private fun PreviewRequestFullSpaceButton() { 174 | HelloAndroidXRTheme { 175 | RequestFullSpaceButton {} 176 | } 177 | } 178 | 179 | @Preview 180 | @Composable 181 | private fun PreviewSetPassthroughButton() { 182 | HelloAndroidXRTheme { 183 | SetPassthroughButton {} 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/components/SearchBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui.components 18 | 19 | import android.content.Context 20 | import android.widget.Toast 21 | import androidx.compose.foundation.layout.height 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.foundation.layout.width 24 | import androidx.compose.foundation.shape.CircleShape 25 | import androidx.compose.foundation.text.KeyboardActions 26 | import androidx.compose.foundation.text.KeyboardOptions 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.filled.Search 29 | import androidx.compose.material3.Icon 30 | import androidx.compose.material3.Surface 31 | import androidx.compose.material3.Text 32 | import androidx.compose.material3.TextField 33 | import androidx.compose.material3.TextFieldDefaults 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Modifier 40 | import androidx.compose.ui.draw.clip 41 | import androidx.compose.ui.graphics.Color 42 | import androidx.compose.ui.platform.LocalContext 43 | import androidx.compose.ui.platform.LocalFocusManager 44 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 45 | import androidx.compose.ui.res.stringResource 46 | import androidx.compose.ui.text.input.ImeAction 47 | import androidx.compose.ui.tooling.preview.Preview 48 | import androidx.compose.ui.unit.dp 49 | import com.example.helloandroidxr.R 50 | import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme 51 | 52 | /** 53 | * A search textbox that expands its surface when spatial UI is enabled 54 | */ 55 | @Composable 56 | fun SearchBar(modifier: Modifier = Modifier) { 57 | val context = LocalContext.current 58 | Surface(modifier = modifier.clip(CircleShape)) { 59 | SearchTextBox( 60 | onSearch = { query -> 61 | showNotImplementedToast( 62 | query = query, 63 | context = context 64 | ) 65 | }, 66 | modifier = Modifier.padding(16.dp), 67 | ) 68 | } 69 | } 70 | 71 | @Composable 72 | fun SearchTextBox( 73 | onSearch: (query: String) -> Unit, 74 | modifier: Modifier = Modifier, 75 | ) { 76 | var text by remember { mutableStateOf("") } 77 | val focusManager = LocalFocusManager.current 78 | val keyboardController = LocalSoftwareKeyboardController.current 79 | 80 | TextField( 81 | value = text, 82 | onValueChange = { text = it }, 83 | placeholder = { 84 | Text( 85 | stringResource(R.string.search_product_name), 86 | ) 87 | }, 88 | leadingIcon = { Icon(Icons.Filled.Search, null) }, 89 | colors = TextFieldDefaults.colors( 90 | focusedIndicatorColor = Color.Transparent, 91 | unfocusedIndicatorColor = Color.Transparent 92 | ), 93 | singleLine = true, 94 | // keyboardOptions change the newline key to a search key on the soft keyboard 95 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), 96 | // keyboardActions submits the search query when the search key is pressed 97 | keyboardActions = KeyboardActions(onSearch = { 98 | onSearch(text) 99 | text = "" 100 | keyboardController?.hide() 101 | focusManager.clearFocus(force = true) 102 | }), 103 | modifier = modifier 104 | .width(640.dp) 105 | .height(56.dp) 106 | .clip(CircleShape) 107 | ) 108 | } 109 | 110 | private fun showNotImplementedToast(query: String, context: Context) { 111 | Toast.makeText( 112 | context, context.getString(R.string.search_is_not_implemented, query), Toast.LENGTH_SHORT 113 | ).show() 114 | } 115 | 116 | @Composable 117 | @Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") 118 | @Preview(device = "spec:width=411dp,height=891dp") 119 | fun SearchTextBoxPreview() { 120 | HelloAndroidXRTheme { 121 | SearchTextBox(onSearch = {}) 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | val PaneBackground = Color(0xFFFFF8F7) 21 | val AppContainerBackground = Color(0xFFF8EBE9) 22 | 23 | val Purple80 = Color(0xFFD0BCFF) 24 | val PurpleGrey80 = Color(0xFFCCC2DC) 25 | val Pink80 = Color(0xFFEFB8C8) 26 | 27 | val Purple40 = Color(0xFF6650a4) 28 | val PurpleGrey40 = Color(0xFF625b71) 29 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui.theme 18 | 19 | import android.os.Build 20 | import androidx.compose.foundation.isSystemInDarkTheme 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.darkColorScheme 23 | import androidx.compose.material3.dynamicDarkColorScheme 24 | import androidx.compose.material3.dynamicLightColorScheme 25 | import androidx.compose.material3.lightColorScheme 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.platform.LocalContext 28 | 29 | private val DarkColorScheme = darkColorScheme( 30 | primary = Purple80, 31 | secondary = PurpleGrey80, 32 | tertiary = Pink80 33 | ) 34 | 35 | private val LightColorScheme = lightColorScheme( 36 | primary = Purple40, 37 | secondary = PurpleGrey40, 38 | tertiary = Pink40 39 | 40 | /* Other default colors to override 41 | background = Color(0xFFFFFBFE), 42 | surface = Color(0xFFFFFBFE), 43 | onPrimary = Color.White, 44 | onSecondary = Color.White, 45 | onTertiary = Color.White, 46 | onBackground = Color(0xFF1C1B1F), 47 | onSurface = Color(0xFF1C1B1F), 48 | */ 49 | ) 50 | 51 | @Composable 52 | fun HelloAndroidXRTheme( 53 | darkTheme: Boolean = isSystemInDarkTheme(), 54 | // Dynamic color is available on Android 12+ 55 | dynamicColor: Boolean = true, 56 | content: @Composable () -> Unit 57 | ) { 58 | val colorScheme = when { 59 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 60 | val context = LocalContext.current 61 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 62 | } 63 | 64 | darkTheme -> DarkColorScheme 65 | else -> LightColorScheme 66 | } 67 | 68 | MaterialTheme( 69 | colorScheme = colorScheme, 70 | typography = Typography, 71 | content = content 72 | ) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/helloandroidxr/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 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.helloandroidxr.ui.theme 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | val Typography = Typography( 27 | bodyLarge = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp, 31 | lineHeight = 24.sp, 32 | letterSpacing = 0.5.sp 33 | ) 34 | /* Other default text styles to override 35 | titleLarge = TextStyle( 36 | fontFamily = FontFamily.Default, 37 | fontWeight = FontWeight.Normal, 38 | fontSize = 22.sp, 39 | lineHeight = 28.sp, 40 | letterSpacing = 0.sp 41 | ), 42 | labelSmall = TextStyle( 43 | fontFamily = FontFamily.Default, 44 | fontWeight = FontWeight.Medium, 45 | fontSize = 11.sp, 46 | lineHeight = 16.sp, 47 | letterSpacing = 0.5.sp 48 | ) 49 | */ 50 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/environment_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_request_full_space.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_request_home_space.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/passthrough_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/bugdroid_animated_wave.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/xr-samples/34263a4339406c2da5f06cf239c5424429adf24f/app/src/main/res/raw/bugdroid_animated_wave.glb -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | #FFBB86FC 20 | #FF6200EE 21 | #FF3700B3 22 | #FF03DAC5 23 | #FF018786 24 | #FF000000 25 | #FFFFFFFF 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 96dp 19 | 96dp 20 | 16dp 21 | 24dp 22 | 48dp 23 | 80dp 24 | 296dp 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Hello Android XR 19 | enter home space mode 20 | enter full space mode 21 | Primary content 22 | Block of Content\n1 23 | Block of Content\n2 24 | Searching for %s (not implemented) 25 | Search Product Name 26 | set virtual environment 27 | set passthrough 28 | Show bugdroid 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 |